模块初始化
NAPI-RS 提供两个模块初始化 API:#[napi_derive::module_init] 和 #[napi(module_exports)]。它们看起来相似,但用途不同,执行时机也不同。
执行时间线
要正确使用这两个 API,关键是理解各自何时执行:
┌─────────────────────────────────────────────────────────────────┐
│ Node.js loads .node file │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. #[napi_derive::module_init] runs │
│ (via ctor - runs at dynamic library load time) │
│ Runs once for this native library load │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. napi_register_module_v1 called by Node.js │
│ - Registers all #[napi] exports (functions, classes, etc.) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. #[napi(module_exports)] runs │
│ Receives the exports object, can customize it │
│ Runs ONCE per Node.js thread/context │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Module is ready for use in JavaScript │
└─────────────────────────────────────────────────────────────────┘
#[napi_derive::module_init]
该宏标记一个函数,使其在原生模块加载时运行。它内部使用
ctor crate,在 Node-API 注册 JavaScript 导出之前执行。
时机
- 在动态库加载时运行(早于
napi_register_module_v1) - 每次原生动态库加载执行一次,而不是每个 Node-API 环境执行一次; 同一进程中的 worker 通常共享已加载的动态库
- 无法访问 Node.js 环境或 exports 对象
签名
#[napi_derive::module_init]
fn init() {
// initialization code
}
该函数不能有参数或返回值。
使用场景
以下情况适合使用 #[napi_derive::module_init]:
- 设置异步运行时(例如 tokio)
- 初始化全局状态,供所有线程共享
- 在注册任何导出之前必须完成的一次性设置
- 配置日志或 tracing
示例:自定义 Tokio Runtime
use napi::bindgen_prelude::create_custom_tokio_runtime;
#[napi_derive::module_init]
fn init() {
let runtime = napi::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("my-native-module")
.build();
match runtime {
Ok(rt) => create_custom_tokio_runtime(rt),
Err(err) => eprintln!("failed to create custom Tokio runtime: {err}"),
}
}
WARNING
上例配置的多线程 Tokio 运行时依赖具体目标。请对该运行时设置进行条件编译,或选择
WebAssembly 目标支持的运行时配置。module_init 宏本身支持 WebAssembly 构建。
#[napi(module_exports)]
该宏标记一个接收模块 exports 对象的函数,使你能在模块返回给 JavaScript 前自定义该对象。
时机
- 在注册所有
#[napi]导出之后运行 - 在
napi_register_module_v1(Node.js 模块注册)期间运行 - 每个 Node.js 上下文执行一次(主线程和每个 worker thread)
签名
函数可返回 () 或 Result<()>。如果存在参数,每个参数必须是
Env、Object 或对它们的引用。Object 接收模块的 exports 对象,
Env 接收当前 Node-API 环境。常见签名如下:
// With just the exports object
#[napi(module_exports)]
pub fn init(mut exports: Object) -> Result<()> {
// customize exports
Ok(())
}
// With exports and Env
#[napi(module_exports)]
pub fn init(mut exports: Object, env: Env) -> Result<()> {
// customize exports with access to Env
Ok(())
}
使用场景
以下情况适合使用 #[napi(module_exports)]:
- 向 exports 对象添加自定义属性
- 创建需要导出的 symbol
- 以程序化方式注册导出(不使用
#[napi]) - 需要 Node.js 环境的每线程初始化
示例:添加 Symbol
use napi::bindgen_prelude::*;
#[napi(module_exports)]
pub fn init(mut exports: Object) -> Result<()> {
// Add a unique symbol to exports
let symbol = Symbol::new("MY_MODULE_SYMBOL");
exports.set_named_property("MY_SYMBOL", symbol)?;
// Add a version string
exports.set_named_property("VERSION", "1.0.0")?;
Ok(())
}
const native = require('./index.node')
console.log(native.MY_SYMBOL) // Symbol(MY_MODULE_SYMBOL)
console.log(native.VERSION) // "1.0.0"
主要区别
| 方面 | #[napi_derive::module_init] |
#[napi(module_exports)] |
|---|---|---|
| 执行时间 | 加载 .node 文件时 |
模块注册期间 |
| 执行频率 | 每次原生库/模块加载 | 每个 Node-API 环境/上下文 |
| 接收 exports | 否 | 是 |
| 可以修改 exports | 否 | 是 |
| 访问 Env | 否 | 是(可选) |
| WebAssembly 支持 | 是(通过我们的 JS 绑定) | 是 |
配合使用
这两个 API 彼此补充,可以同时使用:
use napi::bindgen_prelude::*;
// Runs once at module load - setup tokio runtime
#[cfg(not(target_family = "wasm"))]
#[napi_derive::module_init]
fn setup_runtime() {
let runtime = napi::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build();
match runtime {
Ok(rt) => create_custom_tokio_runtime(rt),
Err(err) => eprintln!("failed to create custom Tokio runtime: {err}"),
}
}
// Runs per thread - customize exports
#[napi(module_exports)]
pub fn customize_exports(mut exports: Object) -> Result<()> {
exports.set_named_property("THREAD_SAFE_SYMBOL", Symbol::new("THREAD_SAFE"))?;
Ok(())
}
// Regular export via #[napi]
#[napi]
pub async fn do_async_work() -> String {
// This uses the tokio runtime set up in module_init
napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"done".to_string()
}
自定义 runtime 示例需要启用 napi 的 async(或 tokio_rt)feature。
sleep 示例还需要 tokio_time:
[dependencies]
napi = { version = "3", features = ["async", "tokio_time"] }
napi-derive = "3"
Worker Thread 行为
在 Node.js 中使用 worker thread 时,两个 API 的行为不同:
const { Worker } = require('worker_threads')
// Main thread loads module
const native = require('./index.node')
// -> module_init runs (first time)
// -> module_exports runs (main thread)
// Worker thread loads same module
new Worker(
`
const native = require('./index.node')
// -> module_init does NOT run again (already ran)
// -> module_exports DOES run again (new thread context)
`,
{ eval: true },
)
这意味着:
- 全局资源(例如 tokio runtime)只初始化一次并共享
- 可以在
module_exports中为每个上下文设置线程状态
INFO
#[napi_derive::module_init] 函数通过 ctor crate 运行;该 crate 使用平台特定机制(Unix 上的 .init_array、Windows 上的特殊构造函数)在动态库加载时执行。