Calling native functions from JavaScript

Sep 10, 2021 • 5 minutes to read

WasmEdge enables JavaScript to call native functions in shared libraries.

In my previous articles, I explained why and how to run JavaScript programs in a WebAssembly sandbox. I also discussed how to create custom JavaScript APIs for WasmEdge using Rust.

However, in order to fully access the underlying system's OS and hardware features, we sometimes need 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. The example is in the examples/host_function folder of this repository.

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

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 program to bind the function to JavaScript

Next, we will need to create a customized JavaScript interpreter in Rust. It interprets JavaScript calls to host_inc and directs the call to the native C function through the customized WasmEdge runtime (demo_wasmedge). The src/main.rs file has the full Rust source code for registering the external function.

mod host_extern {
    use quickjs_rs_wasi::{Context, JsValue};

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

    pub struct HostIncFn;
    impl quickjs_rs_wasi::JsFn for HostIncFn {
        fn call(ctx: &mut Context, _this_val: JsValue, argv: &[JsValue]) -> JsValue {
            if let Some(JsValue::Int(i)) = argv.get(0) {
                unsafe {
                    let r = host_inc(*i);
                    r.into()
                }
            } else {
                ctx.throw_type_error("'v' is not a int").into()
            }
        }
    }
}

use quickjs_rs_wasi::*;

fn main() {
    let mut ctx = Context::new();
    let f = ctx.new_function::<host_extern::HostIncFn>("host_inc");
    ctx.get_global().set("host_inc", f.into());
    
    // Run the embedded JavaScript
    ctx.eval_global_str("print('js=> host_inc(2)=',host_inc(2))");
}

The Rust program creates a customized QuickJS interpreter, and then executes a JavaScript program which in turn calls the C-based native function registered in the WasmEdge runtime.

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

The customized QuickJS interpreter with the embedded JavaScript program is available at target/wasm32-wasi/release/host_function.wasm.

Call the JavaScript function

The embedded JavaScript program calls the host_inc() function. The JavaScript interpreter (the Rust program for host_function.wasm) routes this call to a WebAssembly host_inc() call. The customized WasmEdge runtime (the C program for demo_wasmedge) routes the WebAssembly call to a native C function.

print('js=> host_inc(2)=',host_inc(2))

Of course, you can also write a generic Rust program that reads the JavaScript from a file.

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 --dir .:. ../target/wasm32-wasi/release/host_function.wasm
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!

Articles in this series:

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