Skip to main content

enif_ffi/
lib.rs

1//! `enif-ffi` — raw FFI bindings to the Erlang NIF API (`erl_nif`).
2//!
3//! A thin, 1:1, all-`unsafe` binding to the `enif_*` C API exported by the
4//! BEAM. It is the foundation a safe NIF library is built on; it adds no
5//! abstractions of its own.
6//!
7//! # Naming
8//!
9//! Every C prefix is dropped — `enif_` (functions), `ERL_NIF_` (the term type,
10//! macros, constants), and `ErlNif` (everything else) — because the whole API
11//! lives under the `enif_ffi::` namespace and the prefix would be pure
12//! redundancy:
13//!
14//! - `ERL_NIF_TERM` → [`Term`], `ErlNifEnv` → [`Env`], `ErlNifBinary` → [`Binary`]
15//! - `enif_make_atom` → `make_atom`, `enif_is_atom` → `is_atom`
16//! - `ERL_NIF_SELECT_READ` → [`SelectFlags::READ`]
17//!
18//! A bare name that would collide with a Rust keyword or a `std` prelude item
19//! takes a trailing underscore: `ErlNifOption` → `Option_`, `enif_self` →
20//! `self_`.
21//!
22//! The public surface is flat: everything is re-exported to the crate root.
23//! Prefer namespaced use (`enif_ffi::Term`, `enif_ffi::make_atom`) over a glob
24//! import.
25//!
26//! # Who this is for
27//!
28//! Most NIF authors want a *safe* library on top of this — writing NIFs by hand
29//! against a raw, all-`unsafe` table is deliberate, low-level work. Reach for
30//! `enif-ffi` directly when you are building that safe layer, or when you want
31//! the bare `enif_*` surface with no abstraction in the way.
32//!
33//! # A minimal NIF
34//!
35//! A consumer touches three things: the C-ABI functions (the BEAM calling
36//! convention), the [`Entry`] that registers them and carries the load-time
37//! metadata, and [`nif_init!`] to define the entry point. The same source
38//! compiles on Unix and Windows.
39//!
40//! ```no_run
41//! use enif_ffi::*;
42//! use std::ffi::c_int;
43//!
44//! // add(A, B) -> A + B  — decode two integers, return their sum.
45//! unsafe extern "C" fn nif_add(env: *mut Env, argc: c_int, argv: *const Term) -> Term {
46//!     if argc != 2 {
47//!         return unsafe { make_badarg(env) };
48//!     }
49//!     let args = unsafe { std::slice::from_raw_parts(argv, 2) };
50//!     let (mut a, mut b): (c_int, c_int) = (0, 0);
51//!     if unsafe { get_int(env, args[0], &mut a) } == 0
52//!         || unsafe { get_int(env, args[1], &mut b) } == 0
53//!     {
54//!         return unsafe { make_badarg(env) };
55//!     }
56//!     unsafe { make_int(env, a.wrapping_add(b)) }
57//! }
58//!
59//! // mk_tuple() -> {ok, 42}  — build a 2-tuple of an atom and an integer.
60//! unsafe extern "C" fn nif_mk_tuple(env: *mut Env, _argc: c_int, _argv: *const Term) -> Term {
61//!     let ok = unsafe { make_atom(env, c"ok".as_ptr()) };
62//!     let n = unsafe { make_int(env, 42) };
63//!     unsafe { make_tuple2(env, ok, n) }
64//! }
65//!
66//! // Build the library descriptor: the function table plus the version and name
67//! // metadata the BEAM reads at load. Both the table and the `Entry` must outlive
68//! // the call, so they are leaked.
69//! fn build_entry() -> *const Entry {
70//!     let funcs = vec![
71//!         Func { name: c"add".as_ptr(), arity: 2, fptr: nif_add, flags: 0 },
72//!         Func { name: c"mk_tuple".as_ptr(), arity: 0, fptr: nif_mk_tuple, flags: 0 },
73//!     ]
74//!     .leak();
75//!
76//!     Box::leak(Box::new(Entry {
77//!         major: MAJOR_VERSION,
78//!         minor: MINOR_VERSION,
79//!         name: c"example".as_ptr(),
80//!         num_of_funcs: funcs.len() as c_int,
81//!         funcs: funcs.as_mut_ptr(),
82//!         load: None,
83//!         reload: None,
84//!         upgrade: None,
85//!         unload: None,
86//!         vm_variant: VM_VARIANT.as_ptr(),
87//!         options: 1,
88//!         sizeof_resource_type_init: std::mem::size_of::<ResourceTypeInit>(),
89//!         min_erts: MIN_ERTS_VERSION.as_ptr(),
90//!     }))
91//! }
92//!
93//! // Generates the platform-correct `nif_init` symbol the BEAM calls at load.
94//! enif_ffi::nif_init!(build_entry);
95//! ```
96//!
97//! On the Erlang side, once the module has loaded the library:
98//!
99//! ```erlang
100//! example:add(2, 3).    %=> 5
101//! example:mk_tuple().   %=> {ok, 42}
102//! ```
103//!
104//! `smoke_test/` in the repository is the full, CI-exercised version of this:
105//! built and loaded into real BEAMs across every supported NIF version.
106//!
107//! # Version support
108//!
109//! The floor is **NIF 2.15 (OTP 22)** — always compiled. Newer API is opt-in
110//! through an additive feature ladder, each rung pulling in the one below:
111//!
112//! - `nif_2_16` — OTP 24
113//! - `nif_2_17` — OTP 26
114//! - `nif_2_18` — OTP 29
115//!
116//! Because the rungs chain, the enabled set is always a contiguous prefix, so
117//! each version-gated item carries exactly one `#[cfg]`. Enabling a rung means
118//! "I require at least this OTP"; the symbols it adds are resolved at load time
119//! and a BEAM older than the target will fail the load rather than misbehave.
120//!
121//! Item-level version annotations and the gating boundaries are derived from the
122//! tagged `erl_nif` header history (the `otp-enif` snapshot repo).
123//!
124//! # Platform
125//!
126//! Unix and Windows are both supported; the binding mechanism is chosen at
127//! compile time. On Unix the `enif_*` table is resolved with `dlsym` at load;
128//! on Windows the BEAM passes a callback struct to `nif_init`, which is stored
129//! instead. [`nif_init!`] generates the correctly-typed entry point for the
130//! target, so a NIF's source is identical on both.
131//!
132//! # Verification
133//!
134//! Correctness rests on two checks rather than exhaustive per-symbol tests:
135//! the `enif_*` table layout is audited against the tagged `erl_nif` header
136//! history (every field version-gated and order-matched), and a smoke NIF is
137//! built and loaded into real BEAMs across NIF 2.15–2.18 on both Unix and
138//! Windows in CI, proving the binding resolves and dispatches the table on
139//! every supported version.
140
141// Every `unsafe fn` body must still mark its unsafe operations with an inner
142// `unsafe` block (the default in edition 2024; required here on edition 2021).
143#![deny(unsafe_op_in_unsafe_fn)]
144
145mod api;
146mod ffi;
147mod types;
148
149// Platform-specific definitions: the load-time symbol resolver and the
150// divergent `SysIOVec`. Exactly one is compiled. These are `pub` only so the
151// exported `nif_init!` macro can name their `init` (and `TWinDynNifCallbacks`)
152// from a downstream crate; `#[doc(hidden)]` keeps that machinery — `init`
153// included — out of the documented surface.
154#[cfg(unix)]
155#[doc(hidden)]
156pub mod unix;
157#[cfg(windows)]
158#[doc(hidden)]
159pub mod windows;
160
161#[cfg(not(any(unix, windows)))]
162compile_error!("enif-ffi supports only Unix and Windows targets");
163
164pub use api::*;
165pub use types::*;
166
167/// Define the NIF library's entry point.
168///
169/// Generates the `nif_init` symbol the BEAM calls at load — with the correct
170/// signature for the target platform — resolves the `enif_*` table (`dlsym` on
171/// Unix, the BEAM-supplied callback table on Windows), then calls `$builder`,
172/// your platform-agnostic function returning the library descriptor. The symbol
173/// resolution is handled entirely inside the generated entry point; nothing else
174/// in the crate needs to be called to wire the binding up.
175///
176/// `$builder` must be a `fn() -> *const `[`Entry`](crate::Entry). It runs once
177/// during load, after the table is resolved, so it (and any wrapper it calls)
178/// can use the `enif_*` API.
179///
180/// ```no_run
181/// # use enif_ffi::*;
182/// enif_ffi::nif_init!(build_entry);
183///
184/// fn build_entry() -> *const enif_ffi::Entry {
185///     // build and register your functions, then leak a 'static `Entry`;
186///     // see the crate-level example, or `smoke_test/` for a complete one
187/// #   Box::leak(Box::new(Entry {
188/// #       major: MAJOR_VERSION,
189/// #       minor: MINOR_VERSION,
190/// #       name: c"example".as_ptr(),
191/// #       num_of_funcs: 0,
192/// #       funcs: std::ptr::null_mut(),
193/// #       load: None,
194/// #       reload: None,
195/// #       upgrade: None,
196/// #       unload: None,
197/// #       vm_variant: VM_VARIANT.as_ptr(),
198/// #       options: 1,
199/// #       sizeof_resource_type_init: std::mem::size_of::<ResourceTypeInit>(),
200/// #       min_erts: MIN_ERTS_VERSION.as_ptr(),
201/// #   }))
202/// }
203/// ```
204#[macro_export]
205macro_rules! nif_init {
206    ($builder:path) => {
207        #[cfg(unix)]
208        #[no_mangle]
209        pub extern "C" fn nif_init() -> *const $crate::Entry {
210            if unsafe { $crate::unix::init() }.is_err() {
211                return ::core::ptr::null();
212            }
213            $builder()
214        }
215
216        #[cfg(windows)]
217        #[no_mangle]
218        pub extern "C" fn nif_init(
219            callbacks: *const $crate::windows::TWinDynNifCallbacks,
220        ) -> *const $crate::Entry {
221            unsafe { $crate::windows::init(callbacks) };
222            $builder()
223        }
224    };
225}