├── .gitignore ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── include ├── foo_lib.h └── ld_magic.h ├── scripts └── build-mjs.sh └── src ├── CMakeLists.txt ├── foo.js ├── foo_lib.c ├── main.c └── my-foo-lib.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(jescx) 3 | 4 | set(COMMON_LIBS m pthread dl) 5 | 6 | include_directories(quickjs) 7 | include_directories(include) 8 | add_subdirectory(src) 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Jeremy DeJournett 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Calling JavaScript from C/C++ using QuickJS and esbuild 2 | 3 | When you search for "How to call JavaScript from C/C++", you'll find tons of results about how to write JS modules in C, and call them from your Node environment, but almost nothing for the reverse direction. Often times, you'll hear the reason is that "JavaScript is too complex to be used from C", and to extent, that's true. But if you don't mind building (relatively) big binaries, and you have a pretty good understanding of the data types required and returned by the library, you can use QuickJS and esbuild to bundle all of the functionality from your favorite Node.JS libraries into a native library, a static binary, etc. 4 | 5 | ## How it works (Linux only, for now) 6 | 7 | To call JS from C, the general process is: 8 | 9 | 1. Get QuickJS and esbuild 10 | 2. esbuild your desired library/script into an ESM format using CommonJS. This will output one big script with all needed dependencies included. 11 | ```bash 12 | output=/path/to/esbuild/output 13 | npx esbuild --bundle /path/to/original/node-library --format=esm --outfile="$output" 14 | ``` 15 | 3. Patch the output of esbuild to make it compatible with QuickJS: 16 | 17 | ```bash 18 | sed -i 's/Function(\"return this\")()/globalThis/g' $output 19 | sed -i 's@export default@//export default@g' $output 20 | ``` 21 | 4. Load the script text into an object file using your linker: 22 | 23 | ```bash 24 | ld -r -b binary my_obj_file.o $output 25 | ``` 26 | Depending on your compiler, this will automatically create 3 symbols in the object file: 27 | 28 | - name_start 29 | - name_end 30 | - name_size 31 | 32 | `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`. 33 | 34 | You can use `ld_magic.h` for a cross platform way to access this data from your C code. 35 | 36 | After the object file is generated, you should see the patched esbuild output if you run `strings`: 37 | 38 | ```bash 39 | % strings foo_lib_js.o 40 | var __getOwnPropNames = Object.getOwnPropertyNames; 41 | var __commonJS = (cb, mod) => function __require() { 42 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 43 | // src/foo.js 44 | var require_foo = __commonJS({ 45 | "src/foo.js"(exports, module) { 46 | function foo(bar, baz) { 47 | return bar + baz; 48 | } 49 | module.exports = foo; 50 | //export default require_foo(); 51 | _binary_my_foo_lib_mjs_end 52 | _binary_my_foo_lib_mjs_start 53 | _binary_my_foo_lib_mjs_size 54 | .symtab 55 | .strtab 56 | .shstrtab 57 | .data 58 | ``` 59 | 5. Link the object file into your binary: 60 | ```bash 61 | gcc my_obj_file.o -o my_static_binary 62 | ``` 63 | 64 | You can also link the object file into a shared library, for use in other applications: 65 | ```bash 66 | gcc -shared -o my_shared_library.so my_obj_file.o 67 | ``` 68 | 69 | The source of this repo shows how to do this with a CMake project. 70 | 71 | ## How to actually call the JS functions 72 | 73 | Let's say you have a NodeJS library with a function you want to call from C: 74 | 75 | ```js 76 | // Let's say this lives in foo.js, and esbuild output goes in my-lib-foo.mjs 77 | function foo(bar, baz) { 78 | return bar + baz 79 | } 80 | 81 | module.exports = foo; 82 | ``` 83 | 84 | `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. 85 | 86 | A simple loader in QuickJS looks like this: 87 | 88 | ```c 89 | JSValue commonjs_module_data_to_function(JSContext *ctx, const uint8_t *data, size_t data_length, const char *function_name) 90 | { 91 | JSValue result = JS_UNDEFINED; 92 | char * module_function_name = NULL; 93 | 94 | // Make sure you properly free all JSValues created from this procedure 95 | 96 | if(data == NULL) { 97 | goto done; 98 | } 99 | 100 | /** 101 | * To pull the script objects, including require_thing() etc, into global scope, 102 | * load the patched NodeJS script from the object file embedded in the binary 103 | */ 104 | result = JS_Eval(ctx, data, data_length, "", JS_EVAL_TYPE_GLOBAL); 105 | 106 | if(JS_IsException(result)) { 107 | printf("failed to parse module function '%s'\n", function_name); 108 | goto cleanup_fail; 109 | } 110 | 111 | JSValue global = JS_GetGlobalObject(ctx); 112 | 113 | /** 114 | * Automatically create the require_thing() function name 115 | */ 116 | asprintf(&module_function_name, "require_%s", function_name); 117 | JSValue module = JS_GetPropertyStr(ctx, global, module_function_name); 118 | if(JS_IsException(module)) { 119 | printf("failed to find %s module function\n", function_name); 120 | goto cleanup_fail; 121 | } 122 | result = JS_Call(ctx, module, global, 0, NULL); 123 | if(JS_IsException(result)) { 124 | goto cleanup_fail; 125 | } 126 | 127 | /* don't lose the object we've built by passing over failure case */ 128 | goto done; 129 | 130 | cleanup_fail: 131 | /* nothing to do, cleanup context elsewhere */ 132 | result = JS_UNDEFINED; 133 | 134 | done: 135 | free(module_function_name); 136 | return result; 137 | } 138 | ``` 139 | 140 | If you wanted to, for example, get the `foo(bar, baz)` function mentioned above, you would write a function like this: 141 | 142 | ```c 143 | #include 144 | #include 145 | 146 | // A simple helper for getting a JSContext 147 | JSContext * easy_context(void) 148 | { 149 | JSRuntime *runtime = JS_NewRuntime(); 150 | if(runtime == NULL) { 151 | puts("unable to create JS Runtime"); 152 | goto cleanup_content_fail; 153 | } 154 | 155 | JSContext *ctx = JS_NewContext(runtime); 156 | if(ctx == NULL) { 157 | puts("unable to create JS context"); 158 | goto cleanup_runtime_fail; 159 | } 160 | return ctx; 161 | 162 | cleanup_runtime_fail: 163 | free(runtime); 164 | 165 | cleanup_content_fail: 166 | return NULL; 167 | 168 | } 169 | 170 | 171 | int call_foo(int bar, int baz) 172 | { 173 | JSContext *ctx = easy_context(); 174 | JSValue global = JS_GetGlobalObject(ctx); 175 | 176 | /** 177 | * esbuild output was to my-foo-lib.mjs, so symbols will be named with my_foo_lib_mjs 178 | */ 179 | JSValue foo_fn = commonjs_module_data_to_function( 180 | ctx 181 | , _binary_my_foo_lib_mjs_start // gcc/Linux-specific naming 182 | , _binary_my_foo_lib_mjs_size 183 | , "foo" 184 | ); 185 | 186 | /** 187 | * To create more complex objects as arguments, use 188 | * JS_ParseJSON(ctx, json_str, strlen(json_str), ""); 189 | * You can also pass callback functions by loading them just like we loaded foo_fn 190 | */ 191 | JSValue args[] = { 192 | JS_NewInt32(ctx, bar), 193 | JS_NewInt32(ctx, baz) 194 | }; 195 | 196 | JSValue js_result = JS_Call(ctx 197 | , foo_fn 198 | , global 199 | , sizeof(args)/sizeof(*args) 200 | , args 201 | ); 202 | 203 | int32_t c_result = -1; 204 | 205 | JS_ToInt32(ctx, &c_result, js_result); 206 | 207 | return c_result; 208 | 209 | } 210 | ``` 211 | 212 | ## Why should anyone do this? 213 | I'm doing this to add support for [a popular open source glucose control algorithm](https://github.com/OpenAPS/oref0) into a clinical glucose simulator, to see how state of the art controllers perform against it. Since OpenAPS is a fairly active project, and the simulator is written in C++, the two options for using it were: 214 | 215 | 1. Reimplement OpenAPS in C++ (costly in time, error prone, liable to fall behind future changes) 216 | 2. Transpile it to C++ (nothing exists that I could find easily) 217 | 218 | So if you have a popular NodeJS library you want to integrate into some sort of native application that's not written using Electron, that's the use case I see this process covering. 219 | 220 | Make sure you're only applying this process to source you have an appropriate license to use. I claim no responsibility for open source license violations that occur as a result of using this process. 221 | 222 | ## Does this work on Windows/Mac? 223 | Conceivably you could use MinGW64 on Windows to have a similar build process, but that's outside my use case, so I haven't invested much time into it. Similarly, I don't have a Mac to test on. 224 | 225 | If you want to submit a PR for how you implemented this with your platform, feel free! 226 | 227 | ## Build Instructions 228 | To have cmake generate makefiles for you, you need to create the object file first, then subsquent times it will generate for you 229 | 230 | ```bash 231 | 232 | # run from your build directory, then run cmake .. 233 | ld -r -b binary -o foo_lib_js.o ../src/my-foo-lib.mjs 234 | ``` 235 | -------------------------------------------------------------------------------- /include/foo_lib.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "quickjs.h" 4 | extern int call_foo(int, int); 5 | 6 | void list_properties (JSContext *ctx, JSValue map, const char *comment); 7 | -------------------------------------------------------------------------------- /include/ld_magic.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by jcdej on 2022-11-20. 3 | // 4 | // From here: http://gareus.org/wiki/embedding_resources_in_executables (also available on wayback machine) 5 | 6 | /** 7 | * Usage: 8 | * 1. Add the custom commands to CMakeLists.txt to create the object files from source text 9 | * 2. Add the object files to the list of source files in add_executable or add_library 10 | * 3. Replacing all non-alphanumeric characters from the source file name with underscores, 11 | * find the embedded variable name. For example, "determine-basal.mjs" -> "determine_basal_mjs" 12 | * 13 | * In your source file where you want to use the data, declare it using EXTLD, get u8 data pointer with LDVAR, length with LDLEN 14 | * 15 | * EXTLD(determine_basal_mjs) 16 | * 17 | * int main(void) { 18 | * const unsigned char *data = LDVAR(determine_basal_mjs); 19 | * const unsigned long = LDLEN(determine_basal_mjs); // this should be size_t 20 | */ 21 | 22 | #ifndef QUICKJS_LIBC_C_LD_MAGIC_H 23 | #define QUICKJS_LIBC_C_LD_MAGIC_H 24 | 25 | #ifdef __APPLE__ 26 | #include 27 | 28 | #define EXTLD(NAME) \ 29 | extern const unsigned char _section$__DATA__ ## NAME []; 30 | #define LDVAR(NAME) _section$__DATA__ ## NAME 31 | #define LDLEN(NAME) (getsectbyname("__DATA", "__" #NAME)->size) 32 | 33 | #elif (defined __WIN32__) /* mingw */ 34 | 35 | #define EXTLD(NAME) \ 36 | extern const unsigned char binary_ ## NAME ## _start[]; \ 37 | extern const unsigned char binary_ ## NAME ## _end[]; 38 | #define LDVAR(NAME) \ 39 | binary_ ## NAME ## _start 40 | #define LDLEN(NAME) \ 41 | ((binary_ ## NAME ## _end) - (binary_ ## NAME ## _start)) 42 | 43 | #else /* gnu/linux ld */ 44 | 45 | #define EXTLD(NAME) \ 46 | extern const unsigned char _binary_ ## NAME ## _start[]; \ 47 | extern const unsigned char _binary_ ## NAME ## _end[]; 48 | #define LDVAR(NAME) \ 49 | _binary_ ## NAME ## _start 50 | #define LDLEN(NAME) \ 51 | ((_binary_ ## NAME ## _end) - (_binary_ ## NAME ## _start)) 52 | #endif 53 | 54 | #endif //QUICKJS_LIBC_C_LD_MAGIC_H 55 | -------------------------------------------------------------------------------- /scripts/build-mjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #source $HOME/.nvm/nvm.sh 3 | if [ ! -d "./src" ]; then 4 | echo "call this from root of repo" 5 | exit 1 6 | fi 7 | npx esbuild --bundle src/foo.js --format=esm --outfile=src/my-foo-lib.mjs 8 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ## Automatically donwload and use module CPM.cmake 2 | file(DOWNLOAD https://raw.githubusercontent.com/TheLartians/CPM.cmake/v0.26.2/cmake/CPM.cmake 3 | "${CMAKE_BINARY_DIR}/CPM.cmake") 4 | include("${CMAKE_BINARY_DIR}/CPM.cmake") 5 | 6 | #----------- Add dependencies --------------------------# 7 | 8 | CPMAddPackage( 9 | NAME quickjs 10 | GITHUB_REPOSITORY bellard/quickjs 11 | GIT_TAG 2788d71e823b522b178db3b3660ce93689534e6d 12 | # DOWNLOAD_ONLY YES 13 | ) 14 | 15 | 16 | # Add this directory where is this file (CMakeLists.txt) to include path. 17 | include_directories( ${CMAKE_CURRENT_LIST_DIR} ) 18 | 19 | # =============== QuickJS settings ====================================# 20 | 21 | include_directories( ${quickjs_SOURCE_DIR}/ ) 22 | message([TRACE] " quickjs source = ${quickjs_SOURCE_DIR} ") 23 | 24 | file(GLOB quickjs_hpp ${quickjs_SOURCE_DIR}/*.h ) 25 | 26 | file(GLOB quickjs_src ${quickjs_SOURCE_DIR}/quickjs.c 27 | ${quickjs_SOURCE_DIR}/libregexp.c 28 | ${quickjs_SOURCE_DIR}/libunicode.c 29 | ${quickjs_SOURCE_DIR}/cutils.c 30 | ${quickjs_SOURCE_DIR}/quickjs-libc.c 31 | ${quickjs_SOURCE_DIR}/libbf.c 32 | ) 33 | 34 | 35 | add_library( quickjs ${quickjs_src} ${quickjs_hpp} ) 36 | target_compile_options( quickjs PRIVATE 37 | -MMD -MF 38 | -Wno-sign-compare 39 | -Wno-missing-field-initializers 40 | -Wundef -Wuninitialized 41 | -Wundef -Wuninitialized -Wwrite-strings -Wchar-subscripts 42 | -fPIC 43 | ) 44 | target_compile_definitions( quickjs PUBLIC 45 | CONFIG_BIGNUM=y 46 | CONFIG_VERSION="2021-03-27" 47 | DUMP_MODULE_RESOLVE=1 48 | _GNU_SOURCE 49 | ) 50 | 51 | if(UNIX) 52 | target_link_libraries( quickjs PRIVATE ${COMMON_LIBS}) 53 | endif() 54 | 55 | add_custom_command( 56 | OUTPUT my-foo-lib.mjs 57 | COMMAND ./scripts/build-mjs.sh 58 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/foo.js 59 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 60 | ) 61 | 62 | add_custom_command( 63 | OUTPUT foo_lib_js.o 64 | COMMAND ld -r -b binary -o ${CMAKE_BINARY_DIR}/foo_lib_js.o my-foo-lib.mjs 65 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/my-foo-lib.mjs 66 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 67 | ) 68 | 69 | #set_property(SOURCE foo_lib_js.o PROPERTY GENERATED 1) 70 | set_property(SOURCE foo_lib.c APPEND PROPERTY OBJECT_DEPENDS foo_lib_js.o) 71 | 72 | add_library(foo_lib SHARED foo_lib.c ${CMAKE_BINARY_DIR}/foo_lib_js.o) 73 | 74 | target_link_libraries(foo_lib quickjs ${COMMON_LIBS}) 75 | 76 | add_executable(main main.c) 77 | target_link_libraries(main foo_lib) 78 | 79 | -------------------------------------------------------------------------------- /src/foo.js: -------------------------------------------------------------------------------- 1 | function foo(bar, baz) { 2 | return bar + baz; 3 | } 4 | 5 | module.exports = foo; 6 | -------------------------------------------------------------------------------- /src/foo_lib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "quickjs.h" 6 | #include "foo_lib.h" 7 | #include "ld_magic.h" 8 | 9 | EXTLD(my_foo_lib_mjs) 10 | 11 | JSValue commonjs_module_data_to_function(JSContext *ctx, const uint8_t *data, size_t data_length, const char *function_name) 12 | { 13 | JSValue result = JS_UNDEFINED; 14 | char * module_function_name = NULL; 15 | 16 | // Make sure you properly free all JSValues created from this procedure 17 | 18 | if(data == NULL) { 19 | goto done; 20 | } 21 | 22 | /** 23 | * To pull the script objects, including require_thing() etc, into global scope, 24 | * load the patched NodeJS script from the object file embedded in the binary 25 | */ 26 | result = JS_Eval(ctx, data, data_length-1, "", JS_EVAL_TYPE_GLOBAL); 27 | 28 | if(JS_IsException(result)) { 29 | printf("failed to parse module function '%s'\n", function_name); 30 | printf("exception: %s\n", JS_ToCString(ctx, JS_GetException(ctx))); 31 | goto cleanup_fail; 32 | } 33 | 34 | JSValue global = JS_GetGlobalObject(ctx); 35 | 36 | /** 37 | * Automatically create the require_thing() function name 38 | */ 39 | asprintf(&module_function_name, "require_%s", function_name); 40 | JSValue module = JS_GetPropertyStr(ctx, global, module_function_name); 41 | if(JS_IsException(module)) { 42 | printf("failed to find %s module function\n", function_name); 43 | goto cleanup_fail; 44 | } 45 | result = JS_Call(ctx, module, global, 0, NULL); 46 | if(JS_IsException(result)) { 47 | goto cleanup_fail; 48 | } 49 | 50 | /* don't lose the object we've built by passing over failure case */ 51 | goto done; 52 | 53 | cleanup_fail: 54 | /* nothing to do, cleanup context elsewhere */ 55 | result = JS_UNDEFINED; 56 | 57 | done: 58 | free(module_function_name); 59 | return result; 60 | } 61 | 62 | // A simple helper for getting a JSContext 63 | JSContext * easy_context(void) 64 | { 65 | JSRuntime *runtime = JS_NewRuntime(); 66 | if(runtime == NULL) { 67 | puts("unable to create JS Runtime"); 68 | goto cleanup_content_fail; 69 | } 70 | 71 | JSContext *ctx = JS_NewContext(runtime); 72 | if(ctx == NULL) { 73 | puts("unable to create JS context"); 74 | goto cleanup_runtime_fail; 75 | } 76 | return ctx; 77 | 78 | cleanup_runtime_fail: 79 | free(runtime); 80 | 81 | cleanup_content_fail: 82 | return NULL; 83 | 84 | } 85 | 86 | 87 | int call_foo(int bar, int baz) 88 | { 89 | JSContext *ctx = easy_context(); 90 | JSValue global = JS_GetGlobalObject(ctx); 91 | 92 | /** 93 | * esbuild output was to my-foo-lib.mjs, so symbols will be named with my_foo_lib_mjs 94 | */ 95 | JSValue foo_fn = commonjs_module_data_to_function( 96 | ctx 97 | , LDVAR(my_foo_lib_mjs) // gcc/Linux-specific naming 98 | , LDLEN(my_foo_lib_mjs) 99 | , "foo" 100 | ); 101 | 102 | if(JS_IsUndefined(foo_fn)) { 103 | printf("couldn't find foo to call\n"); 104 | return -1; 105 | } 106 | 107 | /** 108 | * To create more complex objects as arguments, use 109 | * JS_ParseJSON(ctx, json_str, strlen(json_str), ""); 110 | */ 111 | JSValue args[] = { 112 | JS_NewInt32(ctx, (int32_t) bar), 113 | JS_NewInt32(ctx, (int32_t) baz) 114 | }; 115 | 116 | JSValue js_result = JS_Call(ctx 117 | , foo_fn 118 | , global 119 | , sizeof(args)/sizeof(*args) 120 | , args 121 | ); 122 | 123 | if(JS_IsUndefined(js_result)) { 124 | puts("function call failed\n"); 125 | return -1; 126 | } 127 | 128 | int32_t c_result = -1; 129 | 130 | JS_ToInt32(ctx, &c_result, js_result); 131 | 132 | return c_result; 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "foo_lib.h" 2 | #include 3 | 4 | int main(void) 5 | { 6 | printf("foo(5, 3) = %d\n", call_foo(5, 3)); 7 | } 8 | -------------------------------------------------------------------------------- /src/my-foo-lib.mjs: -------------------------------------------------------------------------------- 1 | var __getOwnPropNames = Object.getOwnPropertyNames; 2 | var __commonJS = (cb, mod) => function __require() { 3 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; 4 | }; 5 | 6 | // src/foo.js 7 | var require_foo = __commonJS({ 8 | "src/foo.js"(exports, module) { 9 | function foo(bar, baz) { 10 | return bar + baz; 11 | } 12 | module.exports = foo; 13 | } 14 | }); 15 | //export default require_foo(); 16 | --------------------------------------------------------------------------------