Take a SWIG and thread the Napi for C++ to call Javascript

It is nice to be able to call JavaScript from C++ on the same single thread, as previously discussed here. It was nice and simple and entirely contained in one SWIG module. But lets face it, it isn't a real party until there are many threads doing different things all at the same time.
Lets change things up a little :
  • Call JavaScript from C++
  • C++ running one or more threads concurrently with the JavaScript main thread
  • Have very little unfamiliar non standard C++ syntax (i.e. as little third party classes and definitions)

Using node-addon-api, it is simple to generate thread safe JavaScript functions which can be called from C++. But - here is the thing - we want our class to be a simple C++ class which is not that abstract :

#include <Thread.H>

class Test : public ThreadedMethod {
  void *threadMain(void);
public:
  Test();
  virtual ~Test();
};

The snippets in this blog are implemented here in full, you can checkout the fine detail at your leisure.

After compiling the C++, we can instantiate the class in JavaScript and get the thread to run :

var SWIGMod = require('./SWIGMod');
let test = new SWIGMod.Test;
test.run(); // run the C++ thread in the SWIG module
test.meetThread(); // wait for the C++ thread to exit - this blocks the nodejs thread (more on that later)










Now here is the hard part, we want to be able to call JavaScript from the C++ thread. To cut a long story short, this is most easily done by using node-addon-api (napi) to create the thread safe function and then using our regular C++ to execute it. In napi we create the thread like so :




Napi::ThreadSafeFunction tsfn; ///< The node api's threadsafe function

Napi::Value Start( const Napi::CallbackInfo& info ){
  // Create a ThreadSafeFunction
  tsfn = Napi::ThreadSafeFunction::New(info.Env(), 
     info[0].As<Napi::Function>(),  // JavaScript function called asynchronously
    "Resource Name", 0, 1);

This two liner of code (above) defines the thread safe function "tsfn" and then the Start function creates it using the input JavaScript code held in info[0]. In JavaScript we instantiate it by passing in the actual function we want C++ to call :

var Napi = require('./Napi');
let fp = Napi.start(function () {console.log("JS callback, args", Array.from(arguments));});




You will notice that "let fp" stores something, and now here is the crazy magic which solves the following :
It is really difficult to share a function pointer between two independent nodejs modules
For that reason, we return the function pointer as a string at the end of the Napi Start function !

  // return the tsfn as a pointer in a string
  char addr[24];
  sprintf(addr,"%p",&tsfn);
  return Napi::String::New(env, addr);
}

Ok, well, now we have to get that memory pointer back into the SWIG module, so we define the following function in the Test class and assign it from the string :

Napi::ThreadSafeFunction *tsfn; ///< The node api's threadsafe function

void Test::setFnPointer(const char* s){
  sscanf(s, "%p", &tsfn);
}

Crazy eh ! But it works.

The javascript code now looks like this :

var Napi = require('./Napi');
let fp = Napi.start(function () {console.log("JS callback, args", Array.from(arguments));});
var SWIGMod = require('./SWIGMod');
let test = new SWIGMod.Test;

test.setFnPointer(fp); // tell swig the callback function pointer to execute
test.run(); // run the C++ thread in the SWIG module

And we're going off at the races !

Now the last trick is to call the thread safe function from C++ in SWIG, which we do like so :

    // define the data transforming callback
    auto callback = []( Napi::Env env, Napi::Function jsCallback, int* value ) {
      jsCallback.Call( {Napi::Number::New( env, *value )} );
    };
    // call the threadsafe function :
    int value = 49;
    napi_status status = tsfn->BlockingCall( &value, callback );

Done!

You can see the full example code here.



Well, there is that one last trick of not blocking the nodejs main thread and at the same time ensuring that the "test" instance is not garbage collected. In comes the hot stepper, like so :

const id = setInterval(()=>{
  if (!test.running())
    clearInterval(id);
},1000);

While the thread is running, check if it is running every second. That will stop the garbage collection and not block the main thread.

Comments