Javascript to C++ and SWIG it back again



Lets face it, at this point in time WASM is fun, but the only way out of the virtual machine is through whence it came. Virtual machines are like wearing headsets and seeing amazing things. Don't get me wrong though, I use WASM a lot, it is very powerful for performing strongly typed computation anywhere. Here and here are some starter WASM projects if you are interested.

But lets face it, executing code outside of the virtual machine is (in some cases) the real deal, because you are outside of the security mechanisms of the virtual machine. It is like breaking (bboys and bgirls), where you have skin in the game, one slip and you've let the smoke out!


But even with real world execution what is dance without music? It is not as good! To have good symbiosis, you need feedback and real world events, and that is why C++ has to be able to call javascript.
SWIG allows us to operate C and C++ from javascript. There are a few nice points about SWIG but the main ones are the following :
  • There is no virtual machine, you are operating the same machine which you execute the code on.
  • There is no strange abstraction in your C++ code.
As we are operating the machine, we can handle hardware and anything else which is normally not available if you were in a virtual machine. As there is no C++ abstraction, you can simply write C++ as you normally would.

One thing which is lacking in SWIG is the ability to call javascript from C++. This lack of "callback" from C++ to javascript is a deal breaker for real world projects where perhaps we want our high level scripting language to respond to real world events. This document will demonstrate how to implement callbacks from C++ to javascript.

The code from this article is available here.

Vanilla C++ in javascript

Given the following C++ class :

class Test {
public:
  Test();
  virtual ~Test();
};

We can compile the class for use in javascript using SWIG, allowing us to require the C++ for use in nodejs and then instantiate the class like so :

var libTest = require('libTest);
let test = new libTest.Test;

SWIG will do this and the template (.i file) for making this happen is very simple :

%module "libTest"
%{
#include "Test.H"
%}
%include "Test.H"

This exemplifies the extreme simplicity of executing C++ in javascript without any fancy requirements on your C++ code. We can now very quickly use C++ in javascript, however this works when you only want to call C++ from javascript sending in arguments and receiving return values.

What if you want javascript to respond to events and things which happen in C++ or at lower layers of the operating system ? We need to use callbacks where C++ can call javascript.

Callbacks from C++ to javascript

Say we have a global javascript function, which prints out the input argument str :

global.fnName = function (str) {
  console.log(str);
}

We would like to be able to tell C++ to use the global.fnName function as its internal callback, like so :

test.setCallback("fnName");

Later we would like to be able to trigger this callback from C++ (during development) to make sure it works :

test.callCallback();

In order to be able to execute javascript from C++ we need to leaverage nodejs's v8 engine (which SWIG will link against for us) to make it happen.

Extending SWIG with v8

To allow our C++ code to store a function for later execution, we add a member variable to our class like so :

#ifdef SWIG_V8_VERSION
public:
  v8::Persistent<v8::Function> theFunction;
#endif

We can still compile our class without the presence of nodejs/SWIG (because of the #ifdef) but when present, the v8 function "theFunction" will be persistent once set up.

To setup the function, we extend the Test C++ class by adding the following C++ code to our SWIG template .i file :

%extend Test {

  void setCallback(const std::string& fnName){
    v8::Isolate* isolate = v8::Isolate::GetCurrent();

    // find the function fnName in the javascript global context
    SWIGV8_VALUE fnObj = SWIGV8_CURRENT_CONTEXT()->Global()
                                                     ->Get(SWIGV8_CURRENT_CONTEXT(),       
                                                     SWIGV8_STRING_NEW(fnName.c_str())).ToLocalChecked();

    if (!fnObj->IsFunction()){
      printf("setupCallback : error no function found\n");
      return;
    } else
      printf("setupCallback : %s function found\n", fnName.c_str());

    // set the function to be persistently present in the function pointer theFunction
    v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(fnObj);
    self->theFunction.Reset(isolate, func);
  }

We can now tell C++ to setup the callback function like so :

test.setCallback("fnName");

Finally, for testing, we want to get C++ to call this callback function, which we do by adding the following v8 C++ code to our SWIG .i template file :

  void callCallback(){
    // pull the persistent javascript function into the local context
    v8::Local<v8::Function> func = v8::Local<v8::Function>::
                                                                   New(v8::Isolate::GetCurrent(), self->theFunction);
    if (!func.IsEmpty()) {

      // setup input arguments for the javascript function 
      const unsigned argc = 1;
      SWIGV8_VALUE argv[argc] = { SWIGV8_STRING_NEW("hello world") };

      // Call the javascript function to execute
      func->Call(SWIGV8_CURRENT_CONTEXT(), func, argc, argv);
     } else {
       printf("Couldn't find a valid function, call setCallback first.\n");
     }
  }

When you execute the following javascript code :

test.setCallback("fnName");
test.callCallback();

C++ sets up the javascript function to call and then does a test call of that function from C++ printing out the following :

fnName function found
hello world

Conclusion

Living life in a virtual machine can be limiting. Here I have shown how to execute C++ from javascript using SWIG. We have taken SWIG a step further by also showing how to call javascript from C++ with very little extra code required.

A working example of this code can be found here.
If you want a multithreaded solution to the same problems, check here.

Comments