我们知道,Node.js 不适合 CPU 密集型计算的场景,通常的解决方法是用 C/C++ 编写 Node.js 的扩展(Addons)。以前只能用 C/C++,现在我们有了新的选择——Rust。
Rust 是 Mozilla 开发的注重安全、性能和并发的现代编程语言。相比较于其他常见的编程语言,它有 3 个独特的概念:
- 所有权
- 借用
- 生命周期
正是这 3 个特性保证了 Rust 是内存安全的,这里不会展开讲解,有兴趣的读者可以去了解一下。
接下来,我们通过三种方式使用 Rust 编写 Node.js 的扩展。
3.5.3 FFI
FFI 的全称是 Foreign Function Interface,即可以用 Node.js 调用动态链接库。
运行以下命令:
$ cargo new ffi-demo && cd ffi-demo
$ npm init -y
$ npm i ffi --save
$ touch index.js
部分文件修改如下:
src/lib.rs
#[no_mangle]
pub extern fn fib(n: i64) -> i64 {
return match n {
1 | 2 => 1,
n => fib(n - 1) + fib(n - 2)
}
}
Cargo.toml
[package]
name = "ffi-demo"
version = "0.1.0"
[lib]
name = "ffi"
crate-type = ["dylib"]
Cargo.toml 是 Rust 项目的配置文件,相当于 Node.js 中的 package.json。这里指定编译生成的类型是 dylib(动态链接库),名字在 *inux 下是 libffi,Windows 下是 ffi。
使用 cargo 编译代码:
$ cargo build #开发环境用
或者
$ cargo build --release #生产环境用,编译器做了更多优化,但编译慢
cargo 是 Rust 的构建工具和包管理工具,负责构建代码、下载依赖库并编译它们。此时会生成一个 target 的目录,该目录下会有 debug(不加 --release)或者 release(加 --release)目录,存放了生成的动态链接库。
index.js
const ffi = require('ffi')
const isWin = /^win/.test(process.platform)
const rust = ffi.Library('target/debug/' + (!isWin ? 'lib' : '') + 'ffi', {
fib: ['int', ['int']]
})
function fib(n) {
if (n === 1 || n === 2) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
// js
console.time('node')
console.log(fib(40))
console.timeEnd('node')
// rust
console.time('rust')
console.log(rust.fib(40))
console.timeEnd('rust')
运行 index.js:
$ node index.js
102334155
node: 1053.743ms
102334155
rust: 1092.570ms
将 index.js 中 debug 改为 release,运行:
$ cargo build --release
$ node index.js
102334155
node: 1050.467ms
102334155
rust: 273.508ms
可以看出:添加了 --release 编译后的代码,执行效率提升十分明显。
3.5.4 Neon
官方介绍:
Rust bindings for writing safe and fast native Node.js modules.
使用方法如下:
$ npm i neon-cli -g
$ neon new neon-demo
$ cd neon-demo
$ tree .
.
├── README.md
├── lib
│ └── index.js
├── native
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── package.json
3 directories, 6 files
$ npm i #触发 neon build
$ node lib/index.js
hello node
接下来我们看看关键的代码文件。
lib/index.js
var addon = require('../native');
console.log(addon.hello());
native/src/lib.rs
#[macro_use]
extern crate neon;
use neon::vm::{Call, JsResult};
use neon::js::JsString;
fn hello(call: Call) -> JsResult<JsString> {
let scope = call.scope;
Ok(JsString::new(scope, "hello node").unwrap())
}
register_module!(m, {
m.export("hello", hello)
});
native/build.rs
extern crate neon_build;
fn main() {
neon_build::setup(); // must be called in build.rs
// add project-specific build logic here...
}
native/Cargo.toml
[package]
name = "neon-demo"
version = "0.1.0"
authors = ["nswbmw"]
license = "MIT"
build = "build.rs"
[lib]
name = "neon_demo"
crate-type = ["dylib"]
[build-dependencies]
neon-build = "0.1.22"
[dependencies]
neon = "0.1.22"
在运行 neon build
时,会根据 native/Cargo.toml 中 build 字段指定的文件(这里是 build.rs)编译,并且生成的类型是 dylib(动态链接库)。native/src/lib.rs 存放了扩展的代码逻辑,通过 register_module 注册了一个 hello 方法,返回 hello node 字符串。
接下来测试原生 Node.js 和 Neon 编写的扩展运行斐波那契数列的执行效率。
修改对应文件如下:
native/src/lib.rs
#[macro_use]
extern crate neon;
use neon::vm::{Call, JsResult};
use neon::mem::Handle;
use neon::js::JsInteger;
fn fib(call: Call) -> JsResult<JsInteger> {
let scope = call.scope;
let index: Handle<JsInteger> = try!(try!(call.arguments.require(scope, 0)).check::<JsInteger>());
let index: i32 = index.value() as i32;
let result: i32 = fibonacci(index);
Ok(JsInteger::new(scope, result))
}
fn fibonacci(n: i32) -> i32 {
match n {
1 | 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
register_module!(m, {
m.export("fib", fib)
});
lib/index.js
const rust = require('../native')
function fib (n) {
if (n === 1 || n === 2) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
// js
console.time('node')
console.log(fib(40))
console.timeEnd('node')
// rust
console.time('rust')
console.log(rust.fib(40))
console.timeEnd('rust')
运行:
$ neon build
$ node lib/index.js
102334155
node: 1030.681ms
102334155
rust: 270.417ms
接下来看一个复杂点的例子,用 Neon 编写一个 User 类,可传入一个含有 first_name 和 last_name 的对象,暴露出一个 get_full_name 方法。
修改对应文件如下:
native/src/lib.rs
#[macro_use]
extern crate neon;
use neon::js::{JsFunction, JsString, Object, JsObject};
use neon::js::class::{Class, JsClass};
use neon::mem::Handle;
use neon::vm::Lock;
pub struct User {
first_name: String,
last_name: String,
}
declare_types! {
pub class JsUser for User {
init(call) {
let scope = call.scope;
let user = try!(try!(call.arguments.require(scope, 0)).check::<JsObject>());
let first_name: Handle<JsString> = try!(try!(user.get(scope, "first_name")).check::<JsString>());
let last_name: Handle<JsString> = try!(try!(user.get(scope, "last_name")).check::<JsString>());
Ok(User {
first_name: first_name.value(),
last_name: last_name.value(),
})
}
method get_full_name(call) {
let scope = call.scope;
let first_name = call.arguments.this(scope).grab(|user| { user.first_name.clone() });
let last_name = call.arguments.this(scope).grab(|user| { user.last_name.clone() });
Ok(try!(JsString::new_or_throw(scope, &(first_name + &last_name))).upcast())
}
}
}
register_module!(m, {
let class: Handle<JsClass<JsUser>> = try!(JsUser::class(m.scope));
let constructor: Handle<JsFunction<JsUser>> = try!(class.constructor(m.scope));
try!(m.exports.set("User", constructor));
Ok(())
});
lib/index.js
const rust = require('../native')
const User = rust.User
const user = new User({
first_name: 'zhang',
last_name: 'san'
})
console.log(user.get_full_name())
运行:
$ neon build
$ node lib/index.js
zhangsan
3.5.5 NAPI
不少 Node.js 开发者可能都遇到过升级 Node.js 版本导致程序运行不起来的情况,需要重新安装依赖解决,比如:node-sass 模块。因为之前编写 Node.js 扩展严重依赖于 V8 暴露的 API,而不同版本的 Node.js 依赖的 V8 版本可能不同,一旦升级 Node.js 版本,原先运行正常的 Node.js 的扩展就可能失效了。
NAPI 是 node@8 新添加的用于原生模块开发的接口,相较于以前的开发方式,NAPI 提供了稳定的 ABI 接口,消除了 Node.js 版本差异、引擎差异等编译后不兼容的问题,解决了编写 Node.js 插件最头疼的问题。
目前 NAPI 还处于试验阶段,所以相关资料并不多,笔者写了一个 demo 放到了 GitHub 上,这里直接 clone 下来运行:
$ git clone https://github.com/nswbmw/rust-napi-demo
主要文件代码如下:
src/lib.rs
#[macro_use]
extern crate napi;
#[macro_use]
extern crate napi_derive;
use napi::{NapiEnv, NapiNumber, NapiResult};
#[derive(NapiArgs)]
struct Args<'a> {
n: NapiNumber<'a>
}
fn fibonacci<'a>(env: &'a NapiEnv, args: &Args<'a>) -> NapiResult<NapiNumber<'a>> {
let number = args.n.to_i32()?;
NapiNumber::from_i32(env, _fibonacci(number))
}
napi_callback!(export_fibonacci, fibonacci);
fn _fibonacci(n: i32) -> i32 {
match n {
1 | 2 => 1,
_ => _fibonacci(n - 1) + _fibonacci(n - 2)
}
}
index.js
const rust = require('./build/Release/example.node')
function fib (n) {
if (n === 1 || n === 2) {
return 1
}
return fib(n - 1) + fib(n - 2)
}
// js
console.time('node')
console.log(fib(40))
console.timeEnd('node')
// rust
console.time('rust')
console.log(rust.fibonacci(40))
console.timeEnd('rust')
运行结果:
$ npm start
102334155
node: 1087.650ms
102334155
rust: 268.395ms
(node:33302) Warning: N-API is an experimental feature and could change at any time.
- https://github.com/neon-bindings/neon
- https://github.com/napi-rs/napi
- https://zhuanlan.zhihu.com/p/27650526
上一节:3.4 Node@8
下一节:3.6 Event Loop