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§
Sourcefn spawn(
&self,
task: AsyncRuntimeTask,
) -> Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>>
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.
Sourcefn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()>
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.
Sourcefn shutdown(&self) -> Result<()>
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§
Sourcefn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>>
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.
Sourcefn start(&self) -> Result<()>
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.
Sourcefn begin_shutdown(&self) -> Result<bool>
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.
Sourcefn shutdown_work_pending(&self) -> bool
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.
Sourcefn finish_shutdown(&self) -> Result<()>
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.
Sourcefn spawn_blocking(
&self,
work: Box<dyn FnOnce() + Send + 'static>,
) -> Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>>
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".