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}