Skip to main content

AsyncRuntime

Trait AsyncRuntime 

Source
pub unsafe trait AsyncRuntime:
    Send
    + Sync
    + 'static {
    // Required methods
    fn spawn(
        &self,
        task: AsyncRuntimeTask,
    ) -> Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>>;
    fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()>;
    fn shutdown(&self) -> Result<()>;

    // Provided methods
    fn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>> { ... }
    fn start(&self) -> Result<()> { ... }
    fn begin_shutdown(&self) -> Result<bool> { ... }
    fn shutdown_work_pending(&self) -> bool { ... }
    fn finish_shutdown(&self) -> Result<()> { ... }
    fn spawn_blocking(
        &self,
        work: Box<dyn FnOnce() + Send + 'static>,
    ) -> Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>> { ... }
}
Expand description

Service-provider interface for plugging a custom async runtime into NAPI-RS.

The async-runtime feature exposes this SPI without by itself imposing a module-load requirement. Implement this trait to back napi with your own scheduler (for example, a single-threaded or WASI-friendly runtime) and register exactly one instance from #[module_init]. A pure async-runtime addon with no registration can still load and expose synchronous APIs; runtime-backed operations reject with a missing-backend error.

If no custom backend has been registered, the registration window closes when napi begins activating the first Node-API environment, or earlier when a runtime-backed operation commits a backend choice. In a combined async-runtime + tokio_rt build, that choice defaults generated #[napi] futures to the built-in Tokio runtime. The established free spawn, spawn_blocking, block_on, and within_runtime_if_available names remain Tokio compatibility APIs whenever tokio_rt is enabled, so Cargo feature unification cannot silently change their signatures or routing; generated #[napi] async fn futures follow the selection (the custom backend if one was registered before the window closed, otherwise Tokio). Selecting and starting a custom backend does not construct Tokio; the first Tokio compatibility helper call constructs it lazily. In a pure async-runtime build there is no Tokio at all, and a missing-backend error before any environment is activated leaves the selection undecided and does not prevent later registration.

Under the noop feature this SPI cannot be installed: try_register_async_runtime safely retires the supplied backend and reports crate::Status::InvalidArg, while the infallible register_async_runtime wrapper retires it and preserves its no-op result.

The implementation is stored once per linked addon image and shared across its threads, hence the Send + Sync + 'static bound. The backend’s Drop implementation is not guaranteed to run; shutdown is the sole resource-release and quiescence hook. Keep a newly constructed backend dormant, create active resources in start, and release them in shutdown. See register_async_runtime for duplicate registration behavior.

Panic containment described by this API requires a panic = "unwind" build. With panic = "abort", including Rust’s currently shipped wasm32-wasip1 and wasm32-wasip1-threads targets, catch_unwind cannot intercept a panic: generated async functions may trap or abort before their JavaScript promise is settled.

§Safety

Node may unload an addon’s native image immediately after its last environment cleanup returns. Implementations must ensure that, after shutdown returns, no backend-owned thread, task, closure, destructor, cancellation callback, or future Node-API callback can execute code or access data from that image. This includes externally retained Waker or RawWaker clones and any other task-owned callback, function pointer, or vtable reference whose later wake, clone, or drop path could enter addon code. This requirement applies to both Ok and Err returns. A backend that cannot prove those references inert must keep the native image loaded itself or terminate the process rather than return.

Required Methods§

Source

fn spawn( &self, task: AsyncRuntimeTask, ) -> Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>>

Submit a task to run to completion in the background.

Return Ok(()) only after taking ownership of the task. Return Err(AsyncRuntimeRejection::new(task, error)) when the runtime is stopped, saturated, or otherwise unable to accept it. The error is surfaced through the generated promise rejection. Dropping an accepted task invokes its cancellation callback, so shutdown implementations may cancel queued work by dropping it without leaving JavaScript promises pending forever; cleanup-driven cancellation rejections are best-effort and are dropped when the promise machinery is already closing. Never forget an accepted task: retain it until completion or drop it on cancellation.

Submissions can arrive before the first start of an environment cycle completes, or after a start that failed: module-init and module-export hooks may invoke runtime-backed APIs during first-env registration (napi runs the start-with-rollback sequence before its first dispatch, but a start failure does not gate dispatch). A dormant or not-ready backend must decline such work with an AsyncRuntimeRejection or accept it and defer execution.

A backend may poll the task synchronously before this hook returns. The first poll commits ownership and may run user work immediately; after that point returning the task in an AsyncRuntimeRejection or panicking cannot roll back effects that already occurred. On unwind-enabled builds, napi already catches task panics. Poll the task directly and do not bypass its Drop implementation.

Source

fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()>

Block the current thread, fully driving the pinned future to completion before returning.

Return a backend-specific error if the drive cannot be started or completed. Run the future to completion rather than returning on the first pending poll. The borrowed future must not be retained, moved to another thread, or accessed after this method returns. On either Ok or Err, the backend must stop accessing the borrowed future before returning.

Source

fn shutdown(&self) -> Result<()>

Shut the runtime down.

napi installs its environment-cleanup ownership once per addon image — on the first Node environment that loads the addon — and calls this hook when that accounting observes the last live environment exit. In processes with additional environments (for example worker_threads) or with environment re-creation, the automatic call is therefore not guaranteed to fire; embedders can call shutdown_async_runtime explicitly, including while environments remain live. Stop accepting work before returning and drop queued AsyncRuntimeTask values and queued spawn_blocking closures so their promises are cancelled. Those cancellation-driven promise rejections are best-effort: when the promise machinery is already closing during environment teardown, napi drops the rejection. Return Ok(()) only after backend-owned worker threads, running tasks, and running blocking closures have fully quiesced: Node may unload a worker’s addon image as soon as its environment cleanup returns. Do not wait for JavaScript callbacks triggered by cancellation, and do not call napi’s runtime registration or lifecycle functions recursively from this hook. The hook must be idempotent and tolerate being called before start, after a partial failed start, and repeatedly without an intervening start. If this returns an error, the same quiescence guarantee still applies.

Provided Methods§

Source

fn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>>

Enter the runtime context and return a guard that establishes it for the calling thread.

In pure async-runtime builds within_runtime_if_available delegates here; combined tokio_rt builds retain its established Tokio routing. The returned guard MUST keep the runtime context active for the whole duration of the closure and tear it down on drop. Return a backend-specific error if the context cannot be entered. The default implementation returns a no-op guard, which is correct for backends that do not need an ambient context.

Source

fn start(&self) -> Result<()>

Start (or restart) the runtime.

napi calls this when the first live Node environment for the addon starts, or earlier when a module-init or module-export hook triggers the first runtime-backed dispatch of the cycle. Worker isolates and Electron renderer reloads can take the live environment count from zero to one repeatedly, so this may run more than once over the backend’s lifetime. Implement it idempotently. Return success only after the backend can accept tasks. Do not call napi’s runtime registration or lifecycle functions recursively from this hook. If this returns an error, or panics on an unwind-enabled build, napi calls shutdown to roll back resources created by the partial start. With panic = "abort", a panic traps or aborts before rollback can run. The default is a no-op.

Source

fn begin_shutdown(&self) -> Result<bool>

Optional hook: phase 1 of a two-phase shutdown.

Never wait here. Publish the stop, close admission, cancel and drop everything that is merely queued, wake whatever is parked — and return. Ok(true) reports that the announced stop still owes a wait: backend work that is still live, or a rejected submission whose destructor is still running on another thread. The host should then let its event loop turn and poll shutdown_work_pending before calling finish_shutdown; Ok(false) reports that finish_shutdown has nothing to wait for.

§The contract across the two phases

Between the phases the host may run arbitrary event-loop turns — that is the entire point: on the threaded WASI artifact the shutdown is entered from the JavaScript thread, which is also the only thread that can give a running blocking closure the JavaScript turn it is waiting for, so a single-call shutdown there waits for work that can never finish. The backend must therefore keep rejecting submissions for the whole window, and must tolerate its own cancellation callbacks running JavaScript in it. napi does not call start between the phases and a backend need not support it.

Only finish_shutdown joins, so only it may block; the quiescence guarantee shutdown documents is owed by finish_shutdown, not by this hook. A second begin_shutdown before the matching finish_shutdown must neither start a second shutdown nor wait — re-reporting shutdown_work_pending is the expected answer.

§Not splitting is a valid implementation

The default performs the whole of shutdown here and reports no pending work, which makes finish_shutdown a no-op — so a backend that does not opt in behaves exactly as it does today: the host’s begin blocks for the full teardown and its finish returns immediately.

Source

fn shutdown_work_pending(&self) -> bool

Optional hook: non-blocking poll for the window between begin_shutdown and finish_shutdown.

true means the announced stop still owes a wait: backend work, or a rejected submission whose destructor is still running on another thread. false means finish_shutdown will not wait. Must never block and must be safe to call at any time, including with no shutdown outstanding. The default answers false, which is correct for a backend whose begin_shutdown already finished the teardown.

Never blocking includes locks: when a lock the answer is read under is held by another thread, answer true instead of waiting for it, and the host simply polls again. On wasm32-wasip1-threads a thread that traps while holding a lock unwinds nothing, so that lock never comes back, and a poll waiting for it would park the JavaScript thread for good.

Source

fn finish_shutdown(&self) -> Result<()>

Optional hook: phase 2 of a two-phase shutdown.

Wait for the work begin_shutdown reported, join backend-owned threads, release resources. This is the call that owes the quiescence guarantee in shutdown: return Ok(()) only once every backend-owned thread, task and blocking closure has quiesced, and deliver that guarantee even when returning an error. napi calls it exactly once per begin_shutdown, on the same thread. The default is a no-op, because the default begin_shutdown already did all of it.

Source

fn spawn_blocking( &self, work: Box<dyn FnOnce() + Send + 'static>, ) -> Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>>

Optional hook: run work on the backend’s blocking-capable lane.

Return Ok(()) once the work is accepted; the backend should run the closure exactly once on a thread where blocking is acceptable. Dropping accepted work, for example while shutting down, safely cancels the caller’s pending operation. Return Err(AsyncRuntimeRejection::new(work, error)) to decline; napi surfaces that diagnostic and does not create an unbounded fallback thread. Never forget accepted work: run it exactly once or drop it during cancellation. The default implementation declines with crate::Status::GenericFailure.

The backend may invoke the closure synchronously before this hook returns. Invocation commits ownership, so a later hook panic cannot replace the closure’s result.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§