10

Is it possible to call a JavaScript function (written over, say, node.js) from C?

(There are plenty of tutorials on calling C/C++ from node.js. But not the other way around.)

George
  • 6,927
  • 4
  • 34
  • 67

2 Answers2

1

I've been working on the same problem recently, and found a tractable solution using QuickJS and esbuild. It's not the prettiest, but it works quite well!

To call JS from C, the general process is:

  1. Get QuickJS and esbuild
  2. esbuild your desired library/script into an ESM format using CommonJS. This will output one big script with all needed dependencies included.
output=/path/to/esbuild/output
npx esbuild --bundle /path/to/original/node-library --format=esm --outfile="$output"
  1. Patch the output of esbuild to make it compatible with QuickJS:
sed -i 's/Function(\"return this\")()/globalThis/g' $output
sed -i 's@export default@//export default@g' $output
  1. Load the script text into an object file using your linker:
ld -r -b binary my_obj_file.o $output

Depending on your compiler, this will automatically create 3 symbols in the object file:

- name_start
- name_end
- name_size

name in this context is automatically generated from the filename you provided as the last argument to ld. It replaces all non-alphanumeric characters with underscores, so my-cool-lib.mjs gives a name of my_cool_lib_mjs.

You can use ld_magic.h (here) for a cross platform way to access this data from your C code.

After the object file is generated, you should see the patched esbuild output if you run 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. Link the object file into your binary:
gcc my_obj_file.o <other object files> -o my_static_binary

You can also link the object file into a shared library, for use in other applications:

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

The source of this repo shows how to do this with a CMake project.

How to actually call the JS functions

Let's say you have a NodeJS library with a function you want to call from 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 creates a series of require_thing() functions, which can be used to get the underlying thing(param1, param2...) function object which you can make calls with.

A simple loader in QuickJS looks like this:

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;
}

If you wanted to, for example, get the foo(bar, baz) function mentioned above, you would write a function like this:

#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;
       
}

Check out a minimal example project using CMake here: https://github.com/ijustlovemath/jescx/blob/master/README.md

ijustlovemath
  • 703
  • 10
  • 21
-1

You could use Emscripten.

Emscripten is an LLVM-to-JavaScript compiler.

It takes LLVM bitcode - which can be generated from C/C++, using llvm-gcc (DragonEgg) or clang, or any other language that can be converted into LLVM - and compiles that into JavaScript, which can be run on the web (or anywhere else JavaScript can run).

Also see this: How to execute Javascript function in C++

Community
  • 1
  • 1
sg7
  • 6,108
  • 2
  • 32
  • 40
  • 4
    While I am not the asker, this almost certainly does not answer his question. –  Mar 19 '19 at 20:45