Javascriptcore Call Function with Callback Swift

JavascriptCore: executing a javascript-defined callback function from native code

Ok I sorted this out. Posting this in case anyone else comes across this.

The problem is in the Swift code where the JSValue is coerced to a dictionary:

parameters : [String:AnyObject] = unsafeParameters as? [String: AnyObject]

which uses toDictionary(), and doing so seems to throw away the function property.

Instead the JSValue should be kept in tact and then valueForProperty is use to get the function which is itself a JSValue.

let actionValue = parameters.valueForProperty("action");
actionValue.callWithArguments(nil);

Calling swift Code From Javascript

Yes you can.

Please refer to this Tutorial: https://www.raywenderlich.com/124075/javascriptcore-tutorial

You are interested in the paragraph "Exposing Native Code"

In obj-c is very simple. You can do it just by using your jsContext as a normal dictionary.

context[@"functionName"] = ^ <#returnType#> (<#parameters#>) {
<#Your code#>
}

Hope it helped.

How to pause an asynchronous Swift function until a callback is called when a result is available from another library(JavaScriptCore)

Okay, as it turns out, Swift async/await concurrency does have a support for continuation. It's not mentioned on the main article on Swift documentation and most 3rd party articles bill this feature as a way to integrate the old callback centric concurrency with the new async/await API, however this is much more useful than simply integrating the old code with the new one.

Continuation provides you with an asynchronous function that can be called from within your async function to pause execution with await until you explicitly resume it. That means, you can await input from the user or another library that uses callback to deliver the results. That also means, you are entirely responsible to correctly resume the function.

In my case, I'm providing the JavaScriptCore library with a callback that resumes execution from Swift, which is called by an async JavaScript code upon completion.

Let me show you. This is a JS code that asynchronously processes a request:

async function processRequest(data, callback){
try {
let result = await myAsyncJSProcessing(data)
callback(result, null)
} catch(err) {
callback(null, err)
}
}

To be able to pause the Swift execution until the JS code finishes processing and continue once the JS calls the callback with the data, I can use continuation like this:

//proccessRequestSync is a synchronous Swift function that provides the callback that the JS will call.
// This is also the function with a callback that Swift will wait until the callback is called
// self.engine is the initiated JavaScriptCore context where the JS runs.
// We create the callback that we will pass to the JS engine as JSValue, then pass it to the JS function as a parameter. Once the JS is done, calls this function.
func proccessRequestSync(_ completion : @escaping (Result<JSValue, Error>) -> Void){
let callback : @convention(block) (JSValue, JSValue) -> Void = { success, failure in
completion(.success(success))
}
let jsCallback = JSValue(object: callback, in: self.engine)
self.engine?.objectForKeyedSubscript("processRequest").call(withArguments: ["some_data", jsCallback])
}
// Then we create the async function that will be paused using continuation.
// We can integrate the function into the rest of our async Swift.
func proccessRequestAsync() async throws -> JSValue {
//Continuation happens right here.
return try await withCheckedThrowingContinuation({ continuation in
proccessRequestSync { result in
// This is the closure that will be called by the JS once processing is finished
// We will explicitly resume the execution of the Swift code by calling continuation.resume()
switch result {
case .success(let val):
continuation.resume(returning: val)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}

if let result = try? await proccessRequestAsync()

JavascriptCore isn't calling function with arguments properly

Ok, I've found a solution !

Instead of using evaluationScript on my function everytime my event occur I save it once inside a map and I use it to call my function after. So basically:

[_executionContext evaluateScript:[NSString stringWithFormat:@"var %@ = %@", _functionHash, _script]];
_functionMap[_functionHash] = [_executionContext evaluateScript:_functionHash];

And when needed, I call:

[_functionMap[_functionHash] callWithArguments:@[event.data]];

To me there is no difference with what I did before but it seems to work better...

Edit: Well... It doesn't really work. Now it fails after 10 minutes..

JavascriptCore hangs when running a JS with callback

Probably you are trying to convert some JavaScript value to NSDictionary via toDictionary call. This method attempts to convert value recursively, so it leads to a hung often. I suspect that any JS value with circular references causes infinite recursion in this case. Try to work with JSValue directly instead.

Call to swift method from JavaScript hangs xcode and application

You're creating a deadlock since you are calling from Swift to JavaScript back to Swift. I'm not sure exactly why it is a deadlock but I had a similar issue with WKWebView on Mac recently.

You need to decouple this and make the communication asynchronous. This obviously means you cannot simply return a value from your JS function in this case.

To decouple, you can break the deadlock by deferring the work the JavaScript function needs to do out of the current runloop iteration using setTimeout:

function myFunction() {
setTimeout(function() {
// The actual work is done here.
// Call the Swift part here.
}, 0);
}

The whole native ↔︎ JavaScript communication is very, very tricky. Avoid it if you can. There's a project called XWebView that may be able to help you as it tries to ease bridging between the two worlds.

ObjectiveC and JavaScriptCore: Will using this method of calling CallBacks cause memory issues?

The problem with retain cycles occurs when you have two objects, each of which retains part of another. It's not specific to JavascriptCore. It's not even specific to blocks although blocks make the problem much easier to blunder into.

E.g.

@interface ObjcClass : NSObject
@property (strong,nonatomic) JSValue *badProp;

- (void) makeEvilRetainWithContext:(JSContext *) context;
@end

- (void) makeEvilRetainWithContext:(JSContext *) context{
context[@"aFunc"]=^(JSValue *jsValue){
self.badProp=jsValue;
};
}

The self.context[@"aFunc"] now retains the ObjcClass object because self.badProp is now inside the function obj inside the context created by assigning the block to @"aFunc". Likewise, the context is retained because one of its own strongly retained values is retained in self.badProp.

Really, the best way to avoid all this is just to not try and store JSValue in objective-c objects ever. There really doesn't seem to be a need to do so e.g.

@property (strong,nonatomic) NSString *goodProp;

- (void) makeGoodFunc:(JSContext *) context;
@end

- (void) makeGoodFunc:(JSContext *) context{
context[@"aFunc"]=^(JSValue *jsValue){
self.goodProp=[JSValue toString];
};
}

You code isn't a problem because simply passing a JSValue (even a function) through a method won't retain it.

Another way to think of it might be: After, objCFunction:withCallBack: executes, would there be anyway for the object represented by self to access the JSValue passed as callBack? If not, then no retain cycle.



Related Topics



Leave a reply



Submit