用 Rust 实现 JS API

对于 JavaScript 开发者来说,将 Rust 函数结合到 JavaScript API 中是非常有用的。这样做,使得开发者在用”纯 JavaScript”编写程序的同时,也可使用高性能的 Rust 函数。你可以使用 WasmEdge Runtime 做到这一点。

在开始之前,克隆 GitHub 仓库 wasmedge-quickjs,并切换到目录 examples/embed_js。该操作如下所示。

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

你必须安装有 RustWasmEdge 以构建与运行我们展示给你的示例。

对于如何将 JavaScript 嵌入到 Rust 中,embed_js 列出了几个不同的例子。你可以按如下的命令构建并运行这些示例。

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

注:命令行中的 --dir .:. 给予了 wasmedge 读取文件系统的本地目录的权限。

创建一个 JavaScript 函数 API

下面这个代码片段定义了一个 Rust 函数,该函数可以作为一个 API 结合到 JavaScript 解释器中。

  1. #![allow(unused)]
  2. fn main() {
  3. fn run_rust_function(ctx: &mut Context) {
  4. struct HelloFn;
  5. impl JsFn for HelloFn {
  6. fn call(_ctx: &mut Context, _this_val: JsValue, argv: &[JsValue]) -> JsValue {
  7. println!("hello from rust");
  8. println!("argv={:?}", argv);
  9. JsValue::UnDefined
  10. }
  11. }
  12. ...
  13. }
  14. }

下面的代码展示了如何把这个 Rust 函数添加到 JavaScript 解释器中,并为该函数的 JavaScript API 指定一个签名 hi(),随之在 JavaScript 代码中调用它。

  1. #![allow(unused)]
  2. fn main() {
  3. fn run_rust_function(ctx: &mut Context) {
  4. ...
  5. let f = ctx.new_function::<HelloFn>("hello");
  6. ctx.get_global().set("hi", f.into());
  7. let code = r#"hi(1,2,3)"#;
  8. let r = ctx.eval_global_str(code);
  9. println!("return value:{:?}", r);
  10. }
  11. }

执行的结果如下所示。

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 函数。

  1. #![allow(unused)]
  2. fn main() {
  3. fn rust_new_object_and_js_call(ctx: &mut Context) {
  4. struct ObjectFn;
  5. impl JsFn for ObjectFn {
  6. fn call(_ctx: &mut Context, this_val: JsValue, argv: &[JsValue]) -> JsValue {
  7. println!("hello from rust");
  8. println!("argv={:?}", argv);
  9. if let JsValue::Object(obj) = this_val {
  10. let obj_map = obj.to_map();
  11. println!("this={:#?}", obj_map);
  12. }
  13. JsValue::UnDefined
  14. }
  15. }
  16. ...
  17. }
  18. }

然后,我们在 Rust 端创建一个”对象”,设置它的数据域,然后将该 Rust 函数注册为关联到该对象的 JavaScript 函数。

  1. #![allow(unused)]
  2. fn main() {
  3. let mut obj = ctx.new_object();
  4. obj.set("a", 1.into());
  5. obj.set("b", ctx.new_string("abc").into());
  6. let f = ctx.new_function::<ObjectFn>("anything");
  7. obj.set("f", f.into());
  8. }

接下来,我们使这个 Rust “对象” 能在 JavaScript 解释器中作为 JavaScript 对象 test_obj 使用。

  1. #![allow(unused)]
  2. fn main() {
  3. ctx.get_global().set("test_obj", obj.into());
  4. }

在 JavaScript 代码中,你可以直接将 test_obj 作为 API 的一部分使用了。

  1. #![allow(unused)]
  2. fn main() {
  3. let code = r#"
  4. print('test_obj keys=',Object.keys(test_obj))
  5. print('test_obj.a=',test_obj.a)
  6. print('test_obj.b=',test_obj.b)
  7. test_obj.f(1,2,3,"hi")
  8. "#;
  9. ctx.eval_global_str(code);
  10. }

代码执行的结果如下。

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 中。你可以构建并将其当做一个标准的 Rust 应用,在 WasmEdge 中运行它。

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

该对象的 Rust 实现,如下面这个模块所示。它含有数据域、构造函数、访问器、设置器以及函数。

  1. #![allow(unused)]
  2. fn main() {
  3. mod point {
  4. use wasmedge_quickjs::*;
  5. #[derive(Debug)]
  6. struct Point(i32, i32);
  7. struct PointDef;
  8. impl JsClassDef<Point> for PointDef {
  9. const CLASS_NAME: &'static str = "Point\0";
  10. const CONSTRUCTOR_ARGC: u8 = 2;
  11. fn constructor(_: &mut Context, argv: &[JsValue]) -> Option<Point> {
  12. println!("rust-> new Point {:?}", argv);
  13. let x = argv.get(0);
  14. let y = argv.get(1);
  15. if let ((Some(JsValue::Int(ref x)), Some(JsValue::Int(ref y)))) = (x, y) {
  16. Some(Point(*x, *y))
  17. } else {
  18. None
  19. }
  20. }
  21. fn proto_init(p: &mut JsClassProto<Point, PointDef>) {
  22. struct X;
  23. impl JsClassGetterSetter<Point> for X {
  24. const NAME: &'static str = "x\0";
  25. fn getter(_: &mut Context, this_val: &mut Point) -> JsValue {
  26. println!("rust-> get x");
  27. this_val.0.into()
  28. }
  29. fn setter(_: &mut Context, this_val: &mut Point, val: JsValue) {
  30. println!("rust-> set x:{:?}", val);
  31. if let JsValue::Int(x) = val {
  32. this_val.0 = x
  33. }
  34. }
  35. }
  36. struct Y;
  37. impl JsClassGetterSetter<Point> for Y {
  38. const NAME: &'static str = "y\0";
  39. fn getter(_: &mut Context, this_val: &mut Point) -> JsValue {
  40. println!("rust-> get y");
  41. this_val.1.into()
  42. }
  43. fn setter(_: &mut Context, this_val: &mut Point, val: JsValue) {
  44. println!("rust-> set y:{:?}", val);
  45. if let JsValue::Int(y) = val {
  46. this_val.1 = y
  47. }
  48. }
  49. }
  50. struct FnPrint;
  51. impl JsMethod<Point> for FnPrint {
  52. const NAME: &'static str = "pprint\0";
  53. const LEN: u8 = 0;
  54. fn call(_: &mut Context, this_val: &mut Point, _argv: &[JsValue]) -> JsValue {
  55. println!("rust-> pprint: {:?}", this_val);
  56. JsValue::Int(1)
  57. }
  58. }
  59. p.add_getter_setter(X);
  60. p.add_getter_setter(Y);
  61. p.add_function(FnPrint);
  62. }
  63. }
  64. struct PointModule;
  65. impl ModuleInit for PointModule {
  66. fn init_module(ctx: &mut Context, m: &mut JsModuleDef) {
  67. m.add_export("Point\0", PointDef::class_value(ctx));
  68. }
  69. }
  70. pub fn init_point_module(ctx: &mut Context) {
  71. ctx.register_class(PointDef);
  72. ctx.register_module("point\0", PointModule, &["Point\0"]);
  73. }
  74. }
  75. }

在解释器的实现中,我们先调用 point::init_point_module 以将 Rust 模块注册到 JavaScript 的上下文中,然后运行一个使用了对象 point 的 JavaScript 程序。

  1. use wasmedge_quickjs::*;
  2. fn main() {
  3. let mut ctx = Context::new();
  4. point::init_point_module(&mut ctx);
  5. let code = r#"
  6. import('point').then((point)=>{
  7. let p0 = new point.Point(1,2)
  8. print("js->",p0.x,p0.y)
  9. p0.pprint()
  10. try{
  11. let p = new point.Point()
  12. print("js-> p:",p)
  13. print("js->",p.x,p.y)
  14. p.x=2
  15. p.pprint()
  16. } catch(e) {
  17. print("An error has been caught");
  18. print(e)
  19. }
  20. })
  21. "#;
  22. ctx.eval_global_str(code);
  23. ctx.promise_loop_poll();
  24. }

上面这个应用的执行结果如下所示。

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