我们知道,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 调用动态链接库。

运行以下命令:

$ 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 <[email protected]>"]
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.

3.5.6 参考链接

上一节:3.4 Node@8

下一节:3.6 Event Loop

results matching ""

    No results matching ""