我们知道,Node.js 不适合 CPU 密集型计算的场景,通常的解决方法是用 C/C++ 编写 Node.js 的扩展(Addons)。以前只能用 C/C++,现在我们有了新的选择——Rust。

3.5.1 环境

3.5.2 Rust

Rust 是 Mozilla 开发的注重安全、性能和并发的现代编程语言。相比较于其他常见的编程语言,它有 3 个独特的概念:

  1. 所有权
  2. 借用
  3. 生命周期

正是这 3 个特性保证了 Rust 是内存安全的,这里不会展开讲解,有兴趣的读者可以去了解一下。

接下来,我们通过三种方式使用 Rust 编写 Node.js 的扩展。

3.5.3 FFI

FFI 的全称是 Foreign Function Interface,即可以用 Node.js 调用动态链接库。

运行以下命令:

  1. $ cargo new ffi-demo && cd ffi-demo
  2. $ npm init -y
  3. $ npm i ffi --save
  4. $ touch index.js

部分文件修改如下:

src/lib.rs

  1. #[no_mangle]
  2. pub extern fn fib(n: i64) -> i64 {
  3. return match n {
  4. 1 | 2 => 1,
  5. n => fib(n - 1) + fib(n - 2)
  6. }
  7. }

Cargo.toml

  1. [package]
  2. name = "ffi-demo"
  3. version = "0.1.0"
  4. [lib]
  5. name = "ffi"
  6. crate-type = ["dylib"]

Cargo.toml 是 Rust 项目的配置文件,相当于 Node.js 中的 package.json。这里指定编译生成的类型是 dylib(动态链接库),名字在 *inux 下是 libffi,Windows 下是 ffi。

使用 cargo 编译代码:

  1. $ cargo build #开发环境用
  2. 或者
  3. $ cargo build --release #生产环境用,编译器做了更多优化,但编译慢

cargo 是 Rust 的构建工具和包管理工具,负责构建代码、下载依赖库并编译它们。此时会生成一个 target 的目录,该目录下会有 debug(不加 —release)或者 release(加 —release)目录,存放了生成的动态链接库。

index.js

  1. const ffi = require('ffi')
  2. const isWin = /^win/.test(process.platform)
  3. const rust = ffi.Library('target/debug/' + (!isWin ? 'lib' : '') + 'ffi', {
  4. fib: ['int', ['int']]
  5. })
  6. function fib(n) {
  7. if (n === 1 || n === 2) {
  8. return 1
  9. }
  10. return fib(n - 1) + fib(n - 2)
  11. }
  12. // js
  13. console.time('node')
  14. console.log(fib(40))
  15. console.timeEnd('node')
  16. // rust
  17. console.time('rust')
  18. console.log(rust.fib(40))
  19. console.timeEnd('rust')

运行 index.js:

  1. $ node index.js
  2. 102334155
  3. node: 1053.743ms
  4. 102334155
  5. rust: 1092.570ms

将 index.js 中 debug 改为 release,运行:

  1. $ cargo build --release
  2. $ node index.js
  3. 102334155
  4. node: 1050.467ms
  5. 102334155
  6. rust: 273.508ms

可以看出:添加了 —release 编译后的代码,执行效率提升十分明显。

3.5.4 Neon

官方介绍:

Rust bindings for writing safe and fast native Node.js modules.

使用方法如下:

  1. $ npm i neon-cli -g
  2. $ neon new neon-demo
  3. $ cd neon-demo
  4. $ tree .
  5. .
  6. ├── README.md
  7. ├── lib
  8. └── index.js
  9. ├── native
  10. ├── Cargo.toml
  11. ├── build.rs
  12. └── src
  13. └── lib.rs
  14. └── package.json
  15. 3 directories, 6 files
  16. $ npm i #触发 neon build
  17. $ node lib/index.js
  18. hello node

接下来我们看看关键的代码文件。

lib/index.js

  1. var addon = require('../native');
  2. console.log(addon.hello());

native/src/lib.rs

  1. #[macro_use]
  2. extern crate neon;
  3. use neon::vm::{Call, JsResult};
  4. use neon::js::JsString;
  5. fn hello(call: Call) -> JsResult<JsString> {
  6. let scope = call.scope;
  7. Ok(JsString::new(scope, "hello node").unwrap())
  8. }
  9. register_module!(m, {
  10. m.export("hello", hello)
  11. });

native/build.rs

  1. extern crate neon_build;
  2. fn main() {
  3. neon_build::setup(); // must be called in build.rs
  4. // add project-specific build logic here...
  5. }

native/Cargo.toml

  1. [package]
  2. name = "neon-demo"
  3. version = "0.1.0"
  4. authors = ["nswbmw <gxqzk@126.com>"]
  5. license = "MIT"
  6. build = "build.rs"
  7. [lib]
  8. name = "neon_demo"
  9. crate-type = ["dylib"]
  10. [build-dependencies]
  11. neon-build = "0.1.22"
  12. [dependencies]
  13. 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

  1. #[macro_use]
  2. extern crate neon;
  3. use neon::vm::{Call, JsResult};
  4. use neon::mem::Handle;
  5. use neon::js::JsInteger;
  6. fn fib(call: Call) -> JsResult<JsInteger> {
  7. let scope = call.scope;
  8. let index: Handle<JsInteger> = try!(try!(call.arguments.require(scope, 0)).check::<JsInteger>());
  9. let index: i32 = index.value() as i32;
  10. let result: i32 = fibonacci(index);
  11. Ok(JsInteger::new(scope, result))
  12. }
  13. fn fibonacci(n: i32) -> i32 {
  14. match n {
  15. 1 | 2 => 1,
  16. _ => fibonacci(n - 1) + fibonacci(n - 2)
  17. }
  18. }
  19. register_module!(m, {
  20. m.export("fib", fib)
  21. });

lib/index.js

  1. const rust = require('../native')
  2. function fib (n) {
  3. if (n === 1 || n === 2) {
  4. return 1
  5. }
  6. return fib(n - 1) + fib(n - 2)
  7. }
  8. // js
  9. console.time('node')
  10. console.log(fib(40))
  11. console.timeEnd('node')
  12. // rust
  13. console.time('rust')
  14. console.log(rust.fib(40))
  15. console.timeEnd('rust')

运行:

  1. $ neon build
  2. $ node lib/index.js
  3. 102334155
  4. node: 1030.681ms
  5. 102334155
  6. rust: 270.417ms

接下来看一个复杂点的例子,用 Neon 编写一个 User 类,可传入一个含有 first_name 和 last_name 的对象,暴露出一个 get_full_name 方法。

修改对应文件如下:

native/src/lib.rs

  1. #[macro_use]
  2. extern crate neon;
  3. use neon::js::{JsFunction, JsString, Object, JsObject};
  4. use neon::js::class::{Class, JsClass};
  5. use neon::mem::Handle;
  6. use neon::vm::Lock;
  7. pub struct User {
  8. first_name: String,
  9. last_name: String,
  10. }
  11. declare_types! {
  12. pub class JsUser for User {
  13. init(call) {
  14. let scope = call.scope;
  15. let user = try!(try!(call.arguments.require(scope, 0)).check::<JsObject>());
  16. let first_name: Handle<JsString> = try!(try!(user.get(scope, "first_name")).check::<JsString>());
  17. let last_name: Handle<JsString> = try!(try!(user.get(scope, "last_name")).check::<JsString>());
  18. Ok(User {
  19. first_name: first_name.value(),
  20. last_name: last_name.value(),
  21. })
  22. }
  23. method get_full_name(call) {
  24. let scope = call.scope;
  25. let first_name = call.arguments.this(scope).grab(|user| { user.first_name.clone() });
  26. let last_name = call.arguments.this(scope).grab(|user| { user.last_name.clone() });
  27. Ok(try!(JsString::new_or_throw(scope, &(first_name + &last_name))).upcast())
  28. }
  29. }
  30. }
  31. register_module!(m, {
  32. let class: Handle<JsClass<JsUser>> = try!(JsUser::class(m.scope));
  33. let constructor: Handle<JsFunction<JsUser>> = try!(class.constructor(m.scope));
  34. try!(m.exports.set("User", constructor));
  35. Ok(())
  36. });

lib/index.js

  1. const rust = require('../native')
  2. const User = rust.User
  3. const user = new User({
  4. first_name: 'zhang',
  5. last_name: 'san'
  6. })
  7. console.log(user.get_full_name())

运行:

  1. $ neon build
  2. $ node lib/index.js
  3. zhangsan

3.5.5 NAPI

NAPI 是 node@8 新添加的用于原生模块开发的接口,相较于以前的开发方式,NAPI 提供了稳定的 ABI 接口,消除了 Node.js 版本差异、引擎差异等编译后不兼容的问题。

目前 NAPI 还处于试验阶段,所以相关资料并不多,笔者写了一个 demo 放到了 GitHub 上,这里直接 clone 下来运行:

  1. $ git clone https://github.com/nswbmw/rust-napi-demo

主要文件代码如下:

src/lib.rs

  1. #[macro_use]
  2. extern crate napi;
  3. #[macro_use]
  4. extern crate napi_derive;
  5. use napi::{NapiEnv, NapiNumber, NapiResult};
  6. #[derive(NapiArgs)]
  7. struct Args<'a> {
  8. n: NapiNumber<'a>
  9. }
  10. fn fibonacci<'a>(env: &'a NapiEnv, args: &Args<'a>) -> NapiResult<NapiNumber<'a>> {
  11. let number = args.n.to_i32()?;
  12. NapiNumber::from_i32(env, _fibonacci(number))
  13. }
  14. napi_callback!(export_fibonacci, fibonacci);
  15. fn _fibonacci(n: i32) -> i32 {
  16. match n {
  17. 1 | 2 => 1,
  18. _ => _fibonacci(n - 1) + _fibonacci(n - 2)
  19. }
  20. }

index.js

  1. const rust = require('./build/Release/example.node')
  2. function fib (n) {
  3. if (n === 1 || n === 2) {
  4. return 1
  5. }
  6. return fib(n - 1) + fib(n - 2)
  7. }
  8. // js
  9. console.time('node')
  10. console.log(fib(40))
  11. console.timeEnd('node')
  12. // rust
  13. console.time('rust')
  14. console.log(rust.fibonacci(40))
  15. console.timeEnd('rust')

运行结果:

  1. $ npm start
  2. 102334155
  3. node: 1087.650ms
  4. 102334155
  5. rust: 268.395ms
  6. (node:33302) Warning: N-API is an experimental feature and could change at any time.

3.5.6 参考链接

上一节:3.4 Node@8

下一节:3.6 Event Loop