alef 0.25.37

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.
    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 %}
        // SAFETY: The JS object is owned by the Node.js runtime and lives for
        // the duration of the enclosing #[napi] call. The bridge is only used
        // synchronously during that same call, so 'static is safe here.
        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe {
            std::mem::transmute(js_obj)
        };

        // Cache the plugin name. `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 %}

        Ok(Self {
            inner: js_obj,
            cached_name,
            cancellation_token: std::sync::Arc::new(tokio_util::sync::CancellationToken::new()),
        })
    }

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

    /// Extract napi::Env from the stored Object.
    fn env(&self) -> napi::Env {
        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
        let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
        napi::Env::from_raw(raw[0] as napi::sys::napi_env)
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for {{ wrapper_name }} {}
unsafe impl Sync for {{ wrapper_name }} {}