otter-nif 0.3.1

Write Erlang NIFs in Rust. Direct mapping of the NIF C ABI with compile-time lifetime safety and no hidden magic.
Documentation
//! Internal helpers for `otter_codegen`. **Not part of the public API.**
//!
//! Everything here is `#[doc(hidden)]` and exists solely so that generated code
//! (which lives in the user's crate) can call into otter internals.

use std::ffi::c_int;

pub use crate::codec::{CodecError, Decoder, Encoder};
pub use crate::priv_data::{discard_priv_data, free_priv_data, install_priv_data, PrivData};
#[cfg(feature = "raw")]
pub use crate::priv_data::{old_user_priv_field, user_priv_field};
pub use crate::resource::{register, register_tagged, ResourceFlags};
pub use crate::types::{Atom, CallEnv, CallbackEnv, DeinitEnv, InitEnv, TypedTerm};

use crate::types::{AnyTerm, Env, Term, THE_NON_VALUE};

/// Raw `enif_ffi` types and constants named by generated entry points
/// (`#[otter::nif]` wrappers and `init!`'s `nif_init` scaffolding). They live
/// here, always public, so the crate-root `enif_ffi` re-export can be gated
/// behind the `raw` feature without breaking generated code — which references
/// `::otter::__codegen::ffi::*` instead. (enhance-11)
///
/// A nested module so these raw names don't collide with otter's own `Env` /
/// `Term` traits imported above.
pub mod ffi {
    pub use enif_ffi::{
        DIRTY_JOB_CPU_BOUND, DIRTY_JOB_IO_BOUND, Entry, Env, MAJOR_VERSION, MIN_ERTS_VERSION,
        MINOR_VERSION, ResourceTypeInit, Term, VM_VARIANT,
    };
}

/// Value for `enif_ffi::Entry.options` indicating `sizeof_resource_type_init` is set.
pub const NIF_ENTRY_OPTIONS: u32 = 1;

// ---------------------------------------------------------------------------
// Load callback return codes
// ---------------------------------------------------------------------------
//
// BEAM aborts the library load on any non-zero return from the load callback
// and stringifies the integer into the `{error, {load_failed, "...(N)."}}`
// reason. We assign each failure cause a stable integer so users can read the
// (N) and identify the cause.

/// Load succeeded — register the NIF library.
pub const LOAD_OK: c_int = 0;

/// User load callback returned `false`.
pub const LOAD_FAILED_USER_FALSE: c_int = 1;

/// User load callback panicked (caught at the FFI boundary).
pub const LOAD_FAILED_PANIC: c_int = 2;

/// `Decoder::decode` rejected the `load_info` term.
pub const LOAD_FAILED_DECODE: c_int = 3;

/// Metadata for a single NIF function, generated by `#[otter::nif]`.
pub struct NifMeta {
    pub name: &'static [u8],
    pub arity: u32,
    pub raw_fptr:
        unsafe extern "C" fn(*mut enif_ffi::Env, c_int, *const enif_ffi::Term) -> enif_ffi::Term,
    pub flags: u32,
}

impl NifMeta {
    /// Convert to a [`enif_ffi::Func`] for inclusion in a `enif_ffi::Entry`.
    pub fn to_nif_func(&self) -> enif_ffi::Func {
        enif_ffi::Func {
            name: self.name.as_ptr() as *const std::ffi::c_char,
            arity: self.arity as std::ffi::c_uint,
            fptr: self.raw_fptr,
            flags: self.flags as std::ffi::c_uint,
        }
    }
}

// ---------------------------------------------------------------------------
// Generated-code glue
//
// `AnyTerm::wrap` is `pub(crate)`, so generated code (in the user's crate)
// cannot construct terms directly — these public helpers do it internally.
// ---------------------------------------------------------------------------

/// Decode NIF argument `raw` into the user's declared type, in `env`.
pub fn decode_arg<'id, T: Decoder<'id>>(
    env: impl Env<'id>,
    raw: enif_ffi::Term,
) -> Result<T, CodecError> {
    T::decode(AnyTerm::wrap(raw, env), env)
}

/// Encode a NIF result into the raw word to return from the wrapper.
///
/// An encoder failure — a value outside the Erlang term domain — raises
/// `badret`, symmetric to the `badarg` a failed argument decode raises.
pub fn encode_result<'id, T: Encoder<'id>>(val: &T, env: CallEnv<'id>) -> enif_ffi::Term {
    match val.encode(env) {
        Ok(term) => Term::raw_term(term),
        Err(_) => badret_word(env),
    }
}

/// Raise `badarg` on the call env and return the non-value word.
pub fn badarg_word(env: CallEnv<'_>) -> enif_ffi::Term {
    let _ = env.badarg::<()>();
    THE_NON_VALUE
}

/// Raise `badret` (an encoder failed on the way out) and return the non-value
/// word. Symmetric to [`badarg_word`]: a NIF whose return value could not be
/// converted to a term surfaces as `error:badret`, the encode-side mirror of the
/// `error:badarg` a rejected argument raises. The pending exception is set via
/// `enif_raise_exception`; the returned `THE_NON_VALUE` is the ignored sentinel.
/// The `Err` arm is a defensive fallback that cannot be reached — `"badret"` is
/// 6 characters, so it never trips `AtomError::NameTooLong`.
pub fn badret_word(env: CallEnv<'_>) -> enif_ffi::Term {
    match Atom::intern(env, "badret") {
        Ok(atom) => {
            let _ = env.raise::<()>(atom);
            THE_NON_VALUE
        }
        Err(_) => badarg_word(env),
    }
}

/// Raise an exception with reason word `reason` and return the non-value word.
pub fn raise_word(env: CallEnv<'_>, reason: enif_ffi::Term) -> enif_ffi::Term {
    let _ = env.raise::<()>(AnyTerm::wrap(reason, env));
    THE_NON_VALUE
}