Skip to content

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

rust
#[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

lib.rs
rust
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:

rust
// 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

lib.rs
rust
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(())
}
index.js
js
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:

lib.rs
rust
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:

Cargo.toml
toml
[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:

main.js
js
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_exports for 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.