alef 0.91.4

Opinionated polyglot binding generator for Rust libraries
Documentation
impl {{ wrapper_name }} {
    /// Create a new bridge wrapping a NAPI Object.
    ///
    /// Validates that the object provides all required methods, then builds one
    /// `ThreadsafeFunction` per bridged method eagerly — this runs synchronously on the
    /// JS thread, inside the `#[napi] register_*` frame, with a live `Object` in scope, which
    /// is the only place a `ThreadsafeFunction` can safely be created from a JS function value.
    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
{%- for method in required_methods %}
        if !js_obj.has_named_property("{{ method.name }}").unwrap_or(false)
            && !js_obj.has_named_property("{{ method.snake_case_name }}").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: {}", "{{ method.name }}")
            ));
        }
{% endfor %}
{%- if requires_plugin_name %}
        if !js_obj.has_named_property("name").unwrap_or(false) {
            return Err(napi::Error::new(
                napi::Status::GenericFailure,
                "Object missing required method: name".to_string()
            ));
        }
{%- endif %}
        // Cache the plugin name while the borrowed object is still in scope. `name`
        // may be either a string property or a zero-arg function returning a string
        // (the trait method form). Try the function first, then fall back to a
        // string property.
        let cached_name = js_obj
            .get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
            .and_then(|f| f.call(()))
            .or_else(|_| js_obj.get_named_property::<String>("name"))
{%- if requires_plugin_name %}
            .map_err(|e| napi::Error::new(
                napi::Status::GenericFailure,
                format!("Object missing required method: name ({e})")
            ))?;
{%- else %}
            .unwrap_or_default();
{%- endif %}

        // Capture the raw napi_env that owns the object. It is stable for the
        // lifetime of the Node process and is used to reconstruct an `Env` when
        // materialising the object or building context arguments.
        // SAFETY: `Object<'_>` is laid out as 3 pointer-sized words whose first
        // word is the owning napi_env.
        let env = unsafe {
            let raw: [*mut std::ffi::c_void; 3] = std::mem::transmute_copy(&js_obj);
            raw[0] as napi::sys::napi_env
        };
        // The thread `new()` runs on. Always the JS thread in practice (this constructor only
        // ever runs inside a synchronous `#[napi]` frame), captured rather than assumed so
        // every call site that branches on "am I on the JS thread" (the sync fast/slow split,
        // the off-thread drop-hazard guard) asks the same fact instead of re-deriving it.
        let js_thread = std::thread::current().id();
        // Hold the JS object via a persistent napi reference instead of a borrowed
        // `Object` transmuted to `'static`. The reference keeps the value alive
        // across handle scopes and, crucially, is released in `dispose()`/`Drop`
        // so it no longer pins the event loop.
        let obj_ref = js_obj.create_ref::<false>()?;

        Ok(Self {
            env,
            js_thread,
            obj_ref: std::sync::Mutex::new(Some(obj_ref)),
            cached_name,
{%- for init in tsfn_field_inits %}
            {{ init }}
{%- endfor %}
        })
    }

    /// Materialise a live `Object` from the persistent reference.
    ///
    /// The returned object is tied to the provided `Env` and kept alive by the
    /// underlying napi reference for the duration of the current event-loop turn.
    /// Returns an error once the reference has been released (after `dispose()`).
    fn obj<'env>(&self, env: &'env napi::Env) -> napi::Result<napi::bindgen_prelude::Object<'env>> {
        let guard = self.obj_ref.lock().unwrap_or_else(|e| e.into_inner());
        match guard.as_ref() {
            Some(obj_ref) => obj_ref.get_value(env),
            None => Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Plugin '{}' object has been disposed", self.cached_name),
            )),
        }
    }

    /// Release the persistent reference, unpinning the event loop.
    ///
    /// Idempotent: safe to call multiple times. After disposal the bridge can no
    /// longer invoke the JS object via the fast (`self.obj()`) path — the per-method
    /// `ThreadsafeFunction`s keep working independently, since they hold their own
    /// reference to the underlying JS function.
    fn release_ref(&self) {
        let Ok(mut guard) = self.obj_ref.lock() else {
            return;
        };
        let Some(obj_ref) = guard.take() else {
            return;
        };
        if std::thread::current().id() != self.js_thread {
            // A `napi_ref` is thread-affine: `napi_delete_reference` must run on the owning JS
            // thread, or it mutates V8's GlobalHandles concurrently with that thread and
            // corrupts the heap (see napi-rs's own `FunctionRef::drop` comment, which routes
            // this same case through the env's internal custom-GC threadsafe function — not
            // available to generated code). Dropping `obj_ref` here without calling `.unref()`
            // leaks the JS object (it survives until process exit) instead of corrupting the
            // heap; `ObjectRef<false>`'s own `Drop` is a silent no-op for exactly this case.
            drop(obj_ref);
            return;
        }
        let env = napi::Env::from_raw(self.env);
        let _ = obj_ref.unref(&env);
    }

    /// Clean up TSFN callbacks and release the event loop.
    pub async fn dispose(&self) -> napi::Result<()> {
        self.release_ref();
        Ok(())
    }

    /// Extract napi::Env from the captured raw env pointer.
    fn env(&self) -> napi::Env {
        napi::Env::from_raw(self.env)
    }
}

impl Drop for {{ wrapper_name }} {
    fn drop(&mut self) {
        // Defensive release in case `dispose()` was never called. Releases the
        // napi reference so the JS object can be collected and the event loop is
        // no longer pinned by this bridge.
        self.release_ref();
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the Node.js event loop
// thread. `env`/`obj_ref` are only ever dereferenced from `self.env()`/`self.obj()`, both of
// which are reached only from the sync fast path — itself gated on
// `std::thread::current().id() == self.js_thread` by every caller — so a HandleScope is always
// live when they run. Every other code path (async methods; the sync slow path) goes through a
// per-method `ThreadsafeFunction`, which is `Send + Sync` by construction and safe to call from
// any thread. The bridge genuinely IS moved across threads (unlike the historic claim this
// comment replaces): async trait methods run on whatever tokio worker polls them.
unsafe impl Send for {{ wrapper_name }} {}
unsafe impl Sync for {{ wrapper_name }} {}