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

14
  • Er... I don't think there's a way, C programs can talk to the web. JavaScript is a language for the web. So, I feel, no. Commented Nov 6, 2016 at 22:58
  • @Praveen: Node.js is used as a backend for non-web things all the time. Commented Nov 6, 2016 at 22:59
  • I know George, but generally, as a JavaScript... Well, I said it's just my feeling. Commented Nov 6, 2016 at 23:00
  • C can use system() to execute shell commands, so you can do system("npm scriptname.js") Commented Nov 6, 2016 at 23:01
  • 1
    @George Then put that function by itself in a script and execute that? Commented Nov 6, 2016 at 23:04

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

Comments

-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++

1 Comment

While I am not the asker, this almost certainly does not answer his question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.