用 Rust 創建高性能 JavaScript API,並在 WebAssembly 裏運行

WasmEdge 將 Rust 的性能和 JavaScript 的易用性結合在一起

在我之前的文章中,我討論了如何將 JavaScript 代碼嵌入到 Rust 程序。然而,對於 JavaScript 開發者來說,需求往往相反——是將 Rust 函數合併到 JavaScript API 中。這使開發者能夠使用 “純 JavaScript” 編寫程序,同時仍然可以利用 Rust 函數的絕佳性能。使用 WasmEdge Runtime[1] ,你可以做到這一點。

接下來,讓我們看幾個示例。查看 wasmedge-quickjs[2] Github repo 並打開 examples/embed_js 文件夾。

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

要先安裝 Rust[3] 和  WasmEdge[4] 才能構建和運行本文中的示例。

embed_js demo 展示了幾個關於如何在 Rust 中嵌入 JavaScript 的不同示例。你可以按如下方式構建和運行所有示例。

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

創建一個 JavaScript 函數 API

以下代碼片段定義了一個 Rust 函數,該函數可以作爲 API 合併到 JavaScript 解釋器中。

fn run_rust_function(ctx: &mut Context) {

    struct HelloFn;
    impl JsFn for HelloFn {
        fn call(_ctx: &mut Context, _this_val: JsValue, argv: &[JsValue]) -> JsValue {
            println!("hello from rust");
            println!("argv={:?}", argv);
            JsValue::UnDefined
        }
    }
    
    ...
}

下面的代碼片段展示瞭如何將這個 Rust 函數添加到 JavaScript 解釋器中,命名爲 hi() 作爲它的 JavaScript API,然後從 JavaScript 代碼中調用它。

fn run_rust_function(ctx: &mut Context) {
    ...
    
    let f = ctx.new_function::<HelloFn>("hello");
    ctx.get_global().set("hi", f.into());
    let code = r#"hi(1,2,3)"#;
    let r = ctx.eval_global_str(code);
    println!("return value:{:?}", r);
}

執行結果如下:

hello from rust
argv=[Int(1), Int(2), Int(3)]
return value:UnDefined

使用這種方法,可以創建一個帶有自定義 API 函數的 JavaScript 解釋器。解釋器在 WasmEdge 內部運行,可以從 CLI 或網絡執行調用此類 API 函數的 JavaScript 代碼。

創建 JavaScript 對象 API

在 JavaScript API 設計中,我們有時需要提供一個同時封裝數據和函數的對象。在以下示例中,我們爲 JavaScript API 定義了一個 Rust 函數。

fn rust_new_object_and_js_call(ctx: &mut Context) {

    struct ObjectFn;
    impl JsFn for ObjectFn {
        fn call(_ctx: &mut Context, this_val: JsValue, argv: &[JsValue]) -> JsValue {
            println!("hello from rust");
            println!("argv={:?}", argv);
            if let JsValue::Object(obj) = this_val {
                let obj_map = obj.to_map();
                println!("this={:#?}", obj_map);
            }
            JsValue::UnDefined
        }
    }

然後我們在 Rust 端創建一個 “對象”,設置它的數據字段,然後將 Rust 函數註冊爲與對象關聯的 JavaScript 函數。

    let mut obj = ctx.new_object();
    obj.set("a", 1.into());
    obj.set("b", ctx.new_string("abc").into());
    
    let f = ctx.new_function::<ObjectFn>("anything");
    obj.set("f", f.into());

接下來,我們使 Rust “對象” 作爲 JavaScript 對象 test_obj在 JavaScript 解釋器中可用。

ctx.get_global().set("test_obj", obj.into());

在 JavaScript 代碼中,你現在可以直接使用 test_obj 作爲 API 的一部分。

let code = r#"
      print('test_obj keys=',Object.keys(test_obj))
      print('test_obj.a=',test_obj.a)
      print('test_obj.b=',test_obj.b)
      test_obj.f(1,2,3,"hi")
    "#;

    ctx.eval_global_str(code);
}

執行結果如下。

test_obj keys= a,b,f
test_obj.a= 1
test_obj.b= abc
hello from rust
argv=[Int(1), Int(2), Int(3), String(JsString(hi))]
this=Ok(
    {
        "a": Int(
            1,
        ),
        "b": String(
            JsString(
                abc,
            ),
        ),
        "f": Function(
            JsFunction(
                function anything() {
                    [native code]
                },
            ),
        ),
    },
)

一個完整的 JavaScript 對象 API

在前面的示例中,我們演示了從 Rust 創建 JavaScript API 的簡單示例。在這個例子中,我們將創建一個完整的 Rust 模塊並將其作爲 JavaScript 對象 API 提供。該項目位於 examples/embed_rust_module[5] 文件夾中。你可以在 WasmEdge 中將其作爲標準 Rust 應用程序構建和運行。

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

該對象的 Rust 實現是一個模塊,如下所示。它具有數據字段、構造函數、getter 和 setter 以及函數。

mod point {
    use wasmedge_quickjs::*;

    #[derive(Debug)]
    struct Point(i32, i32);

    struct PointDef;

    impl JsClassDef<Point> for PointDef {
        const CLASS_NAME: &'static str = "Point\0";
        const CONSTRUCTOR_ARGC: u8 = 2;

        fn constructor(_: &mut Context, argv: &[JsValue]) -> Option<Point> {
            println!("rust-> new Point {:?}", argv);
            let x = argv.get(0);
            let y = argv.get(1);
            if let ((Some(JsValue::Int(ref x)), Some(JsValue::Int(ref y)))) = (x, y) {
                Some(Point(*x, *y))
            } else {
                None
            }
        }

        fn proto_init(p: &mut JsClassProto<Point, PointDef>) {
            struct X;
            impl JsClassGetterSetter<Point> for X {
                const NAME: &'static str = "x\0";

                fn getter(_: &mut Context, this_val: &mut Point) -> JsValue {
                    println!("rust-> get x");
                    this_val.0.into()
                }

                fn setter(_: &mut Context, this_val: &mut Point, val: JsValue) {
                    println!("rust-> set x:{:?}", val);
                    if let JsValue::Int(x) = val {
                        this_val.0 = x
                    }
                }
            }

            struct Y;
            impl JsClassGetterSetter<Point> for Y {
                const NAME: &'static str = "y\0";

                fn getter(_: &mut Context, this_val: &mut Point) -> JsValue {
                    println!("rust-> get y");
                    this_val.1.into()
                }

                fn setter(_: &mut Context, this_val: &mut Point, val: JsValue) {
                    println!("rust-> set y:{:?}", val);
                    if let JsValue::Int(y) = val {
                        this_val.1 = y
                    }
                }
            }

            struct FnPrint;
            impl JsMethod<Point> for FnPrint {
                const NAME: &'static str = "pprint\0";
                const LEN: u8 = 0;

                fn call(_: &mut Context, this_val: &mut Point, _argv: &[JsValue]) -> JsValue {
                    println!("rust-> pprint: {:?}", this_val);
                    JsValue::Int(1)
                }
            }

            p.add_getter_setter(X);
            p.add_getter_setter(Y);
            p.add_function(FnPrint);
        }
    }

    struct PointModule;
    impl ModuleInit for PointModule {
        fn init_module(ctx: &mut Context, m: &mut JsModuleDef) {
            m.add_export("Point\0", PointDef::class_value(ctx));
        }
    }

    pub fn init_point_module(ctx: &mut Context) {
        ctx.register_class(PointDef);
        ctx.register_module("point\0", PointModule, &["Point\0"]);
    }
}

在解釋器的實現中,首先調用 point::init_point_module 將 Rust 模塊註冊到 JavaScript 上下文中,然後我們就可以運行一個簡單地使用 point 對象的 JavaScript 程序。

use wasmedge_quickjs::*;
fn main() {
    let mut ctx = Context::new();
    point::init_point_module(&mut ctx);

    let code = r#"
      import('point').then((point)=>{
        let p0 = new point.Point(1,2)
        print("js->",p0.x,p0.y)
        p0.pprint()
        try{
            let p = new point.Point()
            print("js-> p:",p)
            print("js->",p.x,p.y)
            p.x=2
            p.pprint()
        } catch(e) {
            print("An error has been caught");
            print(e)
        }    
      })
    "#;

    ctx.eval_global_str(code);
    ctx.promise_loop_poll();
}

上述應用程序的執行結果如下。

rust-> new Point [Int(1), Int(2)]
rust-> get x
rust-> get y
js-> 1 2
rust-> pprint: Point(1, 2)
rust-> new Point []
js-> p: undefined
An error has been caught
TypeError: cannot read property 'x' of undefined

接下來

使用 Rust 函數和模塊來實現 JavaScript API 是一個強大的想法。這允許 WasmEdge Runtime 顯着提高 JavaScript 應用程序的性能。然而,Rust 函數仍然需要編譯成 WebAssembly 字節碼。對於一些函數,比如 AI 推理,直接從 WasmEdge 調用原生 C 庫函數效率更高。在下一篇文章中,我將討論如何運用 WasmEdge 來支持 C 原生函數,然後將這些函數公開爲 Rust 和 JavaScript API。

雲原生 WebAssembly 中的 JavaScript 是下一代雲和邊緣計算基礎設施中的新興領域。我們也是剛開始涉足。如果對此感興趣,請加入 WasmEdge 項目 [6] 或通過提出 feature request issue 告訴我們,你需要的是什麼。

參考資料

[1]

WasmEdge Runtime: https://github.com/WasmEdge/WasmEdge

[2]

wasmedge-quickjs: https://github.com/second-state/wasmedge-quickjs/

[3]

Rust: https://www.rust-lang.org/tools/install

[4]

WasmEdge: https://github.com/WasmEdge/WasmEdge/blob/master/docs/install.md

[5]

examples/embed_rust_module: https://github.com/second-state/wasmedge-quickjs/tree/main/examples/embed_rust_module

[6]

WasmEdge 項目: https://github.com/WasmEdge/WasmEdge

關於 WasmEdge

WasmEdge 是輕量級、安全、高性能、實時的軟件容器與運行環境。目前是 CNCF 沙箱項目。WasmEdge 被應用在 SaaS、雲原生,service mesh、邊緣計算、汽車等領域。

** ✨ GitHub**:https://github.com/WasmEdge/WasmEdge

** 💻 官網**:https://wasmedge.org/

 👨‍💻‍ Slack **羣:**CNCF slack 搜索 WasmEdge

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qY5tzgfH5fD0BdM1HtN3tw