Passing callbacks between Native JS modules and native code

I’m trying to understand how to take a function passed in as a parameter to a Native JS module method and pass it to Objective-C as a block. More concretely, say I have a function in a Native JS module (module) where the first argument is a success callback and the second argument is a failure callback. In JavaScript, I might call it like this:

module.getStatus(
  function(result) { [handle result] }, 
  function(error) { [handle error] }
);

The Native JS module would have a NativeCallback method like this:

object getStatus(Context c, object[] args) 
{
  var successCb = args[0] as Function;   // seems I can do this per /docs/native-interop/native-js-modules
  var failureCb = args[1] as Function;
  _getStatus(successCb, failureCb); // do I need to cast these callback functions somehow before calling the foreign function below?
  ...
}

and a foreign function that calls my native Objective-C library that accepts two callback blocks as parameters, kinda like:

[Foreign(Language.ObjC)]
public extern(iOS) void _getStatus([what’s the type?] success, [what’s the type?] failure)
@{
  [_nativeLibrary methodOnSuccess:success onError:fail];
@}

What are the types of those parameters for the foreign function? Func<>? Action<>? And how do I convert from the Function to it?

Hey!

Good question.

The Function type you’re seeing is this class. That class has a Call method with signature object Call(params object[] args)

You will probably want to create a wrapper closure for that method with a more specific type, and then pass that to foreign code, because then you get automatic parameter type marshalling (e.g. from NSString* to Uno string). Also make sure that the JS function is not called on any other thread than the calling thread, as that is not generally safe.

Let’s say that you want the success callback to take a string argument. Then you might do something like this (untested code):

class SuccessClosure
{
  Function _callback;
  public SuccessClosure(Function callback)
  {
    _callback = callback;
  }
  public void Call(string arg)
  {
    _callback.Call(arg);
  }
}

Then in getStatus you’ll do something like:

object getStatus(Context c, object[] args) 
{
  var successCb = args[0] as Function;
  var failureCb = args[1] as Function;
  _getStatus(new SuccessClosure(successCb).Call, ...);
  ...
}

The signature of _getStatus will be:

public extern(iOS) void _getStatus(Action<string> success, ...)

Brilliant!! I’ll test this out later today and let you know if I have any further questions. Thanks so much, Olle!!

Olle,

I was finally able to try your suggestion, but I ran into a small issue - looks like the app crashes with:
terminating with uncaught exception of type uThrowable: Uno.Exception.

I’m not an expert at debugging in XCode, especially unwrapping JSValueRefs, but it appears to go south in Fuse.Scripting.JavaScriptCore’s CreateUnoCallback.

I’ve attempted to call the Action<string> success parameter in two ways:

First, I tried just calling it directly from the foreign function:

public extern(iOS) void _getStatus(Action<string> success)
@{
    success(@"Testing");
@}

Also, I’ve tried to call it from my linked Objective-C lib:

public extern(iOS) void _getStatus(Action<string> success)
@{
    [Lib getStatusWithCallback: success];
@}

// in Lib
- (void) getStatusWithCallback: (void (^)(NSString* arg)) successCallback
{
    successCallback(@"Testing");
}

Both of those resulted in error. Any ideas?

I’ll keep chipping away at trying to figure out exactly where the error is, or the specific exception that seems to come from this compiled line return uUnbox< ::JSValueRef>(unoDelegate->Invoke(2, unoArguments, exception)); in JavaScriptCore.

Running:

  • Fuse v0.35.0 (build 10867)
  • macOS Sierra v10.12.3
  • XCode 8.3 beta
  • iOS 10.3

I can add a bit more detail regarding the crash I am getting above. I’ve whittled down my code to as minimal as possible and still get the crash.

I feel like I’m doing something trivially stupid or perhaps it has something to do with which thread it’s running on (I’m certainly not trying to do anything funny with respect to threading, but the stack trace has some suspicious posix_thread stuff.

Here’s my UX:

<App>
<JavaScript>
      var test = require("Test");
      var getStatus = () => {
        test.getStatus(
          function(result) { debug_log(JSON.parse(result)); }
        );
      };
      module.exports = {
        getStatus: getStatus
      };
  </JavaScript>
  <Button Text="Get Status" Clicked="{getStatus}" />
</App>

Here’s the native module:

using Fuse;
using Fuse.Scripting;
using Fuse.Reactive;
using Uno.UX;
using Uno.Compiler.ExportTargetInterop;
using Uno;
using Uno.Collections;

[Require("Xcode.Framework","Foundation.framework")]

[UXGlobalModule]
public class TestModule : NativeModule
{
  static readonly TestModule _instance;

  public TestModule()
  {
    if(_instance != null)
      return;

    _instance = this;
    Resource.SetGlobalKey(_instance, "Test");
    AddMember(new NativeFunction("getStatus", (NativeCallback)getStatus));
  }

  class SuccessClosure
  {
    Function _callback;
    public SuccessClosure(Function callback)
    {
      _callback = callback;
    }
    public void Call(string arg)
    {
      _callback.Call(arg);
    }
  }

  object getStatus(Context c, object[] args)
  {
    if defined(iOS) {
      Function successCb = args[0] as Function;
      _getStatus(new SuccessClosure(successCb).Call);
      return null;
    } else {
      debug_log "getStatus is only implemented for iOS";
      return null;
    }
  }

  [Foreign(Language.ObjC)]
  public extern(iOS) void _getStatus(Action<string> success)
  @{
    success(@"Hello");
  @}

}

When I tap “Get Status”, it gets to the extern(iOS) line success(@"Hello"); and then enters JavaScriptCore-land and crashes with the following stack trace:

libc++abi.dylib: terminating with uncaught exception of type uThrowable: Uno.Exception
(lldb) bt
warning: could not execute support code to read Objective-C class data in the process. This may reduce the quality of type information available.
* thread #12, stop reason = signal SIGABRT
    frame #0: 0x0000000182659014 libsystem_kernel.dylib`__pthread_kill + 8
    frame #1: 0x0000000182723334 libsystem_pthread.dylib`pthread_kill + 112
    frame #2: 0x00000001825cd9c4 libsystem_c.dylib`abort + 140
    frame #3: 0x00000001820991b0 libc++abi.dylib`abort_message + 132
    frame #4: 0x00000001820b2bec libc++abi.dylib`default_terminate_handler() + 280
    frame #5: 0x00000001820c0830 libobjc.A.dylib`_objc_terminate() + 140
    frame #6: 0x00000001820af5d4 libc++abi.dylib`std::__terminate(void (*)()) + 16
    frame #7: 0x00000001820aeef8 libc++abi.dylib`__cxa_throw + 136
  * frame #8: fuse-test`g::Fuse::Scripting::JavaScriptCore::Context__CallbackWrapper::Call(this=0x0000000170265480, args=0x000000017027eec0, exception=0x000000016e4c22d0) at Fuse.Scripting.JavaScriptCore.g.cpp:239
    frame #9: fuse-test`g::Fuse::Scripting::JavaScriptCore::Context__CallbackWrapper__Call_fn(__this=0x0000000170265480, args=0x000000017027eec0, exception=0x000000016e4c22d0, __retval=0x000000016e4c2120) at Fuse.Scripting.JavaScriptCore.g.cpp:207
    frame #10: fuse-test`uInvoke(func=0x00000001002757b0, args=0x000000016e4c2090, count=4) at _invoke.cpp:20
    frame #11: fuse-test`uDelegate::Invoke(this=0x000000017029fbd0, retval=(_address = 0x000000016e4c2120), args=0x000000016e4c2130, count=4) at ObjectModel.cpp:1440
    frame #12: fuse-test`uDelegate::Invoke(this=0x000000017029fbd0, count=2) at ObjectModel.cpp:1531
    frame #13: fuse-test`g::Fuse::Scripting::JavaScriptCore::JSClassRef::CreateUnoCallback(this=0x0000000000000002, ctx=0x000000016e4c2460, function=0x00000001077989a0, thisObject=0x00000001077c12e0, argumentCount=1, arguments=0x000000016e4c22e8, exception=0x000000016e4c22d0)::$_1::operator()(OpaqueJSContext const*, OpaqueJSValue*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) const at Fuse.Scripting.JavaScriptCore.g.cpp:887
    frame #14: fuse-test`g::Fuse::Scripting::JavaScriptCore::JSClassRef::CreateUnoCallback(ctx=0x000000016e4c2460, function=0x00000001077989a0, thisObject=0x00000001077c12e0, argumentCount=1, arguments=0x000000016e4c22e8, exception=0x000000016e4c22d0)::$_1::__invoke(OpaqueJSContext const*, OpaqueJSValue*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) at Fuse.Scripting.JavaScriptCore.g.cpp:872
    frame #15: 0x0000000187c1be7c JavaScriptCore`JSC::JSCallbackObject<JSC::JSDestructibleObject>::call(JSC::ExecState*) + 456
    frame #16: 0x000000018756aba8 JavaScriptCore`JSC::LLInt::setUpCall(JSC::ExecState*, JSC::Instruction*, JSC::CodeSpecializationKind, JSC::JSValue, JSC::LLIntCallLinkInfo*) + 456
    frame #17: 0x0000000187cfeb88 JavaScriptCore`llint_entry + 26392
    frame #18: 0x0000000187cf82a8 JavaScriptCore`vmEntryToJavaScript + 264
    frame #19: 0x0000000187be25b8 JavaScriptCore`JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 164
    frame #20: 0x000000018756efe8 JavaScriptCore`JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 416
    frame #21: 0x000000018787b5ec JavaScriptCore`JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 164
    frame #22: 0x000000018756ed5c JavaScriptCore`JSObjectCallAsFunction + 636
    frame #23: fuse-test`g::Fuse::Scripting::JavaScriptCore::JSObjectRef::CallAsFunction(__this=0x000000010775ece0, ctx=0x00000001077d80e0, thisObject=0x0000000000000000, arguments=0x000000017027ee40, onException=0x000000017029a220) at Fuse.Scripting.JavaScriptCore.g.cpp:1220
    frame #24: fuse-test`g::Fuse::Scripting::JavaScriptCore::Function__Call_fn(__this=0x0000000170075f40, args=0x000000017027ee00, __retval=0x000000016e4c2918) at Fuse.Scripting.JavaScriptCore.g.cpp:754
    frame #25: fuse-test`g::Fuse::Scripting::Function::Call(this=0x0000000170075f40, args=0x000000017027ee00) at Fuse.Scripting.Function.h:30
    frame #26: fuse-test`g::Fuse::Reactive::EventBinding__CallClosure::Call(this=0x000000017428bcc0) at Fuse.Reactive.g.cpp:841
    frame #27: fuse-test`g::Fuse::Reactive::EventBinding__CallClosure__Call_fn(__this=0x000000017428bcc0) at Fuse.Reactive.g.cpp:757
    frame #28: fuse-test`uDelegate::InvokeVoid(this=0x00000001742813b0) at ObjectModel.cpp:1382
    frame #29: fuse-test`g::Fuse::Reactive::ThreadWorker::RunInner(this=0x00000001740a8820) at Fuse.Reactive.g.cpp:3658
    frame #30: fuse-test`g::Fuse::Reactive::ThreadWorker::Run(this=0x00000001740a8820) at Fuse.Reactive.g.cpp:3563
    frame #31: fuse-test`g::Fuse::Reactive::ThreadWorker__Run_fn(__this=0x00000001740a8820) at Fuse.Reactive.g.cpp:3472
    frame #32: fuse-test`uDelegate::InvokeVoid(this=0x0000000174287620) at ObjectModel.cpp:1382
    frame #33: fuse-test`ThreadStartup(arg=0x0000000174261880) at posix_thread.cpp:43
    frame #34: 0x000000018272175c libsystem_pthread.dylib`_pthread_body + 240
    frame #35: 0x000000018272166c libsystem_pthread.dylib`_pthread_start + 284
    frame #36: 0x000000018271ed84 libsystem_pthread.dylib`thread_start + 4

That code looks OK to me, so it’s difficult to say what’s wrong. Can you try adding an exception breakpoint so you can find out where it’s thrown? In the exceptions tab in Xcode there’s a plus button on the bottom left that allows you to add an exception breakpoint.

Sidenote: ES6 arrows are only supported since iOS 10, so you might run into trouble if you try running this code on devices with older versions of iOS.

Olle,

I was actually just about to reply to the thread to give you an update on my progress. I pinged the forum last night and Bolav gave me a couple pointers to set me straight.

He mentioned “trying to run the callback on the JS thread” and then I stumbled upon one of his repos where he uses Context.Dispatcher.Invoke to ensure the callback is invoked in the context in which it was called. That seems to work. Not sure if that’s overkill or necessary. Perhaps you can clarify why that works for future readers of this thread.

Thanks again for your help!
Fuse rocks!

-Atish

Hey again Atish,

To expand on what Bolav was talking about: If you’re running the JS callback on a thread other than the thread calling the NativeFunction (or are unsure if you are, e.g. when passing the callback to a third-party library), then use Context.Dispatcher.Invoke which will schedule it to be run on the JS thread.

In simple cases like your testing code above it shouldn’t be necessary though because you’re already on the JS thread since that’s where NativeFunctions are invoked.

Thanks, and I’m glad you managed to solve your issues!

Hey Olle,

I updated to Fuse 1.6 and now a call in an uno module says the following:

 Call to 'Fuse.Scripting.Context.Invoke(Uno.Action<Fuse.Scripting.Context>)' has some invalid arguments (<method_group>)

The line in question is currently:

_context.Dispatcher.Invoke(RunInternal);

Here’s the repo: https://github.com/Tapped/CapturePanel-Fuse