Module Initialization
NAPI-RS provides two APIs for module initialization: #[napi_derive::module_init] and #[napi(module_exports)]. While they may seem similar, they serve different purposes and execute at different times.
Execution Timeline
Understanding when each API executes is crucial for using them correctly:
┌─────────────────────────────────────────────────────────────────┐
│ 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]
This macro marks a function to run when the native module is loaded. It uses
the ctor crate internally to execute before
Node-API registers the JavaScript exports.
Timing
- Runs at dynamic library load time (before
napi_register_module_v1) - Executes once for a native library load, not once per Node-API environment; workers in the same process normally share that loaded library
- No access to Node.js environment or exports object
Signature
#[napi_derive::module_init]
fn init() {
// initialization code
}
The function must have no parameters and no return value.
When to Use
Use #[napi_derive::module_init] for:
- Setting up async runtimes (e.g., tokio)
- Initializing global state that should be shared across all threads
- One-time setup that must happen before any exports are registered
- Configuring logging or tracing
Example: Custom 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
The multi-thread Tokio runtime configured in the example above is
target-dependent. Gate that runtime setup or choose a runtime configuration
supported by your WebAssembly target. The module_init macro itself supports
WebAssembly builds.
#[napi(module_exports)]
This macro marks a function that receives the module's exports object, allowing you to customize it before the module is returned to JavaScript.
Timing
- Runs after all
#[napi]exports are registered - Runs during
napi_register_module_v1(Node.js module registration) - Executes once per Node.js context (main thread + each worker thread)
Signature
The function can return () or Result<()>. Each parameter, if present, must
be Env, Object, or a reference to one of those types. Object receives the
module's exports object, while Env receives the current Node-API environment.
The usual signatures are:
// 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(())
}
When to Use
Use #[napi(module_exports)] for:
- Adding custom properties to the exports object
- Creating symbols that should be exported
- Registering exports programmatically (not via
#[napi]) - Per-thread initialization that needs the Node.js environment
Example: Adding a 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"
Key Differences
| Aspect | #[napi_derive::module_init] |
#[napi(module_exports)] |
|---|---|---|
| Execution time | At .node file load |
During module registration |
| Runs per | Native library/module load | Node-API environment/context |
| Receives exports | No | Yes |
| Can modify exports | No | Yes |
| Access to Env | No | Yes (Optional) |
| WebAssembly support | Yes (Via our js binding) | Yes |
Using Both Together
These APIs are complementary and can be used together:
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()
}
The custom-runtime examples require napi's async (or tokio_rt) feature.
The sleep example additionally requires tokio_time:
[dependencies]
napi = { version = "3", features = ["async", "tokio_time"] }
napi-derive = "3"
Worker Thread Behavior
When using worker threads in Node.js, the behavior differs between the two APIs:
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 },
)
This means:
- Global resources (like tokio runtime) are initialized once and shared
- Per-thread state can be set up in
module_exportsfor each context
INFO
The #[napi_derive::module_init] function runs via the ctor crate, which uses platform-specific mechanisms (.init_array on Unix, special constructor functions on Windows) to execute at dynamic library load time.