Rust 第二课 - napi

225 阅读3分钟

名词解释

  • NAPI Node-API
  • NAPI-RS is a framework for building pre-compiled Node.js addons in Rust. 也就是说以前要用 C++ 写现在可以通过这个框架用 Rust 写!

目标

Rust 编写代码

fn fib(n: u32) {
    match n {
        1 | 2 => 1,
        _ => fib(n - 1) + fib(n - 2)
    }
}

Node.js 调用(当然 esm 和 commonjs 都需要支持)

import { fib } from './index.js'

console.log(fib(3)) // 2
console.log(fib(4)) // 3
console.log(fib(5)) // 5
console.log(fib(6)) // 8
console.log(fib(7)) // 13
console.log(fib(8)) // 21
console.log(fib(9)) // 34
console.log(fib(10)) // 55

然后对比二者的性能差异

import { fib } from './index.js'

console.time('napi-rs')
...
console.timeEnd('napi-rs')

console.time('node')
...
console.timeEnd('node')

此次需要安装 crate,故需要按照第一课配置好 crate registry 以便能快速安装包,配置后尝试在 hello_world 中安装 rand,验证下速度。

第一次会下载整个 index,故比较慢,后续 cargo add 会很快。我们试试安装一个随机数的 crate 看看我们的代理是否配置成功。

安装:

❯ cargo add rand
    Updating `rsproxy` index
       Fetch [===>                     ]  19.97%, 3.07MiB/s

main.rs 使用:

use rand::Rng;

fn main() {
    println!("Hello, world! 你好,世界!");

    let num = rand::thread_rng().gen_range(1..=100);

    println!("{num}")
}

运行:cargo run

Hello, world! 你好,世界!
17

成功了!我们第一次使用了标准库之外的外部库,这是一个小里程碑事件!记下来我们回到本文的目标。

napi-rs

按照 napi.rs/docs/introd… 文档操作会遇到

aarch64-apple-darwin vs x86_64-apple-darwin 如何选择?

我们通过下面表格知道 aarch64 x86_64 到底是什么。

CPU ArchitectureDescription
x86_64/x86/amd64Same name for 64-bit AMD/Intel CPUs
AArch64/arm64/ARMv8/ARMv9Same name for 64-bit ARM CPUs
i38632-bit AMD/Intel CPUs
AArch32/arm/ARMv1 to ARMv7Same name for 32-bit ARM CPUs
rv64gc/rv64gSame name for 64-bit RISC-V CPUs
ppc64le64-bit PowerPC CPUs with little-endian memory ordering
表格来自 - https://www.linuxconsultant.org/arm-vs-aarch64-vs-amd64-vs-x86_64-vs-x86-whats-the-difference/ - https://itsfoss.com/arm-aarch64-x86_64/

通过命令行 $ arch 输出 arm64,知道我们本地的架构(arch),而且我们是苹果电脑(app-darwin),故选择 aarch64-apple-darwin

继续执行会报错,因为我们并未安装 yarn,可以忽略,因为我们自己执行 npm install 也可以安装依赖。

/bin/sh: yarn: command not found

  1. npm install
  2. npm run build

此时多了三个文件:

  • xx.darwin-x64.node is the Node.js addon binary file。js 就是通过 require 该文件获得我们的目标函数。
  • index.js is the generated JavaScript binding file which helps you export all the stuffs in the addon to the package caller. 这是入口文件
  • And the index.d.ts is the generated TypeScript definition file. TS 类型文件,为导出的目标函数提供类型注解。

接下里测试下构建是否成功。

  1. npm test

__test__/index.spec.mjs

import test from 'ava'

import { sum } from '../index.js'
console.log(sum.toString()) 

test('sum from native', (t) => {
  t.is(sum(1, 2), 3)
})

单测成功说明我们已经能通过 js 调用 Rust 函数了。Another milestone event!

为了满足我们的好奇心,我们打印下 sum,可以发现 function sum() { [native code] } 说明确实变成了 node.js addon。

接下来是重头戏:

实现 Fibonacci 函数

TDD

单测先行

import test from 'ava'

import { fib } from '../index.js'

test('fib from native', (t) => {
  t.is(fib(1), 1)
  t.is(fib(2), 1)
  t.is(fib(3), 2)
  t.is(fib(4), 3)
  t.is(fib(5), 5)
  t.is(fib(6), 8)
  t.is(fib(7), 13)
  t.is(fib(8), 21)
  t.is(fib(9), 34)
  t.is(fib(10), 55)
})

src/lib.rs

#![deny(clippy::all)]

#[macro_use]
extern crate napi_derive;

#[napi]
pub fn fib(n: u32) -> u32 {
  match n {
      1 | 2 => 1,
      _ => fib(n - 1) + fib(n - 2)
  }
}

性能对比

test/perf.js

const { fib } = require('..')
const { jsFib } = require('./js-fib')
const process = require('node:process')

const n = Number(process.argv.at(-1)) || 1

console.time('fib from native when n = ' + n)
fib(n)
console.timeEnd('fib from native when n = ' + n)

console.time('fib from javascript when n = ' + n)
jsFib(n)
console.timeEnd('fib from javascript when n = ' + n)

结果

rust 的斐波那契函数性能是 js 的 3 倍,当超过 42 性能差异更加明显

❯ node __test__/perf.js 42

fib from native when n = 42: 501.801ms
fib from javascript when n = 42: 1.494s

❯ node __test__/perf.js 44


fib from native when n = 44: 1.314s
fib from javascript when n = 44: 3.849s

总结

本文成功通过 napi-rs 生成了供 js 调用的 Rust 函数,性能有了三倍的提升,过程还是比较顺利的 😄。有了这个框架,我们在日常工作中如果 node.js 遇到性能问题多了一个称手的工具。

更多阅读