Incorporating JavaScript into a Rust app

• 5 minutes to read

WasmEdge brings together Rust’s performance and JavaScript’s ease of use

In my previous article, I discussed how to run JavaScript programs in a WebAssembly sandbox. The WasmEdge runtime provides a lightweight, high-performance, and OCI compliant “container” for cloud-native JavaScript applications.

However, JavaScript is a “slow” language. The reason to use JavaScript is mostly due to its ease of use and developer productivity, especially for beginner developers. WebAssembly, on the other hand, is capable of running high-performance applications written in languages such as Rust. Is there a solution that combines JavaScript and Rust? WasmEdge offers the best of both worlds.

With WasmEdge and QuickJS, developers can now mix and match those two powerful languages to create applications. It works like this.

  • The JavaScript interpreter is written in Rust and compiled into WebAssembly. It can be compiled and deployed as a single wasm bytecode file. It can be managed by OCI-compliant container tools like Docker Hub, CRI-O and k8s.
  • JavaScript programs are embedded inside the Rust application. The JavaScript source code could be included at compile-time, or at runtime through a file, STDIN, a network request, or even a function call. That allows the JavaScript program to be written by a different developer than the Rust program.
  • The Rust program could handle computationally intensive tasks in the application, while the JavaScript program could handle the “business logic”. For example, the Rust program could prepare data for the JavaScript program.

Next, let’s look into several examples. Check out the wasmedge-quickjs Github repo and change to the examples/embed_js folder to follow along.

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

You must have Rust and WasmEdge installed to build and run the examples in this article.

The embed_js demo showcases several different examples on how to embed JavaScript inside Rust. You can build and run all the examples as follows.

$ cargo build --target wasm32-wasi --release
$ wasmedge --dir .:. target/wasm32-wasi/release/embed_js.wasm

Hello WasmEdge

The following Rust function main.rs embeds an one-line JavaScript program at compile-time.

fn js_hello(ctx: &mut Context) {
    let code = r#"print('hello quickjs')"#;
    let r = ctx.eval_global_str(code);
    println!("return value:{:?}", r);
}

Since the embedded JavaScript program does not return anything, you will see the execution result as follows.

hello quickjs
return value:UnDefined

Return value

Of course, the embedded JavaScript program could return a value to the Rust program.

fn run_js_code(ctx: &mut Context) {
    let code = r#"
      let a = 1+1;
      print('js print: 1+1=',a);
      'hello'; // eval_return
    "#;
    let r = ctx.eval_global_str(code);
    println!("return value:{:?}", r);
}

The return value is a string hello. The output from running this Rust program is as follows. Note that the JavaScript string value is wrapped in JsString in Rust.

js print: 1+1= 2
return value:String(JsString(hello))

Call an embedded JavaScript function

From the Rust program, you can call a JavaScript function, pass in call parameters, and capture the return value. This first example shows passing a Rust string into a JavaScript function.

fn run_js_function(ctx: &mut Context) {
    let code = r#"
      (x)=>{
          print("js print: x=",x)
      }
    "#;
    let r = ctx.eval_global_str(code);
    if let JsValue::Function(f) = r {
        let hello_str = ctx.new_string("hello");
        let mut argv = vec![hello_str.into()];
        let r = f.call(&mut argv);
        println!("return value:{:?}", r);
    }

    ...
}

The value r from ctx.eval_global_str(code) is a Rust function that maps to the JavaScript function. You can call the Rust function and get the results you would expect from calling the JavaScript function.

js print: x= hello
return value:UnDefined

You could also pass an array from Rust to a JavaScript function, and let the JavaScript function return a value.

fn run_js_function(ctx: &mut Context) {
    ...

    let code = r#"
      (x)=>{
          print("\nx=",x)
          let old_value = x[0]
          x[0] = 1
          return old_value
      }
    "#;
    let r = ctx.eval_global_str(code);
    if let JsValue::Function(f) = r {
        let mut x = ctx.new_array();
        x.set(0, 0.into());
        x.set(1, 1.into());
        x.set(2, 2.into());

        let mut argv = vec![x.into()];
        println!("argv = {:?}", argv);
        let r = f.call(&mut argv);
        println!("return value:{:?}", r);
    }
}

The result is as follows.

argv = [Array(JsArray(0,1,2))]
x= 0,1,2
return value:Int(0)

Use JavaScript modules

The embedded JavaScript program can load external JavaScript files as modules. The examples/embed_js_module project demonstrates this use case. The async_demo.js file contains a JavaScript module with an exported function, which we will then import and use in our embedded JavaScript.

import * as std from 'std'

async function simple_val (){
    return "abc"
}

export async function wait_simple_val (a){
    let x = await simple_val()
    print("wait_simple_val:",a,':',x)
    return 12345
}

The Rust program source code imports the async_demo.js module asynchronously as the ES spec requires. The execution result p is a Rust Promise that will have the correct value after the asynchronous functions return. The ctx.promise_loop_poll() call in Rust waits until the asynchrnous execution to complete. We can then receive the return value from the Promise.

use wasmedge_quickjs::*;

fn main() {
    let mut ctx = Context::new();

    let code = r#"
      import('async_demo.js').then((demo)=>{
        return demo.wait_simple_val(1)
      })
    "#;

    let p = ctx.eval_global_str(code);
    ctx.promise_loop_poll();
    println!("after poll:{:?}", p);
    if let JsValue::Promise(ref p) = p {
        let v = p.get_result();
        println!("v = {:?}", v);
    }
}

The result is as follows.

wait_simple_val: 1 : abc
after poll:Promise(JsPromise([object Promise]))
v = Int(12345)

What’s next

Embedding JavaScript in Rust is a powerful way to create high-performance cloud-native applications. In the next article, we will look into another approach: using Rust to implement JavaScript APIs. It allows JavaScript developers to take advantage of high performance Rust functions inside the WasmEdge Runtime.

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