24

Is there a way to call JS functions from C++ through node.js (as callbacks or something like that)? If yes, how? I'm searching for it on the web, but haven't found any helpful resource.

Thanks in advance

plasmacel
  • 8,183
  • 7
  • 53
  • 101

2 Answers2

6

One way to do it form a native addon can be using the provided function as a callback, for example let's gonna assume that you have a function named setPrintFunction() declared in your native environment (A native addon):

(Call this for example main.cc)

#include <node.h>
#include <string>

v8::Persistent<v8::Function> fn;

// Call this at any time, but after the capture!
void printToNode(std::string msg) {
  auto isolate = fn->GetIsolate();
  // This part is the one that transforms your std::string to a javascript
  // string, and passes it as the first argument:
  const unsigned argc = 1;
  auto argv[argc] = {
      v8::String::NewFromUtf8(isolate,
                          msg.c_str(),
                          v8::NewStringType::kNormal).ToLocalChecked()
  };
  cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}

// This is your native function that captures the reference
void setPrintFunction(const v8::FunctionCallbackInfo<Value>& args) {
  auto isolate = args.GetIsolate();
  auto context = isolate->GetCurrentContext();
  auto cb = v8::Local<v8::Function>::Cast(args[0]);
  fn = v8::Persistent<v8::Function>::New(cb);
}

// This part exports the function
void Init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module) {
  NODE_SET_METHOD(module, "exports", setPrintFunction);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

Then, just importing your addon and using it like:

(Call this for example index.js)

const { setPrintFunction } = require('<your path to .node file>');

function printNodeMsg(msg) {
  console.log('<msg>: ' + msg);
}

setPrintFunction(printNodeMsg);

So what you're basically doing is capturing the reference to the v8::Function (Which is the javascript function, but in the native environment) and then invoking it and passing "Hello World!" as the first (and unique) parameter.

More on the subject: https://nodejs.org/api/addons.html

Sigma Octantis
  • 903
  • 1
  • 8
  • 25
  • Can you add an example how to use the solution in C++? In this case passing an `std::string` to the C++ function, which will be passed to its Javascript counterpart. – plasmacel Mar 29 '19 at 15:05
  • I think I get what you want: For example could be a function that captures a reference to another function and then, at your will, make a function in the native part that invokes it. That could be a possible solution? – Sigma Octantis Mar 29 '19 at 15:13
  • Well, when I was asking this question, I wanted to know the following: Let's say I have a JS function `printNodeMsg(msg)` and I want to call it and pass the `msg` parameter to it from C++. The addon is not a problem, but I wanted to be able to pass parameters, not just invoking a function. So basically calling C++ function `void printNodeMsg(const std::string& msg)` would call `prontNodeMsg(msg)` on the JS site, passing the specified string data. – plasmacel Mar 29 '19 at 15:17
  • 2
    Isn't this solution opposite to what OP's questions is? – barath May 07 '20 at 15:27
  • @barath the OP asked how to call a JavaScript-created function from a C++ context, but using node. So as far as I can understand, the OP wants a C/C++ procedure that can be supplied with a JS function so it can be crafted from a JS context, but used later during the course of the C++ program. – Sigma Octantis May 07 '20 at 17:16
-3

Of course you can. For example, if you want to write a simple factorial function in C++, you could do something like

#include <node.h>

using namespace v8;

int factorial(int n) {
    if (n == 0) return 1;
    else return n * factorial(n - 1);
}

void Factorial(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = Isolate::GetCurrent();
    HandleScope scope(isolate);

    if (args.Length() != 2) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments")));
    } else {
        if (!(args[0]->IsNumber() && args[1]->IsFunction())) {
            isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong arguments type")));
        } else {
            int result = factorial(args[0]->Int32Value());

            Local<Function> callbackFunction = Local<Function>::Cast(args[1]);
            const unsigned argc = 1;
            Local<Value> argv[argc] = { Number::New(isolate, result) };

            callbackFunction->Call(isolate->GetCurrentContext()->Global(), argc, argv);
        }
    }
}

void Init(Handle<Object> exports) {
    NODE_SET_METHOD(exports, "factorial", Factorial);
}

NODE_MODULE(Factorial, Init)

And in your JavaScript file, call it like this

var factorialAddon = require('./addons/Factorial');
factorialAddon.factorial(5, function (result) {
    console.log(result);
});