2个稳定版本
6.0.3 | 2024年2月14日 |
---|---|
6.0.2 | 2024年2月13日 |
#18 in #idiomatic
41KB
644 代码行(不含注释)
ohos-node-bindgen
兼容华为鸿蒙ArkTS N-API
如何实现兼容
在原作者的伟大作品基础上,我做了三项工作使node-bindgen
支持华为鸿蒙ArkTS N-API
开发
- 将封装了nodejs N-API的内部子工程
nj-sys
替换为包装了【鸿蒙ArkTS N-API
】的外部依赖项oh-napi-sys。- 一方面,
node-bindgen
原作者的代码设计非常科学合理,所以对核心模块*-sys crate
的替换工作很省心。 - 另一方面,【鸿蒙
ArkTS N-API
】与nodejs N-API
的相似度极高。所以,模块替换后的适配工作量少之又少。
- 一方面,
- 添加【编译条件】 — 这算是一处适配点
- 原因:【
C无符号长整类型
unsigned long
】在鸿蒙armv7
架构上是32bit
,而在鸿蒙aarch64
与x86_64
架构上却是64bit
。所以,若既不搞【条件编译】又不预备多套代码,那么rustc
就会交叉编译失败。感谢Rust
的【条件编译】语言支持,让Cpp
开发都哭去吧! - 打广告了:在该基建之上做鸿蒙
ArkTS N-API
开发的中国同胞们就不用再分心考虑这类【架构差异】的技术细节了。这些破事实在太糟心!
- 原因:【
- 修改包名从
node-bindgen
至ohos-node-bindgen
。
就目前而言,【鸿蒙ArkTS N-API
】与nodejs N-API
大约是95%
相似。但是,我相信随着【鸿蒙操作系统】的后续发展,ArkTS N-API
会引入越来越多与外国同类产品(比如,nodejs / Deno
)不同的内容。
ohos-node-bindgen
用法
新/旧用法差异不在代码调用,而全部集中于Cargo.toml
工程配置中
-
不再需要向
[build-dependencies]
配置表添加node-bindgen = { version = "6.0", default-features = false, features = ["build"] }
依赖项了,因为【编译时链接】已完全委托给外部依赖项oh-napi-sys完成了。 -
输出链接库的编码格式不再是
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
尽管依赖“血缘”关系隔了四层之远,但它仍会阻碍交叉编译。我亲自测试的解决方案是:
-
【我已经完成,大家无需操作】我已经
fork
了[email protected]分支,并解决了其对【华为鸿蒙操作系统】的兼容问题。因此, -
大家可以直接克隆我的
fork
版本到本地硬盘,并切换到v0.4.x
分支git clone [email protected]:stuartZhang/socket2.git git checkout v0.4.x
-
重写(
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 check
或cargo 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
、*.js
和package.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"
使用 tslink
和 node-bindgen
的完整示例请见 此处。
有关 tslink
的更多 API 文档,请参阅 crate 页面。
注意。node-bindgen 的开发者不对 tslink crate 工作的正确性负责。所有与 tslink 相关的可能问题和功能请求都应联系 tslink 的开发者。
贡献
如果您想为该项目做出贡献,请阅读我们的 贡献指南。
许可证
本项目受 Apache 许可证许可。
依赖
~0–15MB
~134K SLoC