Skip to content

Entendendo lifetime

A interoperabilidade entre o sistema de lifetime do Rust e o gerenciamento de memória do JavaScript é complicada. Na maioria dos casos, você não pode usar os valores JavaScript passados para a função Rust. No entanto, existem várias APIs no Node-API que podem estender o lifetime dos valores JavaScript. O NAPI-RS usa essas APIs para alinhar o lifetime dos valores JavaScript com o sistema de lifetime do Rust tanto quanto possível.

Em uma chamada de função Node-API, os ponteiros para valores JavaScript só são válidos até o fim da chamada da função; veja Object Lifetime Management.

À medida que chamadas Node-API são feitas, handles para objetos no heap da VM subjacente podem ser retornados como napi_values. Esses handles precisam manter os objetos "vivos" enquanto eles ainda forem necessários ao código nativo; caso contrário, os objetos podem ser coletados antes que o código nativo termine de usá-los.

À medida que handles de objeto são retornados, eles são associados a um "scope". O lifetime do scope padrão está ligado ao lifetime da chamada do método nativo. O resultado é que, por padrão, os handles permanecem válidos e os objetos associados a esses handles serão mantidos vivos durante o lifetime da chamada do método nativo.

Conversões primitivas com ownership

Quando primitivas JavaScript são recebidas como valores Rust com ownership, como bool, um inteiro ou ponto flutuante Rust, ou String, o NAPI-RS copia o valor para dados pertencentes ao Rust. Esses dados Rust não ficam vinculados ao escopo de handles do Node-API. Isso é diferente de receber um wrapper de handle como JsString<'env> ou JsNumber<'env>.

Lifetime de JsValue

Wrappers de handle como JsNumber<'env> e JsString<'env> fazem referência a um napi_value no escopo de handles do ambiente atual. Você pode ler deles um valor Rust com ownership — por exemplo, ler um JsNumber como f64 ou u32 —, mas o wrapper em si continua limitado ao escopo.

lib.rs
rust
use napi::{bindgen_prelude::{Either, Result}, JsNumber};
use napi_derive::napi;

#[napi]
pub fn read_number(a: JsNumber) -> Result<Either<f64, u32>> {
  let input_u32 = a.get_uint32()?;
  let input_f64 = a.get_double()?;
  if input_u32 as f64 == input_f64 {
    Ok(Either::B(input_u32))
  } else {
    Ok(Either::A(input_f64))
  }
}

Os números retornados neste exemplo são valores Rust com ownership. O handle JsNumber não é: seu lifetime impede o uso depois que o escopo da chamada nativa se fecha. A mesma distinção vale para strings: String contém uma cópia, enquanto JsString<'env> é um handle JavaScript limitado ao escopo. Na maioria das assinaturas, o Rust infere esse lifetime para você.

Lifetime de instâncias de classe

Em uma classe #[napi], a instância é criada pelo lado Rust e a posse é enviada para o lado JavaScript:

lib.rs
rust
use std::sync::Arc;

use napi_derive::napi;

#[napi]
pub struct Engine {
  inner: Arc<()>,
}

#[napi]
impl Engine {
  #[napi(constructor)]
  pub fn new() -> Self {
    Self { inner: Arc::new(()) }
  }
}
index.ts
ts
const engine = new Engine()

Nesse caso, a instância Engine é criada no construtor e retornada ao JavaScript.

Diferentemente de JsNumber ou JsString, Engine mantém a struct Rust sob o capô, então, se ela for passada de volta pelo lado JavaScript, você poderá obter &Engine ou &mut Engine diretamente.

Fluxograma de lifetime de instâncias de classe

O fluxograma a seguir ilustra o lifetime de uma instância de struct do NAPI-RS:

mermaid
flowchart
  A[JavaScript chama new Engine]
  B[O construtor Rust cria Engine]
  C[Colocar Engine em Box e anexá-lo com napi_wrap]
  D[Retornar a instância JavaScript de Engine]
  F[Passar de volta para Rust]
  G[napi_unwrap]
  H[Obter &Engine ou &mut Engine]
  I[GC do JavaScript]
  J[napi_finalize_cb]
  K[Excluir a struct Engine]
  A --> B
  B --> C
  C --> D
  D --> F
  F --> G
  G --> H
  D --> I
  I --> J
  J --> K

Lifetime de Buffer e TypedArray

Buffer e os tipos concretos de typed array com ownership (Uint8Array, Int32Array e assim por diante) podem sobreviver a uma chamada nativa. Seus wrappers mantêm o armazenamento subjacente vivo enquanto o Rust os possui. Já BufferSlice<'env>, os tipos slice de typed array e TypedArray<'env> tomam emprestado um handle do escopo do ambiente atual.

O NAPI-RS fornece duas categorias de tipos de buffer com características de lifetime diferentes:

Tipos com ownership - lifetime entre threads

Para um valor originado no JavaScript, a conversão para um Buffer, Uint8Array ou tipo semelhante com ownership cria um napi_ref:

  • A referência mantém o objeto JavaScript e seus dados subjacentes vivos até o wrapper Rust ser descartado
  • O wrapper pode atravessar fronteiras assíncronas e entre threads
  • Descartar o wrapper libera a referência do Rust; o JavaScript ainda pode manter o mesmo objeto de forma independente
lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn print_buffer(buffer: Buffer) {
  // Crie uma cópia pertencente ao Rust enquanto este callback síncrono controla a execução.
  let data = buffer.to_vec();
  std::thread::spawn(move || {
    println!("data: {:?}", data);
  });
}

WARNING

Send e Sync permitem mover o wrapper; eles não sincronizam o acesso aos bytes. O JavaScript pode manter e modificar o mesmo armazenamento subjacente enquanto o Rust segura o wrapper. Ler ou gravar essa memória em uma worker Rust enquanto o JavaScript ou outra thread Rust pode modificá-la constitui uma corrida de dados e pode causar comportamento indefinido. Copie os dados antes de despachar o trabalho ou imponha um protocolo de ownership que exclua todo acesso não sincronizado.

INFO

A limpeza está vinculada ao Drop do wrapper Rust, não ao GC do JavaScript. Com a feature napi4, cada ambiente/isolate do Node-API tem sua própria ThreadsafeFunction de GC customizada e sem referência ao event loop. Um wrapper descartado na thread JavaScript de seu ambiente chama napi_reference_unref e napi_delete_reference diretamente. Se o wrapper for descartado em outro lugar, seu napi_ref será enviado à ThreadsafeFunction capturada do ambiente proprietário do valor, cujo callback o libera na thread JavaScript desse ambiente. Se esse ambiente já tiver sido encerrado, o NAPI-RS detecta o handle abortado e não faz outra chamada Node-API, pois o runtime já invalidou a referência.

Liberar a referência do Rust só torna o valor elegível para GC se o JavaScript não mantiver outras referências. Para buffers criados no Rust, o Rust possui a alocação até sua exportação; depois disso, o finalizer do JavaScript possui essa alocação (ou o NAPI-RS a copia quando o runtime rejeita buffers externos).

Tipos emprestados - lifetime no escopo da função

Tipos emprestados (BufferSlice<'env>, Uint8ArraySlice<'env> etc.) têm lifetimes vinculados ao escopo da função:

  • Acesso sem cópia aos dados subjacentes
  • Não podem atravessar fronteiras assíncronas devido às restrições de lifetime
  • Precisam ser usados dentro da mesma chamada de função em que foram criados
lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn process_buffer_slice<'env>(env: &'env Env, data: &'env [u8]) -> Result<BufferSlice<'env>> {
  // O lifetime de BufferSlice está vinculado a este escopo de função
  BufferSlice::from_data(env, data.to_vec())
}

Fluxograma de lifetime de buffer

mermaid
flowchart TD
    A[JavaScript chama Rust com um Buffer]
    B{Tipo do parâmetro Rust}
    C[BufferSlice&lt;'env&gt; ou outra view de escopo]
    D[Usar apenas enquanto o escopo da chamada nativa está aberto]
    E[A chamada nativa retorna]
    F[O handle Rust de escopo expira; o lifetime JavaScript é independente]
    G[Buffer ou typed array com ownership]
    H[napi_create_reference em FromNapiValue]
    I[O wrapper pode atravessar await ou fronteiras de thread]
    J[Drop do wrapper Rust]
    K{Descartado na thread de seu ambiente proprietário?}
    L[Unref e excluir o napi_ref diretamente]
    M[Enfileirar o napi_ref na TSFN de GC do ambiente proprietário]
    N[A thread JavaScript proprietária faz unref e exclui a referência]
    O[A referência do Rust foi liberada]
    P{O JavaScript mantém outra referência?}
    Q[O valor JavaScript continua vivo]
    R[O valor fica elegível para o GC do JavaScript]

    A --> B
    B -->|Emprestado| C
    C --> D
    D --> E
    E --> F
    B -->|Com ownership| G
    G --> H
    H --> I
    I --> J
    J --> K
    K -->|Sim| L
    K -->|Não| M
    M --> N
    L --> O
    N --> O
    O --> P
    P -->|Sim| Q
    P -->|Não| R

Quando lifetimes importam

Lifetime no escopo da função (BufferSlice<'env>):

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn sync_only(env: &Env) -> Result<BufferSlice<'_>> {
  // ✅ Funciona: o lifetime de BufferSlice está ligado ao escopo da função
  BufferSlice::from_data(env, vec![1, 2, 3])
}

// ❌ Não compila: não pode atravessar fronteiras assíncronas
// #[napi]
// async fn async_fail(env: &Env) -> Result<BufferSlice<'_>> {
//     let slice = BufferSlice::from_data(env, vec![1, 2, 3])?;
//     napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
//     Ok(slice) // Erro: slice não vive tempo suficiente
// }

Os exemplos com sleep abaixo exigem as features async e tokio_time na dependência napi.

Lifetime apoiado por referência (Buffer):

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub async fn async_works(buffer: Buffer) -> Result<Buffer> {
  // ✅ Funciona: Buffer é Send + Sync
  napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
  Ok(buffer)
}

Para mais detalhes sobre padrões de uso de Buffer e TypedArray, veja a documentação de TypedArray.

Referência a valores JavaScript

Para outros valores, wrappers de referência como ObjectRef, UnknownRef, SymbolRef, FunctionRef e ExternalRef usam um napi_ref para manter um valor JavaScript vivo além do callback atual. O wrapper em si não tem lifetime de escopo, mas isso não torna as APIs JavaScript independentes do ambiente nem seguras para chamar de qualquer thread. Recupere o valor com escopo usando o Env proprietário e siga o contrato de liberação do tipo: alguns wrappers liberam no Drop, enquanto ObjectRef, UnknownRef e SymbolRef exigem um unref(env) explícito (ou precisam ser retornados ao JavaScript).

Veja Reference para mais detalhes.