#nodejs #node #js #module #native #idiomatic #write

ohos-node-bindgen

使用Rust轻松编写nodejs模块

2个稳定版本

6.0.3 2024年2月14日
6.0.2 2024年2月13日

#18 in #idiomatic

Apache-2.0

41KB
644 代码行(不含注释)

ohos-node-bindgen

使用惯用Rust轻松编写原生Node.js模块

兼容华为鸿蒙ArkTS N-API

如何实现兼容

在原作者的伟大作品基础上,我做了三项工作使node-bindgen支持华为鸿蒙ArkTS N-API开发

  1. 将封装了nodejs N-API的内部子工程nj-sys替换为包装了【鸿蒙ArkTS N-API】的外部依赖项oh-napi-sys
    1. 一方面,node-bindgen原作者的代码设计非常科学合理,所以对核心模块*-sys crate的替换工作很省心。
    2. 另一方面,【鸿蒙ArkTS N-API】与nodejs N-API的相似度极高。所以,模块替换后的适配工作量少之又少。
  2. 添加【编译条件】 — 这算是一处适配点
    1. 原因:【C无符号长整类型unsigned long】在鸿蒙armv7架构上是32bit,而在鸿蒙aarch64x86_64架构上却是64bit。所以,若既不搞【条件编译】又不预备多套代码,那么rustc就会交叉编译失败。感谢Rust的【条件编译】语言支持,让Cpp开发都哭去吧!
    2. 打广告了:在该基建之上做鸿蒙ArkTS N-API开发的中国同胞们就不用再分心考虑这类【架构差异】的技术细节了。这些破事实在太糟心!
  3. 修改包名从node-bindgenohos-node-bindgen

就目前而言,【鸿蒙ArkTS N-API】与nodejs N-API大约是95%相似。但是,我相信随着【鸿蒙操作系统】的后续发展,ArkTS N-API会引入越来越多与外国同类产品(比如,nodejs / Deno)不同的内容。

ohos-node-bindgen用法

新/旧用法差异不在代码调用,而全部集中于Cargo.toml工程配置中

  1. 不再需要向[build-dependencies]配置表添加node-bindgen = { version = "6.0", default-features = false, features = ["build"] }依赖项了,因为【编译时链接】已完全委托给外部依赖项oh-napi-sys完成了。

  2. 输出链接库的编码格式不再是cdylib,而是dylib。即,

    [lib]
    crate-type = ["dylib"]
    

ohos-node-bindgen还不能被直接使用

原因是ohos-node-bindgen的间接依赖项socket2 v0.4.10与【华为鸿蒙操作系统】不兼容【(别急,有解决方案)】。依赖图如下

socket2 v0.4.10
├── async-io v1.13.0
   ├── async-std v1.12.0
   │   └── fluvio-future v0.6.2
   │       └── nj-core v6.0.1
   │           └── ohos-node-bindgen v6.0.2

尽管依赖“血缘”关系隔了四层之远,但它仍会阻碍交叉编译。我亲自测试的解决方案是:

  1. 【我已经完成,大家无需操作】我已经fork[email protected]分支,并解决了其对【华为鸿蒙操作系统】的兼容问题。因此,

  2. 大家可以直接克隆我的fork版本到本地硬盘,并切换到v0.4.x分支

    git clone [email protected]:stuartZhang/socket2.git
    git checkout v0.4.x
    
  3. 重写(Override)调用端工程的【依赖图】,以指示Cargo优先加载本地的socket2:0.4.10依赖项,而不是从crates.io下载。即,在Cargo.toml文件中添加以下配置

    [dependencies]
    socket2 = "0.4.10"
    [patch.crates-io]
    socket2 = { path = "<指向 socket2 本地克隆复本的完整路径>" }
    

然后,就能绕过线上的问题socket2 crate并成功进行交叉编译。

特性

  • 简单:只需编写惯用的Rust代码,node-bindgen会负责生成Node.js FFI包装代码。
  • 安全:Node.js参数会根据Rust类型自动检查。
  • 异步:支持异步Rust。异步代码会被转换为Node.js承诺。
  • 类:可以使用Node.js类访问Rust结构体。
  • 流:使用Rust实现Node.js流。
  • N-API:使用Node.js N-API,这意味着您不需要重新编译您的模块。

与Node.js版本兼容性

本项目使用Node N-API的v8。请参阅以下兼容性矩阵

支持的操作系统

  • Linux
  • MacOs
  • Windows

为什么选择node-bindgen?

编写原生node-js需要大量的样板代码。Node-bindgen从rust代码生成外部"C"粘合代码,包括原生模块注册。node-bindgen让编写node-js模块变得简单且有趣。

入门指南

CLI安装

安装nj-cli命令行,该命令行将被用于生成原生库。

cargo install nj-cli

这是一个一次性步骤。

配置Cargo.toml

将两个依赖项添加到项目的Cargo.toml文件中。

node-bindgen作为常规依赖项添加(如下所示)

[dependencies]
node-bindgen = { version = "6.0" }

然后更新crate类型为dylib以生成与node.js兼容的原生模块

[lib]
crate-type = ["dylib"]

最后,在项目的顶部添加build.rs文件,并包含以下内容

fn main() {
    ohos_node_bindgen::build::configure();
}

示例

这是一个添加两个数字的函数。请注意,您无需担心JS转换。


use ohos_node_bindgen::derive::node_bindgen;

/// add two integer
#[ohos_node_bindgen]
fn sum(first: i32, second: i32) -> i32 {
    first + second
}

构建原生库

要构建node.js库,使用nj-cli进行构建

nj-cli build

这将生成位于"./dist"文件夹中的Node.js模块。

构建发布版本

nj-cli build --release

监视./src中的更改

在开发您的原生模块时,您可能希望监视文件更改,并在更改发生时运行命令,例如cargo checkcargo build

为此,我们可以使用nj-cli watch

nj-cli watch安装(如果不存在)并传递参数给cargo watch。默认情况下,nj-cli watch将运行cargo check对您的./src文件。

要查看nj-cli watch的所有可用方法,请运行以下命令

nj-cli watch-- --help

在Node.js中使用

然后在Node.js中,Rust函数可以像普通Node.js函数一样调用。

$ node
Welcome to Node.js v18.18.0.
Type ".help" for more information.
> let addon = require('./dist');
undefined
> addon.sum(2,3)
5
>

特性

函数名或方法可以重命名,而不是使用默认映射。

#[node_bindgen(name="multiply")]
fn mul(first: i32,second: i32) -> i32 {
    first * second
}

Rust函数mul被重命名为multiply

可选参数

如果参数被标记为可选,则可以跳过。

#[ohos_node_bindgen]
fn sum(first: i32, second: Option<i32>) -> i32 {
    first + second.unwrap_or(0)
}

然后sum可以像这样调用:sum(10)sum(10,20)

回调

JS回调映射为Rust闭包。

#[ohos_node_bindgen]
fn hello<F: Fn(String)>(first: f64, second: F) {

    let msg = format!("argument is: {}", first);

    second(msg);
}

来自node

let addon = require('./dist');

addon.hello(2,function(msg){
  assert.equal(msg,"argument is: 2");
  console.log(msg);  // print out argument is 2
});

异步Rust也支持回调。

异步Rust支持

异步Rust函数映射到Node.js的Promise。


use std::time::Duration;
use flv_future_aio::time::sleep;
use ohos_node_bindgen::derive::node_bindgen;


#[ohos_node_bindgen]
async fn hello(arg: f64) -> f64 {
    println!("sleeping");
    sleep(Duration::from_secs(1)).await;
    println!("woke and adding 10.0");
    arg + 10.0
}
let addon = require('./dist');

addon.hello(5).then((val) => {
  console.log("future value is %s",val);
});

结构体序列化

结构体,包括泛型结构体,可以自动生成到JS的转换模板。只需将node_bindgen宏应用到你的结构体上。

#[ohos_node_bindgen]
struct MyJson {
    some_name: String,
    a_number: i64
}

#[ohos_node_bindgen]
fn my_json() -> MyJson {
    MyJson {
        some_name: "John".to_owned(),
        a_number: 1337
    }
}
let addon = require('./dist');
assert.deepStrictEqual(addon.my_json(), {
    someName: "John",
    aNumber: 1337
});

注意,字段必须自己实现ohos_node_bindgen::core::TryIntoJs。任何引用也必须实现Clone。字段名将被转换为驼峰式。

枚举

枚举也将使用ohos_node_bindgen自动生成其JS表示。

#[ohos_node_bindgen]
enum ErrorType {
    WithMessage(String, usize),
    WithFields {
        val: usize
    },
    UnitErrorType
}

#[ohos_node_bindgen]
fn with_message() -> ErrorType {
    ErrorType::WithMessage("test".to_owned(), 321)
}

#[ohos_node_bindgen]
fn with_fields() -> ErrorType {
    ErrorType::WithFields {
        val: 123
    }
}

#[ohos_node_bindgen]
fn with_unit() -> ErrorType {
    ErrorType::UnitErrorType
}
assert.deepStrictEqual(addon.withMessage(), {
    withMessage: ["test", 321n]
});
assert.deepStrictEqual(addon.withFields(), {
    withFields: {
        val: 123n
    }
});
assert.deepStrictEqual(addon.withUnit(), "UnitErrorType")

元组变体会转换为列表,结构体变体转换为对象,单元变体转换为与变体名称匹配的字符串(帕斯卡大小写)。支持泛型和引用,与结构体相同的注意事项。

JavaScript类

支持JavaScript类。


struct MyClass {
    val: f64,
}


#[ohos_node_bindgen]
impl MyClass {

    #[node_bindgen(constructor)]
    fn new(val: f64) -> Self {
        Self { val }
    }

    #[ohos_node_bindgen]
    fn plus_one(&self) -> f64 {
        self.val + 1.0
    }

    #[node_bindgen(getter)]
    fn value(&self) -> f64 {
        self.val
    }
}
let addon = require('./dist');
const assert = require('assert');

let obj = new addon.MyObject(10);
assert.equal(obj.value,10,"verify value works");
assert.equal(obj.plusOne(),11);

示例文件夹中还有更多功能。

准备npm包

使用node-bindgen生成的Node模块可以直接在任何Node.js项目中使用,只需将index.node复制到其中。但是,如果直接访问模块,IDE将不会突出显示可用的函数、类等。通常,这不太方便,并且一旦Node模块的公共API发生变化,就会增加潜在错误的风险。

要创建一个完整的npm包,包括TypeScript类型定义和所有必要的JavaScript包装器,可以使用crate tslink

tslink crate生成描述npm模块的文件*.d.ts*.jspackage.json。这样的包可以以最小的努力集成到最终项目中。

此外,因为tslink生成TypeScript类型定义,所以对本地Node模块(index.node)的任何更改都会由TypeScript编译器突出显示,这大大降低了与更改的API或公共数据类型相关的错误风险。

例如,

#[macro_use] extern crate tslink;
use tslink::tslink;
use ohos_node_bindgen::derive::node_bindgen;

struct MyScruct {
    inc: i32,
}

#[tslink(class)]
#[ohos_node_bindgen]
impl MyScruct {
    #[tslink(constructor)]
    #[node_bindgen(constructor)]
    pub fn new(inc: i32) -> Self {
        Self { inc }
    }

    #[tslink(snake_case_naming)]
    #[ohos_node_bindgen]
    fn inc_my_number(&self, a: i32) -> i32 {
        a + self.inc
    }
}

将表示为(*.d.ts)为

export declare class MyStruct {
    constructor(inc: number);
    incMyNumber(a: number): number;
}

请注意,调用#[tslink]应该始终在调用#[ohos_node_bindgen]之前。

此外,请注意,node-bindgen 默认情况下将方法名称转换为蛇形命名。您应该使用 #[tslink(snake_case_naming)] 来考虑这一点(更多信息请见 crate 页面)。

tslink 需要在 Cargo.toml 中进行配置(项目根目录下的 [tslink] 部分)。配置应包括指向原生节点模块的有效路径。默认情况下,node-bindgen 在您的 root./dist 文件夹中创建 index.node

文件:./Cargo.toml(项目根目录下)

[project]
...
[lib]
...
[tslink]
node = "./dist/index.node"

使用 tslinknode-bindgen 的完整示例请见 此处

有关 tslink 的更多 API 文档,请参阅 crate 页面

注意。node-bindgen 的开发者不对 tslink crate 工作的正确性负责。所有与 tslink 相关的可能问题和功能请求都应联系 tslink 的开发者。

贡献

如果您想为该项目做出贡献,请阅读我们的 贡献指南

许可证

本项目受 Apache 许可证许可。

依赖

~0–15MB
~134K SLoC