使用node.js从C++调用JavaScript

sh7euo9m  于 2022-12-01  发布在  Node.js
关注(0)|答案(3)|浏览(209)

有没有办法从C++通过node.js调用JS函数(作为回调或类似的东西)?如果有,怎么做?我正在网上搜索,但还没有找到任何有用的资源。
先谢了

mum43rcc

mum43rcc1#

一种方法是使用提供的函数作为回调函数,例如,假设你在原生环境中声明了一个名为setPrintFunction()的函数(A native addon):
(Call例如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)

然后,只是导入您的插件,并使用它像:
(Call例如index.js

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

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

setPrintFunction(printNodeMsg);

因此,您所做的基本工作是捕获对v8::Function的引用(这是javascript函数,但在本机环境中),然后调用它并将"Hello World!"作为第一个(唯一的)参数传递。
关于这个主题的更多信息:https://nodejs.org/api/addons.html

bvuwiixz

bvuwiixz2#

我最近一直在解决这个问题,并且用QuickJS和esbuild找到了一个易于处理的解决方案。它不是最漂亮的,但是它工作得相当好!我的解决方案在C中工作,你应该可以毫不费力地把它适应到C++中。
要从C中调用JS,一般的过程是:
1.获取QuickJS和esbuild
1.使用CommonJS将你想要的库/脚本构建成ESM格式。这将输出一个包含所有需要的依赖项的大脚本。

output=/path/to/esbuild/output
npx esbuild --bundle /path/to/original/node-library --format=esm --outfile="$output"

1.修补esbuild的输出,使其与QuickJS兼容:

sed -i 's/Function(\"return this\")()/globalThis/g' $output
sed -i 's@export default@//export default@g' $output

1.使用链接器将脚本文本加载到目标文件中:

ld -r -b binary my_obj_file.o $output

根据您的编译器,这将在目标文件中自动创建3个符号:

- name_start
- name_end
- name_size

在此上下文中,name是从作为ld的最后一个参数提供的文件名自动生成的。它将所有非字母数字字符替换为下划线,因此my-cool-lib.mjs给出my_cool_lib_mjsname
您可以使用ld_magic.h作为跨平台的方法,从C代码访问这些数据。
生成目标文件后,如果运行strings

% strings foo_lib_js.o
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
// src/foo.js
var require_foo = __commonJS({
  "src/foo.js"(exports, module) {
    function foo(bar, baz) {
      return bar + baz;
    }
    module.exports = foo;
//export default require_foo();
_binary_my_foo_lib_mjs_end
_binary_my_foo_lib_mjs_start
_binary_my_foo_lib_mjs_size
.symtab
.strtab
.shstrtab
.data

1.将目标文件链接到二进制文件中:

gcc my_obj_file.o <other object files> -o my_static_binary

您也可以将目标文件链接到共享库中,以便在其他应用程序中使用:

gcc -shared -o my_shared_library.so my_obj_file.o  <other object files>

这个repo的源代码展示了如何在CMake项目中实现这一点。

如何实际调用JS函数

假设您有一个NodeJS库,其中包含要从C:

// Let's say this lives in foo.js, and esbuild output goes in my-lib-foo.mjs
function foo(bar, baz) {
    return bar + baz
}

module.exports = foo;

esbuild创建了一系列require_thing()函数,这些函数可用于获取底层的thing(param1, param2...)函数对象,您可以使用这些对象进行调用。
QuickJS中的一个简单加载器如下所示:

JSValue commonjs_module_data_to_function(JSContext *ctx, const uint8_t *data, size_t data_length, const char *function_name)
{
    JSValue result = JS_UNDEFINED;
    char * module_function_name = NULL;

    // Make sure you properly free all JSValues created from this procedure

    if(data == NULL) {
        goto done;
    }

    /**
     * To pull the script objects, including require_thing() etc, into global scope,
     * load the patched NodeJS script from the object file embedded in the binary
     */
    result = JS_Eval(ctx, data, data_length, "<embed>", JS_EVAL_TYPE_GLOBAL);

    if(JS_IsException(result)) {
        printf("failed to parse module function '%s'\n", function_name);
        goto cleanup_fail;
    }

    JSValue global = JS_GetGlobalObject(ctx);

    /**
     * Automatically create the require_thing() function name
     */
    asprintf(&module_function_name, "require_%s", function_name);
    JSValue module = JS_GetPropertyStr(ctx, global, module_function_name);
    if(JS_IsException(module)) {
        printf("failed to find %s module function\n", function_name);
        goto cleanup_fail;
    }
    result = JS_Call(ctx, module, global, 0, NULL);
    if(JS_IsException(result)) {
        goto cleanup_fail;
    }

    /* don't lose the object we've built by passing over failure case */
    goto done;

cleanup_fail:
    /* nothing to do, cleanup context elsewhere */
    result = JS_UNDEFINED;

done:
    free(module_function_name);
    return result;
}

例如,如果要获取上面提到的foo(bar, baz)函数,可以编写如下函数:

#include <stdio.h>
#include <inttypes.h>

// A simple helper for getting a JSContext
JSContext * easy_context(void)
{
    JSRuntime *runtime = JS_NewRuntime();
    if(runtime == NULL) {
        puts("unable to create JS Runtime");
        goto cleanup_content_fail;
    }

    JSContext *ctx = JS_NewContext(runtime);
    if(ctx == NULL) {
        puts("unable to create JS context");
        goto cleanup_runtime_fail;
    }
    return ctx;

cleanup_runtime_fail:
    free(runtime);

cleanup_content_fail:
    return NULL;

}

int call_foo(int bar, int baz)
{
    JSContext *ctx = easy_context();
    JSValue global = JS_GetGlobalObject(ctx);

    /**
     * esbuild output was to my-foo-lib.mjs, so symbols will be named with my_foo_lib_mjs
     */
    JSValue foo_fn = commonjs_module_data_to_function(
        ctx
        , _binary_my_foo_lib_mjs_start // gcc/Linux-specific naming
        , _binary_my_foo_lib_mjs_size
        , "foo"
    );
    
    /**
     * To create more complex objects as arguments, use 
     *   JS_ParseJSON(ctx, json_str, strlen(json_str), "<input>");
     * You can also pass callback functions by loading them just like we loaded foo_fn
     */
    JSValue args[] = {
        JS_NewInt32(ctx, bar),
        JS_NewInt32(ctx, baz)
    };

    JSValue js_result = JS_Call(ctx
        , foo_fn
        , global
        , sizeof(args)/sizeof(*args)
        , args
    );

    int32_t c_result = -1;

    JS_ToInt32(ctx, &c_result, js_result);

    return c_result;
       
}

请在此处查看使用CMake的最小示例项目:https://github.com/ijustlovemath/jescx/blob/master/README.md

vmdwslir

vmdwslir3#

当然可以。例如,如果你想用C++编写一个简单的阶乘函数,你可以这样做:

#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)

在您的JavaScript文件中,将其命名为

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

相关问题