Skip to main content

otter/
resource.rs

1//! Resource types: `Resource` trait, `ResourceArc<T>`, `Monitor`.
2//!
3//! Resources allow Rust values to live on the BEAM heap, garbage collected and
4//! reference counted by the VM. A `ResourceArc<T>` is the Rust-side handle; the
5//! corresponding Erlang-side value is a reference term.
6
7use std::ffi::{c_int, c_void};
8use std::marker::PhantomData;
9use std::panic::{catch_unwind, AssertUnwindSafe};
10
11use crate::codec::{CodecError, Decoder, Encoder};
12use crate::priv_data::{PrivData, ResourceRegistry};
13use crate::types::{AnyTerm, CallbackEnv, Env, InitEnv, LocalPid, Term};
14
15// ---------------------------------------------------------------------------
16// ResourceTypeHandle
17// ---------------------------------------------------------------------------
18
19/// A `Send` handle to a registered resource type `T`, obtained from
20/// [`resource_handle`].
21///
22/// Holds the BEAM-side resource-type pointer so a resource can be created
23/// ([`make`](Self::make)) without consulting `enif_priv_data` — capture it
24/// inside a module-bound NIF call, then move it to an OS thread where no
25/// module-bound env is available.
26pub struct ResourceTypeHandle<T: Resource> {
27    ptr: *mut enif_ffi::ResourceType,
28    _t: PhantomData<fn() -> T>,
29}
30
31// SAFETY: enif_ffi::ResourceType is BEAM-internal data that lives for the VM's
32// lifetime. Safe to share across threads once registered.
33unsafe impl<T: Resource> Send for ResourceTypeHandle<T> {}
34unsafe impl<T: Resource> Sync for ResourceTypeHandle<T> {}
35
36impl<T: Resource> ResourceTypeHandle<T> {
37    /// Wrap `val` in a new resource object on the BEAM heap.
38    pub fn make(self, val: T) -> ResourceArc<T> {
39        // Allocate enough for T at its required alignment (see ResourceArc docs).
40        let alloc_size = std::mem::size_of::<T>() + std::mem::align_of::<T>() - 1;
41        let raw = unsafe { enif_ffi::alloc_resource(self.ptr, alloc_size) };
42        assert!(!raw.is_null(), "enif_alloc_resource returned null");
43        let inner = align_ptr::<T>(raw);
44        unsafe { std::ptr::write(inner, val) };
45        ResourceArc { raw, inner }
46    }
47}
48
49/// Look up this build's resource registry from a module-bound env. `None` if the
50/// library has no `PrivData` installed (no load callback).
51fn registry<'id>(env: impl Env<'id>) -> Option<&'id ResourceRegistry> {
52    let pd = unsafe { enif_ffi::priv_data(env.raw_env()) } as *const PrivData;
53    if pd.is_null() {
54        return None;
55    }
56    // SAFETY: pd points at this build's PrivData, leaked for the VM lifetime.
57    Some(unsafe { (*pd).registry() })
58}
59
60/// Obtain the [`ResourceTypeHandle`] for resource type `T`.
61///
62/// Looks `T` up in this library's registry (via `enif_priv_data`), so the env
63/// must be module-bound. Panics if `T` was never registered.
64pub fn resource_handle<'id, T: Resource>(env: impl Env<'id>) -> ResourceTypeHandle<T> {
65    let ptr = registry(env).and_then(|r| r.get::<T>()).expect(
66        "resource type not registered — call otter::resource::register::<T> in your load callback",
67    );
68    ResourceTypeHandle { ptr, _t: PhantomData }
69}
70
71/// Wrap `val` in a new resource object on the BEAM heap. Shorthand for
72/// `resource_handle::<T>(env).make(val)`. Panics if `T` was never registered.
73pub fn make_resource<'id, T: Resource>(env: impl Env<'id>, val: T) -> ResourceArc<T> {
74    resource_handle::<T>(env).make(val)
75}
76
77// ---------------------------------------------------------------------------
78// Monitor
79// ---------------------------------------------------------------------------
80
81/// A process monitor handle, returned by [`ResourceArc::monitor`]. Passed to
82/// [`Resource::down`] when the monitored process exits.
83#[derive(Clone, Copy)]
84pub struct Monitor(pub(crate) enif_ffi::Monitor);
85
86impl Monitor {
87    /// Convert this monitor to a term (`enif_make_monitor_term`).
88    pub fn to_term<'id>(self, env: impl Env<'id>) -> AnyTerm<'id> {
89        let raw = unsafe { enif_ffi::make_monitor_term(env.raw_env(), &self.0) };
90        AnyTerm::wrap(raw, env)
91    }
92}
93
94impl PartialEq for Monitor {
95    fn eq(&self, other: &Self) -> bool {
96        // enif_compare_monitors is env-less.
97        unsafe { enif_ffi::compare_monitors(&self.0, &other.0) == 0 }
98    }
99}
100
101impl Eq for Monitor {}
102
103// ---------------------------------------------------------------------------
104// Resource trait
105// ---------------------------------------------------------------------------
106
107/// Marker and callback trait for Erlang resource types.
108///
109/// Implement on any `Sized + Send + Sync + 'static` type to let it live on the
110/// BEAM heap as a resource object. The callbacks run with a [`CallbackEnv`].
111///
112/// Every resource type must be registered once in the load callback via
113/// [`register`] before any `ResourceArc<T>` is created or decoded. A resource's
114/// `T` is not assumed to survive a hot code upgrade across non-identical builds
115/// (see `docs/UPGRADE.md`).
116pub trait Resource: Sized + Send + Sync + 'static {
117    /// Called when the BEAM garbage collects the last reference. Takes ownership
118    /// of `self`; the default drops it.
119    fn destructor(self, _env: CallbackEnv<'_>) {}
120
121    /// Called when a process monitored via [`ResourceArc::monitor`] exits. The
122    /// exiting process is always local, so `pid` is a [`LocalPid`].
123    fn down<'a>(&'a self, _env: CallbackEnv<'a>, _pid: LocalPid, _monitor: Monitor) {}
124
125    /// Called when the BEAM stops monitoring an event selected on this resource.
126    /// `is_direct_call` is `true` when the callback runs synchronously inside
127    /// the `select` call. A `stop` callback is registered unconditionally.
128    fn stop(&self, _env: CallbackEnv<'_>, _event: enif_ffi::Event, _is_direct_call: bool) {}
129}
130
131// ---------------------------------------------------------------------------
132// C-level callbacks (one instantiation per type T via monomorphization)
133// ---------------------------------------------------------------------------
134
135/// Absorb a panic that escaped a resource callback. These run on scheduler
136/// threads outside any process context, so unwinding across the `extern "C"`
137/// boundary would abort the VM and there is no process to raise into — catch
138/// and log.
139fn absorb_callback_panic(what: &str, result: std::thread::Result<()>) {
140    if let Err(payload) = result {
141        let msg = payload
142            .downcast_ref::<&str>()
143            .copied()
144            .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
145            .unwrap_or("<non-string panic payload>");
146        eprintln!("otter: panic in resource {what} callback absorbed: {msg}");
147    }
148}
149
150unsafe extern "C" fn destructor_callback<T: Resource>(env: *mut enif_ffi::Env, obj: *mut c_void) {
151    let inner = align_ptr::<T>(obj);
152    let result = catch_unwind(AssertUnwindSafe(|| {
153        // SAFETY: obj was written by ResourceTypeHandle::make and is not yet dropped.
154        let val = unsafe { std::ptr::read(inner) };
155        // SAFETY: env is valid for the duration of this callback.
156        unsafe { CallbackEnv::with_raw(env, |cenv| val.destructor(cenv)) };
157    }));
158    absorb_callback_panic("destructor", result);
159}
160
161unsafe extern "C" fn down_callback<T: Resource>(
162    env: *mut enif_ffi::Env,
163    obj: *mut c_void,
164    pid: *mut enif_ffi::Pid,
165    mon: *mut enif_ffi::Monitor,
166) {
167    let inner = align_ptr::<T>(obj) as *const T;
168    let result = catch_unwind(AssertUnwindSafe(|| {
169        let pid = LocalPid { pid: unsafe { *pid } };
170        let monitor = Monitor(unsafe { *mon });
171        // SAFETY: env is valid for the callback; inner points at a live T.
172        unsafe { CallbackEnv::with_raw(env, |cenv| (*inner).down(cenv, pid, monitor)) };
173    }));
174    absorb_callback_panic("down", result);
175}
176
177unsafe extern "C" fn stop_callback<T: Resource>(
178    env: *mut enif_ffi::Env,
179    obj: *mut c_void,
180    event: enif_ffi::Event,
181    is_direct_call: c_int,
182) {
183    let inner = align_ptr::<T>(obj) as *const T;
184    let result = catch_unwind(AssertUnwindSafe(|| {
185        // SAFETY: env is valid for the callback; stop runs before the destructor,
186        // so the value is live.
187        unsafe { CallbackEnv::with_raw(env, |cenv| (*inner).stop(cenv, event, is_direct_call != 0)) };
188    }));
189    absorb_callback_panic("stop", result);
190}
191
192// ---------------------------------------------------------------------------
193// Registration
194// ---------------------------------------------------------------------------
195
196/// Flags controlling resource type registration. `CREATE` opens a new type;
197/// `CREATE | TAKEOVER` additionally takes over a matching type from a previous
198/// build during a hot upgrade.
199pub use enif_ffi::ResourceFlags;
200
201/// Register resource type `T` with the BEAM, keyed by the fully-qualified Rust
202/// type path. Must be called exactly once per type, from the load/upgrade
203/// callback ([`InitEnv`]), before any `ResourceArc<T>` is created or decoded.
204///
205/// The BEAM-side name is `"{type_name}#abi={hash}"`, where `{hash}` is a content
206/// hash of this build's binary, so a different build takes a different name and
207/// the BEAM does not take this build's resources over across an upgrade —
208/// upholding the no-cross-build-ABI invariant. For deliberate cross-build
209/// takeover, use [`register_tagged`].
210pub fn register<'id, T: Resource>(env: InitEnv<'id>, flags: ResourceFlags) {
211    let name = format!("{}#abi={:016x}", std::any::type_name::<T>(), crate::abi::tag());
212    register_named::<T>(env, flags, &name);
213}
214
215/// Register resource type `T` under a stable, ABI-versioned name, opting it into
216/// cross-build takeover during a hot upgrade (a per-type promise that `T`'s
217/// layout is stable across those builds). Same calling rules as [`register`].
218pub fn register_tagged<'id, T: Resource>(env: InitEnv<'id>, flags: ResourceFlags, tag: &str) {
219    let name = format!("{}#tag={}", std::any::type_name::<T>(), tag);
220    register_named::<T>(env, flags, &name);
221}
222
223fn register_named<'id, T: Resource>(env: InitEnv<'id>, flags: ResourceFlags, name: &str) {
224    let cname =
225        std::ffi::CString::new(name).expect("resource type name must not contain null bytes");
226
227    let init = enif_ffi::ResourceTypeInit {
228        dtor: Some(destructor_callback::<T>),
229        stop: Some(stop_callback::<T>),
230        down: Some(down_callback::<T>),
231        members: 3,
232        dyncall: None,
233    };
234
235    let mut tried = flags;
236    let type_ptr =
237        unsafe { enif_ffi::init_resource_type(env.raw_env(), cname.as_ptr(), &init, flags, &mut tried) };
238
239    assert!(
240        !type_ptr.is_null(),
241        "enif_init_resource_type failed — it must run inside load/upgrade, and the \
242         flags must match the name's state (CREATE needs a new name, TAKEOVER an \
243         existing one); a name collision under CREATE alone also returns null"
244    );
245
246    // The load scaffolding installs PrivData before dispatching the user
247    // callback, so the slot is non-null here.
248    let pd = unsafe { enif_ffi::priv_data(env.raw_env()) } as *mut PrivData;
249    assert!(
250        !pd.is_null(),
251        "priv_data not installed — register must run inside otter's load scaffolding"
252    );
253    // SAFETY: single-threaded load/upgrade; pd is this build's PrivData.
254    unsafe { (*pd).registry_mut().insert::<T>(type_ptr) };
255}
256
257// ---------------------------------------------------------------------------
258// ResourceArc<T>
259// ---------------------------------------------------------------------------
260
261/// A reference-counted Rust value on the BEAM heap.
262///
263/// The BEAM holds one reference (via the resource term); each `ResourceArc`
264/// handle holds another. The wrapped value is destroyed only when all
265/// references — Erlang-side and Rust-side — are released.
266///
267/// `enif_alloc_resource` aligns to at least `sizeof(void*)`; to support stricter
268/// alignment we allocate `size_of::<T>() + align_of::<T>() - 1` bytes and store
269/// `raw` (allocation start, for keep/release) and `inner` (aligned write
270/// position, for `Deref`/destructor).
271pub struct ResourceArc<T: Resource> {
272    raw: *mut c_void,
273    inner: *mut T,
274}
275
276unsafe impl<T: Resource> Send for ResourceArc<T> {}
277unsafe impl<T: Resource> Sync for ResourceArc<T> {}
278
279/// Compute the aligned pointer for T within a raw allocation.
280fn align_ptr<T>(raw: *mut c_void) -> *mut T {
281    let align = std::mem::align_of::<T>();
282    let aligned = ((raw as usize) + align - 1) & !(align - 1);
283    aligned as *mut T
284}
285
286impl<T: Resource> ResourceArc<T> {
287    /// The raw resource pointer. Used internally for `enif_select` and similar
288    /// calls that need the raw allocation address.
289    pub fn raw_ptr(&self) -> *mut c_void {
290        self.raw
291    }
292
293    /// Monitor the process identified by `pid`. `Some(Monitor)` on success;
294    /// `None` if the process is already dead or `pid` is not a valid local pid.
295    /// `env` may be `None` when calling from a non-NIF thread.
296    pub fn monitor<'id, E: Env<'id>>(&self, env: Option<E>, pid: &LocalPid) -> Option<Monitor> {
297        let env_ptr = env.map(|e| e.raw_env()).unwrap_or(std::ptr::null_mut());
298        // Zero-init an out-param the BEAM fills; sized by enif-ffi (sizeof(void*)*4,
299        // 32 on 64-bit / 16 on 32-bit), so never hardcode the length here.
300        let mut mon: enif_ffi::Monitor = unsafe { std::mem::zeroed() };
301        let rc = unsafe { enif_ffi::monitor_process(env_ptr, self.raw, &pid.pid, &mut mon) };
302        if rc == 0 {
303            Some(Monitor(mon))
304        } else {
305            None
306        }
307    }
308
309    /// Remove a monitor previously set with [`monitor`](Self::monitor). `true`
310    /// if the monitor existed and was removed.
311    pub fn demonitor<'id, E: Env<'id>>(&self, env: Option<E>, mon: &Monitor) -> bool {
312        let env_ptr = env.map(|e| e.raw_env()).unwrap_or(std::ptr::null_mut());
313        unsafe { enif_ffi::demonitor_process(env_ptr, self.raw, &mon.0) == 0 }
314    }
315}
316
317impl<T: Resource> Clone for ResourceArc<T> {
318    fn clone(&self) -> ResourceArc<T> {
319        unsafe { enif_ffi::keep_resource(self.raw) };
320        ResourceArc { raw: self.raw, inner: self.inner }
321    }
322}
323
324impl<T: Resource> Drop for ResourceArc<T> {
325    fn drop(&mut self) {
326        // Decrement the ref count; at zero the BEAM calls destructor_callback.
327        unsafe { enif_ffi::release_resource(self.raw) };
328    }
329}
330
331impl<T: Resource> std::ops::Deref for ResourceArc<T> {
332    type Target = T;
333    fn deref(&self) -> &T {
334        unsafe { &*self.inner }
335    }
336}
337
338impl<'id, T: Resource> Encoder<'id> for ResourceArc<T> {
339    /// Encode the resource as a reference term (`enif_make_resource`). The BEAM
340    /// releases that reference when the term is garbage collected.
341    fn encode(&self, env: impl Env<'id>) -> Result<AnyTerm<'id>, CodecError> {
342        let raw = unsafe { enif_ffi::make_resource(env.raw_env(), self.raw) };
343        Ok(AnyTerm::wrap(raw, env))
344    }
345}
346
347impl<'id, T: Resource> Decoder<'id> for ResourceArc<T> {
348    /// Decode a resource term into a `ResourceArc<T>`. `WrongType` if the term is
349    /// not a resource of type `T`, or if `T` was never registered.
350    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
351        let type_ptr = registry(env).and_then(|r| r.get::<T>()).ok_or(CodecError::WrongType)?;
352
353        let mut obj: *mut c_void = std::ptr::null_mut();
354        if unsafe { enif_ffi::get_resource(env.raw_env(), term.raw_term(), type_ptr, &mut obj) } == 0 {
355            return Err(CodecError::WrongType);
356        }
357
358        // New Rust-side reference; increment the ref count.
359        unsafe { enif_ffi::keep_resource(obj) };
360        Ok(ResourceArc { raw: obj, inner: align_ptr::<T>(obj) })
361    }
362}
363
364// ---------------------------------------------------------------------------
365// Dynamic resource call
366// ---------------------------------------------------------------------------
367
368/// Invoke a dynamic call on a resource (`enif_dynamic_resource_call`).
369/// `mod_name`/`name` identify the resource type; `rsrc` is the resource term.
370/// Returns `0` on success.
371///
372/// # Safety
373/// `call_data` must match what the dyncall callback expects.
374pub unsafe fn dynamic_resource_call<'id>(
375    env: impl Env<'id>,
376    mod_name: impl Term<'id>,
377    name: impl Term<'id>,
378    rsrc: impl Term<'id>,
379    call_data: *mut c_void,
380) -> i32 {
381    unsafe {
382        enif_ffi::dynamic_resource_call(
383            env.raw_env(),
384            mod_name.raw_term(),
385            name.raw_term(),
386            rsrc.raw_term(),
387            call_data,
388        )
389    }
390}