我们知道,Node.js 不适合 CPU 密集型计算的场景,通常的解决方法是用 C/C++ 编写 Node.js 的扩展(Addons)。以前只能用 C/C++,现在我们有了新的选择——Rust。
Rust 是 Mozilla 开发的注重安全、性能和并发的现代编程语言。相比较于其他常见的编程语言,它有 3 个独特的概念:
正是这 3 个特性保证了 Rust 是内存安全的,这里不会展开讲解,有兴趣的读者可以去了解一下。
接下来,我们通过三种方式使用 Rust 编写 Node.js 的扩展。
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 编译后的代码,执行效率提升十分明显。
官方介绍:
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 字符串。
neon build
接下来测试原生 Node.js 和 Neon 编写的扩展运行斐波那契数列的执行效率。
修改对应文件如下:
#[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) });
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 方法。
#[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(()) });
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
不少 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
主要文件代码如下:
#[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) } }
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.
上一节:3.4 Node@8
下一节:3.6 Event Loop
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8