Call Native Functions from JavaScript

• 5 minutes to read

WasmEdge enables JavaScript to call native functions in shared libraries.

In my previous two articles, I have explained why and how to run JavaScript programs in a WebAssembly sandbox. I have also discussed how to run standalone JavaScript programs, as well as Rust programs with embedded JavaScript in the WasmEdge Runtime.

But many JavaScript developers want only to write in JavaScript. In order to make JavaScript run fast in WebAssembly, we would like to create JavaScript APIs for C-based native functions. That is, when a JavaScript program calls the pre-defined function, WasmEdge will pass it to a native shared library on the OS for execution.

In this article, we will show you how to do this. We will create the following two components.

  • A custom-built WasmEdge runtime that allows WebAssembly functions to call the external native function.
  • A custom-built QuickJS interpreter that parses function calls in JavaScript, and passes external function calls to WebAssembly, which in turn passes them to native function calls.

To follow along, you need to fork or clone the wasmedge-quickjs repository from Github and check out the host_func branch.

$ git clone https://github.com/second-state/wasmedge-quickjs/
$ git checkout host_func

Embed a C-based function into WasmEdge

First, we will add a C-based function to the Wasmedge runtime so that our JavaScript program can call it later. We use the WasmEdge C API to create a HostInc function and then register it as host_inc.

The wasmedge_c/demo_wasmedge.c file contains the full source code for the host function and it’s registration to WasmEdge.

#include <stdio.h>
#include "wasmedge.h"

WasmEdge_Result HostInc(void *Data, WasmEdge_MemoryInstanceContext *MemCxt, const WasmEdge_Value *In, WasmEdge_Value *Out) {
  int32_t Val1 = WasmEdge_ValueGetI32(In[0]);
  printf("Runtime(c)=> host_inc call : %d\n",Val1 + 1);
  Out[0] = WasmEdge_ValueGenI32(Val1 + 1);
  return WasmEdge_Result_Success;
}

// mapping dirs
char* dirs = ".:..\0";
  
int main(int Argc, const char* Argv[]) {
  /* Create the configure context and add the WASI support. */
  /* This step is not necessary unless you need WASI support. */
  WasmEdge_ConfigureContext *ConfCxt = WasmEdge_ConfigureCreate();
  WasmEdge_ConfigureAddHostRegistration(ConfCxt, WasmEdge_HostRegistration_Wasi);
  /* The configure and store context to the VM creation can be NULL. */
  WasmEdge_VMContext *VMCxt = WasmEdge_VMCreate(ConfCxt, NULL);
  WasmEdge_ImportObjectContext *WasiObject = WasmEdge_VMGetImportModuleContext(VMCxt, WasmEdge_HostRegistration_Wasi);
  WasmEdge_ImportObjectInitWASI(WasiObject,Argv+1,Argc-1,NULL,0,&dirs,1,NULL,0);
  
  /* Create the import object. */
  WasmEdge_String ExportName = WasmEdge_StringCreateByCString("extern");
  WasmEdge_ImportObjectContext *ImpObj = WasmEdge_ImportObjectCreate(ExportName, NULL);
  enum WasmEdge_ValType ParamList[1] = { WasmEdge_ValType_I32 };
  enum WasmEdge_ValType ReturnList[1] = { WasmEdge_ValType_I32 };
  WasmEdge_FunctionTypeContext *HostFType = WasmEdge_FunctionTypeCreate(ParamList, 1, ReturnList, 1);
  WasmEdge_HostFunctionContext *HostFunc = WasmEdge_HostFunctionCreate(HostFType, HostInc, 0);
  WasmEdge_FunctionTypeDelete(HostFType);
  WasmEdge_String HostFuncName = WasmEdge_StringCreateByCString("host_inc");
  WasmEdge_ImportObjectAddHostFunction(ImpObj, HostFuncName, HostFunc);
  WasmEdge_StringDelete(HostFuncName);
  
  WasmEdge_VMRegisterModuleFromImport(VMCxt, ImpObj);
  
  /* The parameters and returns arrays. */
  WasmEdge_Value Params[0];
  WasmEdge_Value Returns[0];
  /* Function name. */
  WasmEdge_String FuncName = WasmEdge_StringCreateByCString("_start");
  /* Run the WASM function from file. */
  WasmEdge_Result Res = WasmEdge_VMRunWasmFromFile(VMCxt, Argv[1], FuncName, Params, 0, Returns, 0);
  
  if (WasmEdge_ResultOK(Res)) {
    printf("\nRuntime(c)=> OK\n");
  } else {
    printf("\nRuntime(c)=> Error message: %s\n", WasmEdge_ResultGetMessage(Res));
  }
  
  /* Resources deallocations. */
  WasmEdge_VMDelete(VMCxt);
  WasmEdge_ConfigureDelete(ConfCxt);
  WasmEdge_StringDelete(FuncName);
  return 0;
}

You can use a standard C compiler, such as the GCC, to compile C source code.

#build custom webassembly Runtime
$ cd wasmedge_c

#build a custom Runtime
wasmedge_c/$ gcc demo_wasmedge.c -lwasmedge_c -o demo_wasmedge

The compiler generates a binary executable file, demo_wasmedge, for a customized version of the WasmEdge runtime that contains the host function.

Create a Rust module to bind the function to JavaScript

Next, we will need to create a Rust module for QuickJS. Once compiled into the QuickJS, it can interpret JavaScript calls to host_inc and direct the call to the native C function through the WasmEdge runtime. The src/quickjs_sys/host_fun_demo_module.rs file has the full Rust source code for registering the external function in a module.

use super::*;

mod host_extern {
  #[link(wasm_import_module = "extern")]
  extern "C" {
    pub fn host_inc(v: i32) -> i32;
  }
}

unsafe extern "C" fn bind_host_inc(
    ctx: *mut JSContext,
    this_val: JSValue,
    argc: ::std::os::raw::c_int,
    argv: *mut JSValue,
) -> JSValue {
  if argv.is_null() || argc < 1 {
    return js_throw_type_error(ctx, "too few arguments to function ‘host_inc’");
  }
  let mut v = 0;
  if JS_ToInt32(ctx, &mut v, *argv.offset(0)) > 0 {
    return js_exception();
  }
  JS_NewInt32_real(ctx, host_extern::host_inc(v))
}

unsafe extern "C" fn js_module_init(
    ctx: *mut JSContext,
    m: *mut JSModuleDef,
) -> ::std::os::raw::c_int {
  JS_SetModuleExport(
    ctx,
    m,
    make_c_string("host_inc").as_ptr(),
    JS_NewCFunction_real(ctx, Some(bind_host_inc), "host_inc\0".as_ptr().cast(), 1),
  );
  0
}

pub unsafe fn init_module(ctx: *mut JSContext) -> *mut JSModuleDef {
  let name = make_c_string("host_function_demo");
  let m = JS_NewCModule(ctx, name.as_ptr(), Some(js_module_init));
  if m.is_null() {
    return m;
  }
  JS_AddModuleExport(ctx, m, make_c_string("host_inc").as_ptr());
  return m;
}

Now, we can build our custom QuickJS interpreter for WasmEdge.

$ cargo build --target wasm32-wasi --release

The WebAssembly-based QuickJS interpreter is available at target/wasm32-wasi/release/quickjs-rs-wasi.wasm.

Call the JavaScript function

The example_js/hello.js file shows how to call the host_inc function from JavaScript.

import {host_inc} from 'host_function_demo'
  
args = args.slice(1)
print("js=> Hello",...args) 
print('js=> host_inc(2)=',host_inc(2))

To run this JavaScript, you need to use our custom-built QuickJS interpreter inside our custom-built WasmEdge runtime. The interpreter and runtime are both instrumented to support the host_inc native function call.

$ cd wasmedge_c
$ export LD_LIBRARY_PATH=.
$ ./demo_wasmedge ../target/wasm32-wasi/release/quickjs-rs-wasi.wasm example_js/hello.js WasmEdge Runtime
js=> Hello WasmEdge Runtime
Runtime(c)=> host_inc call : 3
js=> host_inc(2)= 3

What’s next

Though a simple example, I have demonstrated how to turn a C-based native function into a JavaScript API. You can add many native APIs to JavaScript using the same approach. Very excited to see what you come up with!

JavaScript in cloud-native WebAssembly is still an emerging area in the next generation of cloud and edge computing infrastructure. We are just getting started! If you are interested, join us in the WasmEdge project (or tell us what you want by raising feature request issues).

ProductWasmEdgeJavaScriptRust
A high-performance, extensible, and hardware optimized WebAssembly Virtual Machine for automotive, cloud, AI, and blockchain applications