Inicialização do módulo
O NAPI-RS fornece duas APIs para inicialização de módulo:
#[napi_derive::module_init] e #[napi(module_exports)]. Embora elas possam
parecer semelhantes, servem a propósitos diferentes e executam em momentos
diferentes.
Linha do tempo de execução
Entender quando cada API executa é fundamental para usá-las corretamente:
┌─────────────────────────────────────────────────────────────────┐
│ Node.js carrega o arquivo .node │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. #[napi_derive::module_init] executa │
│ (via ctor - executa no carregamento da biblioteca) │
│ Executa uma vez para este carregamento da biblioteca │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. napi_register_module_v1 é chamado pelo Node.js │
│ - Registra todos os exports #[napi] (funções, classes etc.)│
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. #[napi(module_exports)] executa │
│ Recebe o objeto exports e pode personalizá-lo │
│ Executa UMA VEZ por thread/contexto do Node.js │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. O módulo está pronto para uso em JavaScript │
└─────────────────────────────────────────────────────────────────┘
#[napi_derive::module_init]
Esse macro marca uma função para executar quando o módulo nativo é carregado.
Internamente, ele usa o crate ctor para
executar antes que o Node-API registre os exports JavaScript.
Momento
- Executa em tempo de carregamento da biblioteca dinâmica (antes de
napi_register_module_v1) - Executa uma vez por carregamento da biblioteca nativa, não uma vez por ambiente Node-API; workers no mesmo processo normalmente compartilham a biblioteca já carregada
- Não tem acesso ao ambiente do Node.js nem ao objeto exports
Assinatura
#[napi_derive::module_init]
fn init() {
// código de inicialização
}
A função não pode ter parâmetros nem valor de retorno.
Quando usar
Use #[napi_derive::module_init] para:
- Configurar runtimes assíncronos (por exemplo, tokio)
- Inicializar estado global que deve ser compartilhado entre todas as threads
- Configuração única que precisa acontecer antes que qualquer export seja registrado
- Configurar logging ou tracing
Exemplo: runtime Tokio personalizado
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
O runtime Tokio multithread configurado no exemplo acima depende do target.
Coloque apenas essa configuração de runtime sob compilação condicional ou
escolha uma configuração compatível com seu target WebAssembly. O macro
module_init em si oferece suporte a builds WebAssembly.
#[napi(module_exports)]
Esse macro marca uma função que recebe o objeto exports do módulo, permitindo
que você o personalize antes que o módulo seja retornado ao JavaScript.
Momento
- Executa depois que todos os exports
#[napi]são registrados - Executa durante
napi_register_module_v1(registro de módulo do Node.js) - Executa uma vez por contexto do Node.js (thread principal + cada worker thread)
Assinatura
A função pode retornar () ou Result<()>. Cada parâmetro, se houver, deve
ser Env, Object ou uma referência a um desses tipos. Object recebe o
objeto exports do módulo, enquanto Env recebe o ambiente Node-API atual. As
assinaturas usuais são:
// Apenas com o objeto exports
#[napi(module_exports)]
pub fn init(mut exports: Object) -> Result<()> {
// personaliza exports
Ok(())
}
// Com exports e Env
#[napi(module_exports)]
pub fn init(mut exports: Object, env: Env) -> Result<()> {
// personaliza exports com acesso ao Env
Ok(())
}
Quando usar
Use #[napi(module_exports)] para:
- Adicionar propriedades personalizadas ao objeto exports
- Criar symbols que devem ser exportados
- Registrar exports programaticamente (não via
#[napi]) - Inicialização por thread que precisa do ambiente do Node.js
Exemplo: adicionando um symbol
use napi::bindgen_prelude::*;
#[napi(module_exports)]
pub fn init(mut exports: Object) -> Result<()> {
// Adiciona um symbol único a exports
let symbol = Symbol::new("MY_MODULE_SYMBOL");
exports.set_named_property("MY_SYMBOL", symbol)?;
// Adiciona uma string de versão
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"
Principais diferenças
| Aspecto | #[napi_derive::module_init] |
#[napi(module_exports)] |
|---|---|---|
| Momento de execução | No carregamento do arquivo .node |
Durante o registro do módulo |
| Executa por | Carregamento da biblioteca/módulo nativo | Ambiente/contexto Node-API |
| Recebe exports | Não | Sim |
| Pode modificar exports | Não | Sim |
| Acesso a Env | Não | Sim (opcional) |
| Suporte a WebAssembly | Sim (via nosso binding JS) | Sim |
Usando as duas juntas
Essas APIs são complementares e podem ser usadas juntas:
use napi::bindgen_prelude::*;
// Executa uma vez no carregamento do módulo - configura o runtime tokio
#[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}"),
}
}
// Executa por thread - personaliza exports
#[napi(module_exports)]
pub fn customize_exports(mut exports: Object) -> Result<()> {
exports.set_named_property("THREAD_SAFE_SYMBOL", Symbol::new("THREAD_SAFE"))?;
Ok(())
}
// Export normal via #[napi]
#[napi]
pub async fn do_async_work() -> String {
// Isso usa o runtime tokio configurado em module_init
napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"done".to_string()
}
Os exemplos de runtime personalizado exigem o feature async (ou tokio_rt)
de napi. O exemplo com sleep também exige tokio_time:
[dependencies]
napi = { version = "3", features = ["async", "tokio_time"] }
napi-derive = "3"
Comportamento em worker threads
Ao usar worker threads no Node.js, o comportamento difere entre as duas APIs:
const { Worker } = require('worker_threads')
// A thread principal carrega o módulo
const native = require('./index.node')
// -> module_init executa (primeira vez)
// -> module_exports executa (thread principal)
// Uma worker thread carrega o mesmo módulo
new Worker(
`
const native = require('./index.node')
// -> module_init NÃO executa de novo (já executou)
// -> module_exports EXECUTA de novo (novo contexto de thread)
`,
{ eval: true },
)
Isso significa:
- Recursos globais (como o runtime tokio) são inicializados uma vez e compartilhados
- Estado por thread pode ser configurado em
module_exportspara cada contexto
INFO
O #[napi_derive::module_init] executa via o crate ctor, que usa mecanismos
específicos de cada plataforma (.init_array no Unix, funções construtoras
especiais no Windows) para executar em tempo de carregamento da biblioteca
dinâmica.