diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bad9c64..b46f3452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,19 @@ versions. ### Added +- Resource type registration has been refactored to eventually remove the + `rustler::resource!` macro (#617, necessary due to a pending deprecation of a + Rust feature, #606) +- Resources can (and should) now explicitly implement the new `Resource` trait + and provide a custom `destructor` function that is run before `drop` and + receives an `Env` parameter (#617) +- Process monitoring via resources can now be used on resource types that + implement the `Resource::down` callback (#617) + ### Fixed +- Unwinding in the `on_load` callback is now caught and leads to a panic (#617) + ### Changed - NIF implementations are now discovered automatically and the respective diff --git a/UPGRADE.md b/UPGRADE.md index 4eed3113..1b6595df 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -10,6 +10,9 @@ This document is intended to simplify upgrading to newer versions by extending t 2. The functionality related to the `derive` feature is now unconditionally active. The feature flag is kept for compatibility for now but will be removed in the future. +3. To register a type as a resource, the new `#[derive(Resource)]` can be used + now. It is implicitly registered and does not require (or work in) the old + explicit registration with `rustler::resource!` a custom `load` function. ## 0.32 -> 0.33 diff --git a/rustler/src/codegen_runtime.rs b/rustler/src/codegen_runtime.rs index 6e8754a9..725f42c3 100644 --- a/rustler/src/codegen_runtime.rs +++ b/rustler/src/codegen_runtime.rs @@ -12,7 +12,7 @@ pub use inventory; pub use crate::wrapper::exception::raise_exception; pub use crate::wrapper::{ c_char, c_int, c_uint, c_void, get_nif_resource_type_init_size, DEF_NIF_ENTRY, DEF_NIF_FUNC, - MUTABLE_NIF_RESOURCE_HANDLE, NIF_ENV, NIF_MAJOR_VERSION, NIF_MINOR_VERSION, NIF_TERM, + NIF_ENV, NIF_MAJOR_VERSION, NIF_MINOR_VERSION, NIF_TERM, }; #[cfg(windows)] @@ -100,15 +100,12 @@ impl fmt::Debug for NifReturned { /// # Unsafe /// /// This takes arguments, including raw pointers, that must be correct. -pub unsafe fn handle_nif_init_call( - function: Option fn(Env<'a>, Term<'a>) -> bool>, - r_env: NIF_ENV, - load_info: NIF_TERM, +pub unsafe fn handle_nif_init_call<'a>( + function: for<'b> fn(Env<'b>, Term<'b>) -> bool, + env: Env<'a>, + load_info: Term<'a>, ) -> c_int { - let env = Env::new(&(), r_env); - let term = Term::new(env, load_info); - - function.map_or(0, |inner| i32::from(!inner(env, term))) + std::panic::catch_unwind(|| function(env, load_info)).map_or(1, |x| i32::from(!x)) } pub fn handle_nif_result( diff --git a/rustler/src/env.rs b/rustler/src/env.rs index fcb87f92..acec2c25 100644 --- a/rustler/src/env.rs +++ b/rustler/src/env.rs @@ -1,3 +1,4 @@ +use crate::thread::is_scheduler_thread; use crate::types::LocalPid; use crate::wrapper::{NIF_ENV, NIF_TERM}; use crate::{Encoder, Term}; @@ -17,6 +18,7 @@ type EnvId<'a> = PhantomData<*mut &'a u8>; /// There is no way to allocate a Env at the moment, but this may be possible in the future. #[derive(Clone, Copy)] pub struct Env<'a> { + pub(crate) init: bool, env: NIF_ENV, id: EnvId<'a>, } @@ -47,11 +49,19 @@ impl<'a> Env<'a> { /// Don't create multiple `Env`s with the same lifetime. pub unsafe fn new(_lifetime_marker: &'a T, env: NIF_ENV) -> Env<'a> { Env { + init: false, env, id: PhantomData, } } + #[doc(hidden)] + pub unsafe fn new_init_env(_lifetime_marker: &'a T, env: NIF_ENV) -> Env<'a> { + let mut res = Self::new(_lifetime_marker, env); + res.init = true; + res + } + pub fn as_c_arg(self) -> NIF_ENV { self.env } @@ -229,7 +239,7 @@ impl OwnedEnv { F: FnOnce(Env<'a>) -> T, T: Encoder, { - if unsafe { rustler_sys::enif_thread_type() } != rustler_sys::ERL_NIF_THR_UNDEFINED { + if is_scheduler_thread() { panic!("send_and_clear: current thread is managed"); } diff --git a/rustler/src/lib.rs b/rustler/src/lib.rs index e6e4003a..a7aea28b 100644 --- a/rustler/src/lib.rs +++ b/rustler/src/lib.rs @@ -45,8 +45,8 @@ pub use crate::types::{ #[cfg(feature = "big_integer")] pub use crate::types::BigInt; -pub mod resource; -pub use crate::resource::ResourceArc; +mod resource; +pub use crate::resource::{Monitor, Resource, ResourceArc, ResourceInitError}; #[doc(hidden)] pub mod dynamic; diff --git a/rustler/src/resource.rs b/rustler/src/resource.rs deleted file mode 100644 index fb442f5d..00000000 --- a/rustler/src/resource.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Support for storing Rust data in Erlang terms. -//! -//! A NIF resource allows you to safely store Rust structs in a term, and therefore keep it across -//! NIF calls. The struct will be automatically dropped when the BEAM GC decides that there are no -//! more references to the resource. - -use std::marker::PhantomData; -use std::mem; -use std::ops::Deref; -use std::ptr; - -use super::{Binary, Decoder, Encoder, Env, Error, NifResult, Term}; -use crate::wrapper::{ - c_void, resource, NifResourceFlags, MUTABLE_NIF_RESOURCE_HANDLE, NIF_ENV, NIF_RESOURCE_TYPE, -}; - -/// Re-export a type used by the `resource!` macro. -#[doc(hidden)] -pub use crate::wrapper::NIF_RESOURCE_FLAGS; - -/// The ResourceType struct contains a NIF_RESOURCE_TYPE and a phantom reference to the type it -/// is for. It serves as a holder for the information needed to interact with the Erlang VM about -/// the resource type. -/// -/// This is usually stored in an implementation of ResourceTypeProvider. -#[doc(hidden)] -pub struct ResourceType { - pub res: NIF_RESOURCE_TYPE, - pub struct_type: PhantomData, -} - -/// This trait gets implemented for the type we want to put into a resource when -/// resource! is called on it. It provides the ResourceType. -/// -/// In most cases the user should not have to worry about this. -#[doc(hidden)] -pub trait ResourceTypeProvider: Sized + Send + Sync + 'static { - fn get_type() -> &'static ResourceType; -} - -impl Encoder for ResourceArc -where - T: ResourceTypeProvider, -{ - fn encode<'a>(&self, env: Env<'a>) -> Term<'a> { - self.as_term(env) - } -} -impl<'a, T> Decoder<'a> for ResourceArc -where - T: ResourceTypeProvider + 'a, -{ - fn decode(term: Term<'a>) -> NifResult { - ResourceArc::from_term(term) - } -} - -/// Drop a T that lives in an Erlang resource. (erlang_nif-sys requires us to declare this -/// function safe, but it is of course thoroughly unsafe!) -extern "C" fn resource_destructor(_env: NIF_ENV, handle: MUTABLE_NIF_RESOURCE_HANDLE) { - unsafe { - let aligned = align_alloced_mem_for_struct::(handle); - let res = aligned as *mut T; - ptr::read(res); - } -} - -/// This is the function that gets called from resource! in on_load to create a new -/// resource type. -/// -/// # Panics -/// -/// Panics if `name` isn't null-terminated. -#[doc(hidden)] -pub fn open_struct_resource_type( - env: Env, - name: &str, - flags: NifResourceFlags, -) -> Option> { - let res: Option = unsafe { - resource::open_resource_type( - env.as_c_arg(), - name.as_bytes(), - Some(resource_destructor::), - flags, - ) - }; - - res.map(|r| ResourceType { - res: r, - struct_type: PhantomData, - }) -} - -fn get_alloc_size_struct() -> usize { - mem::size_of::() + mem::align_of::() -} - -/// Given a pointer `ptr` to an allocation of `get_alloc_size_struct::()` bytes, return the -/// first aligned pointer within the allocation where a `T` may be stored. -/// Unsafe: `ptr` must point to a large enough allocation and not be null. -unsafe fn align_alloced_mem_for_struct(ptr: *const c_void) -> *const c_void { - let offset = mem::align_of::() - ((ptr as usize) % mem::align_of::()); - ptr.add(offset) -} - -/// A reference to a resource of type `T`. -/// -/// This type is like `std::sync::Arc`: it provides thread-safe, reference-counted storage for Rust -/// data that can be shared across threads. Data stored this way is immutable by default. If you -/// need to modify data in a resource, use a `std::sync::Mutex` or `RwLock`. -/// -/// Rust code and Erlang code can both have references to the same resource at the same time. Rust -/// code uses `ResourceArc`; in Erlang, a reference to a resource is a kind of term. You can -/// convert back and forth between the two using `Encoder` and `Decoder`. -pub struct ResourceArc -where - T: ResourceTypeProvider, -{ - raw: *const c_void, - inner: *mut T, -} - -// Safe because T is `Sync` and `Send`. -unsafe impl Send for ResourceArc where T: ResourceTypeProvider {} -unsafe impl Sync for ResourceArc where T: ResourceTypeProvider {} - -impl ResourceArc -where - T: ResourceTypeProvider, -{ - /// Makes a new ResourceArc from the given type. Note that the type must have - /// ResourceTypeProvider implemented for it. See module documentation for info on this. - pub fn new(data: T) -> Self { - let alloc_size = get_alloc_size_struct::(); - let mem_raw = unsafe { resource::alloc_resource(T::get_type().res, alloc_size) }; - let aligned_mem = unsafe { align_alloced_mem_for_struct::(mem_raw) as *mut T }; - - unsafe { ptr::write(aligned_mem, data) }; - - ResourceArc { - raw: mem_raw, - inner: aligned_mem, - } - } - - /// Make a resource binary associated with the given resource - /// - /// The closure `f` is called with the referenced object and must return a slice with the same - /// lifetime as the object. This means that the slice either has to be derived directly from - /// the instance or that it has to have static lifetime. - pub fn make_binary<'env, 'a, F>(&self, env: Env<'env>, f: F) -> Binary<'env> - where - F: FnOnce(&'a T) -> &'a [u8], - { - // This call is safe because `f` can only return a slice that lives at least as long as - // the given instance of `T`. - unsafe { self.make_binary_unsafe(env, f) } - } - - /// Make a resource binary without strict lifetime checking - /// - /// The user *must* ensure that the lifetime of the returned slice is at least as long as the - /// lifetime of the referenced instance. - /// - /// # Safety - /// - /// This function is only safe if the slice that is returned from the closure is guaranteed to - /// live at least as long as the `ResourceArc` instance. If in doubt, use the safe version - /// `ResourceArc::make_binary` which enforces this bound through its signature. - pub unsafe fn make_binary_unsafe<'env, 'a, 'b, F>(&self, env: Env<'env>, f: F) -> Binary<'env> - where - F: FnOnce(&'a T) -> &'b [u8], - { - let bin = f(&*self.inner); - let binary = rustler_sys::enif_make_resource_binary( - env.as_c_arg(), - self.raw, - bin.as_ptr() as *const c_void, - bin.len(), - ); - - let term = Term::new(env, binary); - Binary::from_term_and_slice(term, bin) - } - - fn from_term(term: Term) -> Result { - let res_resource = match unsafe { - resource::get_resource( - term.get_env().as_c_arg(), - term.as_c_arg(), - T::get_type().res, - ) - } { - Some(res) => res, - None => return Err(Error::BadArg), - }; - unsafe { - resource::keep_resource(res_resource); - } - let casted_ptr = unsafe { align_alloced_mem_for_struct::(res_resource) as *mut T }; - Ok(ResourceArc { - raw: res_resource, - inner: casted_ptr, - }) - } - - fn as_term<'a>(&self, env: Env<'a>) -> Term<'a> { - unsafe { Term::new(env, resource::make_resource(env.as_c_arg(), self.raw)) } - } - - fn as_c_arg(&mut self) -> *const c_void { - self.raw - } - - fn inner(&self) -> &T { - unsafe { &*self.inner } - } -} - -impl Deref for ResourceArc -where - T: ResourceTypeProvider, -{ - type Target = T; - - fn deref(&self) -> &T { - self.inner() - } -} - -impl Clone for ResourceArc -where - T: ResourceTypeProvider, -{ - /// Cloning a `ResourceArc` simply increments the reference count for the - /// resource. The `T` value is not cloned. - fn clone(&self) -> Self { - unsafe { - resource::keep_resource(self.raw); - } - ResourceArc { - raw: self.raw, - inner: self.inner, - } - } -} - -impl Drop for ResourceArc -where - T: ResourceTypeProvider, -{ - /// When a `ResourceArc` is dropped, the reference count is decremented. If - /// there are no other references to the resource, the `T` value is dropped. - /// - /// However, note that in general, the Rust value in a resource is dropped - /// at an unpredictable time: whenever the VM decides to do garbage - /// collection. - fn drop(&mut self) { - unsafe { rustler_sys::enif_release_resource(self.as_c_arg()) }; - } -} - -#[macro_export] -macro_rules! resource { - ($struct_name:ty, $env: ident) => { - { - static mut STRUCT_TYPE: Option<$crate::resource::ResourceType<$struct_name>> = None; - - let temp_struct_type = - match $crate::resource::open_struct_resource_type::<$struct_name>( - $env, - concat!(stringify!($struct_name), "\x00"), - $crate::resource::NIF_RESOURCE_FLAGS::ERL_NIF_RT_CREATE - ) { - Some(inner) => inner, - None => { - println!("Failure in creating resource type"); - return false; - } - }; - unsafe { STRUCT_TYPE = Some(temp_struct_type) }; - - impl $crate::resource::ResourceTypeProvider for $struct_name { - fn get_type() -> &'static $crate::resource::ResourceType { - unsafe { &STRUCT_TYPE }.as_ref() - .expect("The resource type hasn't been initialized. Did you remember to call the function where you used the `resource!` macro?") - } - } - } - } -} diff --git a/rustler/src/resource/arc.rs b/rustler/src/resource/arc.rs new file mode 100644 index 00000000..b935c3fd --- /dev/null +++ b/rustler/src/resource/arc.rs @@ -0,0 +1,254 @@ +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::ptr; + +use rustler_sys::{c_void, ErlNifEnv}; + +use crate::thread::is_scheduler_thread; +use crate::{Binary, Decoder, Encoder, Env, Error, LocalPid, Monitor, NifResult, OwnedEnv, Term}; + +use super::traits::{Resource, ResourceExt}; +use super::util::{align_alloced_mem_for_struct, get_alloc_size_struct}; + +/// A reference to a resource of type `T`. +/// +/// This type is like `std::sync::Arc`: it provides thread-safe, reference-counted storage for Rust +/// data that can be shared across threads. Data stored this way is immutable by default. If you +/// need to modify data in a resource, use a `std::sync::Mutex` or `RwLock`. +/// +/// Rust code and Erlang code can both have references to the same resource at the same time. Rust +/// code uses `ResourceArc`; in Erlang, a reference to a resource is a kind of term. You can +/// convert back and forth between the two using `Encoder` and `Decoder`. +pub struct ResourceArc +where + T: Resource, +{ + raw: *const c_void, + inner: *mut T, +} + +// Safe because T is `Sync` and `Send`. +unsafe impl Send for ResourceArc where T: Resource {} +unsafe impl Sync for ResourceArc where T: Resource {} + +impl ResourceArc +where + T: Resource, +{ + /// Makes a new ResourceArc from the given type. Note that the type must have Resource + /// implemented for it. See module documentation for info on this. + pub fn new(data: T) -> Self { + let alloc_size = get_alloc_size_struct::(); + let resource_type = T::get_resource_type().unwrap(); + let mem_raw = unsafe { rustler_sys::enif_alloc_resource(resource_type, alloc_size) }; + let aligned_mem = unsafe { align_alloced_mem_for_struct::(mem_raw) as *mut T }; + + unsafe { ptr::write(aligned_mem, data) }; + + ResourceArc { + raw: mem_raw, + inner: aligned_mem, + } + } + + /// Make a resource binary associated with the given resource + /// + /// The closure `f` is called with the referenced object and must return a slice with the same + /// lifetime as the object. This means that the slice either has to be derived directly from + /// the instance or that it has to have static lifetime. + pub fn make_binary<'env, 'a, F>(&self, env: Env<'env>, f: F) -> Binary<'env> + where + F: FnOnce(&'a T) -> &'a [u8], + { + // This call is safe because `f` can only return a slice that lives at least as long as + // the given instance of `T`. + unsafe { self.make_binary_unsafe(env, f) } + } + + /// Make a resource binary without strict lifetime checking + /// + /// The user *must* ensure that the lifetime of the returned slice is at least as long as the + /// lifetime of the referenced instance. + /// + /// # Safety + /// + /// This function is only safe if the slice that is returned from the closure is guaranteed to + /// live at least as long as the `ResourceArc` instance. If in doubt, use the safe version + /// `ResourceArc::make_binary` which enforces this bound through its signature. + pub unsafe fn make_binary_unsafe<'env, 'a, 'b, F>(&self, env: Env<'env>, f: F) -> Binary<'env> + where + F: FnOnce(&'a T) -> &'b [u8], + { + let bin = f(&*self.inner); + let binary = rustler_sys::enif_make_resource_binary( + env.as_c_arg(), + self.raw, + bin.as_ptr() as *const c_void, + bin.len(), + ); + + let term = Term::new(env, binary); + Binary::from_term_and_slice(term, bin) + } + + fn from_term(term: Term) -> Result { + let (raw, inner) = unsafe { term.try_get_resource_ptrs::() }.ok_or(Error::BadArg)?; + unsafe { rustler_sys::enif_keep_resource(raw) }; + Ok(ResourceArc { raw, inner }) + } + + fn as_term<'a>(&self, env: Env<'a>) -> Term<'a> { + unsafe { + Term::new( + env, + rustler_sys::enif_make_resource(env.as_c_arg(), self.raw), + ) + } + } + + fn as_c_arg(&mut self) -> *const c_void { + self.raw + } + + fn inner(&self) -> &T { + unsafe { &*self.inner } + } +} + +impl ResourceArc +where + T: Resource, +{ + pub fn monitor(&self, caller_env: Option, pid: &LocalPid) -> Option { + if !T::IMPLEMENTS_DOWN { + return None; + } + + let env = maybe_env(caller_env); + let mut mon = MaybeUninit::uninit(); + let res = unsafe { + rustler_sys::enif_monitor_process(env, self.raw, pid.as_c_arg(), mon.as_mut_ptr()) == 0 + }; + if res { + Some(unsafe { Monitor::new(mon.assume_init()) }) + } else { + None + } + } + + pub fn demonitor(&self, caller_env: Option, mon: &Monitor) -> bool { + if !T::IMPLEMENTS_DOWN { + return false; + } + + let env = maybe_env(caller_env); + unsafe { rustler_sys::enif_demonitor_process(env, self.raw, mon.as_c_arg()) == 0 } + } +} + +impl OwnedEnv { + pub fn monitor( + &self, + resource: &ResourceArc, + pid: &LocalPid, + ) -> Option { + resource.monitor(None, pid) + } + + pub fn demonitor(&self, resource: &ResourceArc, mon: &Monitor) -> bool { + resource.demonitor(None, mon) + } +} + +impl<'a> Env<'a> { + pub fn monitor( + &self, + resource: &ResourceArc, + pid: &LocalPid, + ) -> Option { + resource.monitor(Some(*self), pid) + } + + pub fn demonitor(&self, resource: &ResourceArc, mon: &Monitor) -> bool { + resource.demonitor(Some(*self), mon) + } +} + +impl Deref for ResourceArc +where + T: Resource, +{ + type Target = T; + + fn deref(&self) -> &T { + self.inner() + } +} + +impl Clone for ResourceArc +where + T: Resource, +{ + /// Cloning a `ResourceArc` simply increments the reference count for the + /// resource. The `T` value is not cloned. + fn clone(&self) -> Self { + unsafe { rustler_sys::enif_keep_resource(self.raw) }; + ResourceArc { + raw: self.raw, + inner: self.inner, + } + } +} + +impl Drop for ResourceArc +where + T: Resource, +{ + /// When a `ResourceArc` is dropped, the reference count is decremented. If + /// there are no other references to the resource, the `T` value is dropped. + /// + /// However, note that in general, the Rust value in a resource is dropped + /// at an unpredictable time: whenever the VM decides to do garbage + /// collection. + fn drop(&mut self) { + unsafe { rustler_sys::enif_release_resource(self.as_c_arg()) }; + } +} + +impl From for ResourceArc { + fn from(value: T) -> Self { + Self::new(value) + } +} + +impl Encoder for ResourceArc +where + T: Resource, +{ + fn encode<'a>(&self, env: Env<'a>) -> Term<'a> { + self.as_term(env) + } +} +impl<'a, T> Decoder<'a> for ResourceArc +where + T: Resource + 'a, +{ + fn decode(term: Term<'a>) -> NifResult { + ResourceArc::from_term(term) + } +} + +fn maybe_env(env: Option) -> *mut ErlNifEnv { + if is_scheduler_thread() { + let env = env.expect("Env required when calling from a scheduler thread"); + // Panic if `env` is not the environment of the calling process. + env.pid(); + env.as_c_arg() + } else { + assert!( + env.is_none(), + "Env provided when not calling from a scheduler thread" + ); + ptr::null_mut() + } +} diff --git a/rustler/src/resource/error.rs b/rustler/src/resource/error.rs new file mode 100644 index 00000000..e686aa50 --- /dev/null +++ b/rustler/src/resource/error.rs @@ -0,0 +1,3 @@ +/// Indicates that a resource has not been registered successfully +#[derive(Clone, Copy, Debug)] +pub struct ResourceInitError; diff --git a/rustler/src/resource/mod.rs b/rustler/src/resource/mod.rs new file mode 100644 index 00000000..5fa9fa29 --- /dev/null +++ b/rustler/src/resource/mod.rs @@ -0,0 +1,27 @@ +//! Support for storing Rust data in Erlang terms. +//! +//! A NIF resource allows you to safely store Rust structs in a term, and therefore keep it across +//! NIF calls. The struct will be automatically dropped when the BEAM GC decides that there are no +//! more references to the resource. + +mod arc; +mod error; +mod monitor; +mod registration; +mod term; +mod traits; +mod util; + +pub use arc::ResourceArc; +pub use error::ResourceInitError; +pub use monitor::Monitor; +pub use traits::Resource; +use traits::ResourceExt; + +#[macro_export] +macro_rules! resource { + ($struct_name:ty, $env: ident) => {{ + impl $crate::Resource for $struct_name {} + $env.register::<$struct_name>().is_ok() + }}; +} diff --git a/rustler/src/resource/monitor.rs b/rustler/src/resource/monitor.rs new file mode 100644 index 00000000..c0cdfb25 --- /dev/null +++ b/rustler/src/resource/monitor.rs @@ -0,0 +1,30 @@ +use rustler_sys::ErlNifMonitor; + +/// Handle for a monitor created using `ResourceArc::monitor`. +/// +/// A monitor handle can be compared to other monitor handles. It is opaque and freely copyable. +/// The monitor will not become inactive if this object is dropped. +#[derive(Copy, Clone)] +pub struct Monitor { + inner: ErlNifMonitor, +} + +impl Monitor { + pub unsafe fn new(inner: ErlNifMonitor) -> Self { + Self { inner } + } + + pub fn as_c_arg(&self) -> &ErlNifMonitor { + &self.inner + } + + pub fn from_c_arg(erl_nif_mon: ErlNifMonitor) -> Self { + Monitor { inner: erl_nif_mon } + } +} + +impl PartialEq for Monitor { + fn eq(&self, other: &Self) -> bool { + unsafe { rustler_sys::enif_compare_monitors(&self.inner, &other.inner) == 0 } + } +} diff --git a/rustler/src/resource/registration.rs b/rustler/src/resource/registration.rs new file mode 100644 index 00000000..c06db5e4 --- /dev/null +++ b/rustler/src/resource/registration.rs @@ -0,0 +1,171 @@ +use super::traits; +use super::util::align_alloced_mem_for_struct; +use super::ResourceInitError; +use crate::{Env, LocalPid, Monitor, Resource}; +use rustler_sys::ErlNifResourceDtor; +use rustler_sys::{ + c_char, c_void, ErlNifEnv, ErlNifMonitor, ErlNifPid, ErlNifResourceDown, ErlNifResourceFlags, + ErlNifResourceType, ErlNifResourceTypeInit, +}; +use std::any::TypeId; +use std::ffi::CString; +use std::mem::MaybeUninit; +use std::ptr; + +#[derive(Debug)] +struct Registration { + get_type_id: fn() -> TypeId, + get_type_name: fn() -> &'static str, + init: ErlNifResourceTypeInit, +} + +impl<'a> Env<'a> { + /// Register a resource type, see `Registration::register`. + pub fn register(&self) -> Result<(), ResourceInitError> { + Registration::new::().register(*self) + } +} + +/// Resource registration +/// +/// The type name is derived using Rust's `std::any::type_name` and the callbacks are registered +/// according to the `IMPLEMENTS_...` associated constants on the `Resource` trait implementation. +/// A destructor is always registered if the type requires explicit `drop`ping (checked using +/// `std::mem::needs_drop`). All other callbacks are only registered if `IMPLEMENTS_...` is set to +/// `true`. +impl Registration { + /// Generate a new (pending) resource type registration. + pub const fn new() -> Self { + Self { + init: ErlNifResourceTypeInit { + dtor: ptr::null(), + stop: ptr::null(), + down: ptr::null(), + members: 0, + dyncall: ptr::null(), + }, + get_type_name: std::any::type_name::, + get_type_id: TypeId::of::, + } + .maybe_add_destructor_callback::() + .maybe_add_down_callback::() + } + + const fn maybe_add_destructor_callback(self) -> Self { + if T::IMPLEMENTS_DESTRUCTOR || std::mem::needs_drop::() { + Self { + init: ErlNifResourceTypeInit { + dtor: resource_destructor:: as *const ErlNifResourceDtor, + members: max(self.init.members, 1), + ..self.init + }, + ..self + } + } else { + self + } + } + + const fn maybe_add_down_callback(self) -> Self { + if T::IMPLEMENTS_DOWN { + Self { + init: ErlNifResourceTypeInit { + down: resource_down:: as *const ErlNifResourceDown, + members: max(self.init.members, 3), + ..self.init + }, + ..self + } + } else { + self + } + } + + /// Try to register the resource type for which this registration was created. This function + /// will only succeed when called from the `load` callback and if this type has not yet been + /// registered. + pub fn register(&self, env: Env) -> Result<(), ResourceInitError> { + if !env.init { + return Err(ResourceInitError); + } + + let type_id = (self.get_type_id)(); + let type_name = (self.get_type_name)(); + + let res: Option<*const ErlNifResourceType> = unsafe { + open_resource_type( + env.as_c_arg(), + CString::new(type_name).unwrap().as_bytes_with_nul(), + self.init, + ErlNifResourceFlags::ERL_NIF_RT_CREATE, + ) + }; + if let Some(ptr) = res { + unsafe { traits::register_resource_type(type_id, ptr) }; + Ok(()) + } else { + Err(ResourceInitError) + } + } +} + +/// Drop a T that lives in an Erlang resource +unsafe extern "C" fn resource_destructor(_env: *mut ErlNifEnv, handle: *mut c_void) +where + T: Resource, +{ + let env = Env::new(&_env, _env); + let aligned = align_alloced_mem_for_struct::(handle); + // Destructor takes ownership, thus the resource object will be dropped after the function has + // run. + let obj = ptr::read::(aligned as *mut T); + if T::IMPLEMENTS_DESTRUCTOR { + obj.destructor(env); + } +} + +unsafe extern "C" fn resource_down( + env: *mut ErlNifEnv, + obj: *mut c_void, + pid: *const ErlNifPid, + mon: *const ErlNifMonitor, +) { + let env = Env::new(&env, env); + let aligned = align_alloced_mem_for_struct::(obj); + let res = &*(aligned as *const T); + let pid = LocalPid::from_c_arg(*pid); + let mon = Monitor::from_c_arg(*mon); + + res.down(env, pid, mon); +} + +pub unsafe fn open_resource_type( + env: *mut ErlNifEnv, + name: &[u8], + init: ErlNifResourceTypeInit, + flags: ErlNifResourceFlags, +) -> Option<*const ErlNifResourceType> { + // Panic if name is not null-terminated. + assert_eq!(name.last().cloned(), Some(0u8)); + + let name_p = name.as_ptr() as *const c_char; + + let res = { + let mut tried = MaybeUninit::uninit(); + rustler_sys::enif_open_resource_type_x(env, name_p, &init, flags, tried.as_mut_ptr()) + }; + + if res.is_null() { + None + } else { + Some(res) + } +} + +const fn max(i: i32, j: i32) -> i32 { + if i > j { + i + } else { + j + } +} diff --git a/rustler/src/resource/term.rs b/rustler/src/resource/term.rs new file mode 100644 index 00000000..040174d8 --- /dev/null +++ b/rustler/src/resource/term.rs @@ -0,0 +1,43 @@ +use super::util::align_alloced_mem_for_struct; +use super::{Resource, ResourceExt}; +use crate::{Decoder, Error, NifResult, Term}; +use rustler_sys::c_void; +use std::mem::MaybeUninit; + +impl<'a> Term<'a> { + /// Internal method to retrieve both the "real" resource pointer as well as a pointer to the + /// `T`-aligned region. + pub(super) unsafe fn try_get_resource_ptrs( + &self, + ) -> Option<(*const c_void, *mut T)> { + let typ = T::get_resource_type()?; + let mut ret_obj = MaybeUninit::uninit(); + let res = rustler_sys::enif_get_resource( + self.get_env().as_c_arg(), + self.as_c_arg(), + typ, + ret_obj.as_mut_ptr(), + ); + + if res == 0 { + None + } else { + let res = ret_obj.assume_init(); + Some((res, align_alloced_mem_for_struct::(res) as *mut T)) + } + } + + /// Try to retrieve a reference to a resource object of type `T` from this term. + pub fn try_get_resource(&self) -> Option<&'a T> { + unsafe { self.try_get_resource_ptrs().map(|(_, ptr)| &*ptr) } + } +} + +impl<'a, T> Decoder<'a> for &'a T +where + T: Resource + 'a, +{ + fn decode(term: Term<'a>) -> NifResult { + term.try_get_resource().ok_or(Error::BadArg) + } +} diff --git a/rustler/src/resource/traits.rs b/rustler/src/resource/traits.rs new file mode 100644 index 00000000..feacdfbf --- /dev/null +++ b/rustler/src/resource/traits.rs @@ -0,0 +1,66 @@ +use std::any::TypeId; +use std::collections::HashMap; +use std::sync::OnceLock; + +use crate::{Env, LocalPid, Monitor}; + +type NifResourcePtr = *const rustler_sys::ErlNifResourceType; + +/// Map from `TypeId` to the `NifResourcePtr`. To be able to store this in a `OnceLock`, the +/// pointer is type-erased and stored as a `usize`. +static mut RESOURCE_TYPES: OnceLock> = OnceLock::new(); + +/// Register an Erlang resource type handle for a particular type given by its `TypeId` +pub(crate) unsafe fn register_resource_type(type_id: TypeId, resource_type: NifResourcePtr) { + RESOURCE_TYPES.get_or_init(Default::default); + RESOURCE_TYPES + .get_mut() + .unwrap() + .insert(type_id, resource_type as usize); +} + +/// Trait that needs to be implemented to use a type as a NIF resource type. +/// +/// The Erlang runtime provides the following guarantees: +/// - An object will be valid as long as the associated environment is valid +/// - `destructor` is the last function that is run on an object before it is freed +/// +/// In particular, the type needs to handle all synchronization itself (thus we require it to +/// implement `Sync`) and callbacks or NIFs can run on arbitrary threads (thus we require `Send`). +/// +/// Currently only `destructor` and `down` callbacks are possible. If a callback is implemented, +/// the respective associated constant `IMPLEMENTS_...` must be set to `true` for the registration +/// to take it into account. All callbacks provide (empty) default implementations. +pub trait Resource: Sized + Send + Sync + 'static { + const IMPLEMENTS_DESTRUCTOR: bool = false; + const IMPLEMENTS_DOWN: bool = false; + + /// Callback function that is executed right before dropping a resource object. + /// + /// This callback does not have to be implemented to release associated resources or run + /// constructors. For that it is enough to implement `Drop` or rely on the generated `Drop` + /// implementation which will be called in any case. The function is useful when the cleanup + /// requires access to a NIF env, e.g. to send messages. + #[allow(unused_mut, unused)] + fn destructor(mut self, env: Env<'_>) {} + + /// Callback function to handle process monitoring. + /// + /// This callback is called when a process monitored using `Env::monitor` terminates + /// and receives the `pid` of that process as well as the `Monitor` instance that was returned + /// by `ResourceArc::monitor`. + #[allow(unused)] + fn down<'a>(&'a self, env: Env<'a>, pid: LocalPid, monitor: Monitor) {} +} + +#[doc(hidden)] +pub(crate) trait ResourceExt: 'static { + /// Get the NIF resource type handle for this type if it had been registered before + fn get_resource_type() -> Option { + let map = unsafe { RESOURCE_TYPES.get()? }; + map.get(&TypeId::of::()) + .map(|ptr| *ptr as NifResourcePtr) + } +} + +impl ResourceExt for T {} diff --git a/rustler/src/resource/util.rs b/rustler/src/resource/util.rs new file mode 100644 index 00000000..230d127d --- /dev/null +++ b/rustler/src/resource/util.rs @@ -0,0 +1,14 @@ +use rustler_sys::c_void; +use std::mem; + +pub fn get_alloc_size_struct() -> usize { + mem::size_of::() + mem::align_of::() +} + +/// Given a pointer `ptr` to an allocation of `get_alloc_size_struct::()` bytes, return the +/// first aligned pointer within the allocation where a `T` may be stored. +/// Unsafe: `ptr` must point to a large enough allocation and not be null. +pub unsafe fn align_alloced_mem_for_struct(ptr: *const c_void) -> *const c_void { + let offset = mem::align_of::() - ((ptr as usize) % mem::align_of::()); + ptr.add(offset) +} diff --git a/rustler/src/thread.rs b/rustler/src/thread.rs index 0bc9c8b8..0c2ea2eb 100644 --- a/rustler/src/thread.rs +++ b/rustler/src/thread.rs @@ -61,3 +61,10 @@ where }); }); } + +/// Check if the currently running thread is managed by the ERTS. +/// +/// This is relevant for (e.g.) `enif_send` or `enif_monitor_process` as +pub fn is_scheduler_thread() -> bool { + unsafe { rustler_sys::enif_thread_type() > 0 } +} diff --git a/rustler/src/wrapper.rs b/rustler/src/wrapper.rs index 4efac11d..1e5f8cda 100644 --- a/rustler/src/wrapper.rs +++ b/rustler/src/wrapper.rs @@ -15,7 +15,6 @@ pub mod exception; pub mod list; pub mod map; pub mod pid; -pub mod resource; pub mod term; pub mod tuple; @@ -37,20 +36,12 @@ pub fn get_nif_resource_type_init_size() -> usize { std::mem::size_of::() } -pub type NIF_RESOURCE_HANDLE = *const c_void; -pub type MUTABLE_NIF_RESOURCE_HANDLE = *mut c_void; - -pub type NifResourceDtor = - unsafe extern "C" fn(r_env: NIF_ENV, obj: MUTABLE_NIF_RESOURCE_HANDLE) -> (); -pub type NifResourceFlags = rustler_sys::ErlNifResourceFlags; - pub enum NIF_ERROR { BAD_ARG, } pub type DEF_NIF_FUNC = rustler_sys::ErlNifFunc; pub type DEF_NIF_ENTRY = rustler_sys::ErlNifEntry; -pub use rustler_sys::ErlNifResourceFlags as NIF_RESOURCE_FLAGS; pub use rustler_sys::NIF_MAJOR_VERSION; pub use rustler_sys::NIF_MINOR_VERSION; diff --git a/rustler/src/wrapper/resource.rs b/rustler/src/wrapper/resource.rs deleted file mode 100644 index 8b9cdad4..00000000 --- a/rustler/src/wrapper/resource.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::wrapper::{ - NifResourceDtor, NifResourceFlags, NIF_ENV, NIF_RESOURCE_HANDLE, NIF_RESOURCE_TYPE, NIF_TERM, -}; - -use rustler_sys::c_char; -pub use rustler_sys::{ - enif_alloc_resource as alloc_resource, enif_keep_resource as keep_resource, - enif_make_resource as make_resource, enif_release_resource as release_resource, -}; - -use std::mem::MaybeUninit; -use std::ptr; - -pub unsafe fn open_resource_type( - env: NIF_ENV, - name: &[u8], - dtor: Option, - flags: NifResourceFlags, -) -> Option { - // Panic if name is not null-terminated. - assert_eq!(name.last().cloned(), Some(0u8)); - - // Currently unused as per erlang nif documentation - let module_p: *const c_char = ptr::null(); - let name_p = name.as_ptr() as *const c_char; - let res = { - let mut tried = MaybeUninit::uninit(); - rustler_sys::enif_open_resource_type(env, module_p, name_p, dtor, flags, tried.as_mut_ptr()) - }; - - if res.is_null() { - None - } else { - Some(res) - } -} - -// Functionally incomplete -pub unsafe fn get_resource( - env: NIF_ENV, - term: NIF_TERM, - typ: NIF_RESOURCE_TYPE, -) -> Option { - let mut ret_obj = MaybeUninit::uninit(); - let res = rustler_sys::enif_get_resource(env, term, typ, ret_obj.as_mut_ptr()); - - if res == 0 { - None - } else { - Some(ret_obj.assume_init()) - } -} diff --git a/rustler_codegen/Cargo.toml b/rustler_codegen/Cargo.toml index e7d38940..b440609b 100644 --- a/rustler_codegen/Cargo.toml +++ b/rustler_codegen/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [lib] name = "rustler_codegen" -proc_macro = true +proc-macro = true [dependencies] syn = { version = "2.0", features = ["full", "extra-traits"] } diff --git a/rustler_codegen/src/init.rs b/rustler_codegen/src/init.rs index 5bd15c35..6d11ca5e 100644 --- a/rustler_codegen/src/init.rs +++ b/rustler_codegen/src/init.rs @@ -103,8 +103,14 @@ impl From for proc_macro2::TokenStream { load_info: rustler::codegen_runtime::NIF_TERM ) -> rustler::codegen_runtime::c_int { unsafe { + let mut env = rustler::Env::new_init_env(&env, env); // TODO: If an unwrap ever happens, we will unwind right into C! Fix this! - rustler::codegen_runtime::handle_nif_init_call(#load, env, load_info) + let load_info = rustler::Term::new(env, load_info); + #load.map_or(0, |inner| { + rustler::codegen_runtime::handle_nif_init_call( + inner, env, load_info + ) + }) } } Some(nif_load) diff --git a/rustler_sys/src/rustler_sys_api.rs b/rustler_sys/src/rustler_sys_api.rs index 1d7f3473..67086122 100644 --- a/rustler_sys/src/rustler_sys_api.rs +++ b/rustler_sys/src/rustler_sys_api.rs @@ -147,11 +147,11 @@ pub type ErlNifResourceDynCall = #[derive(Debug, Copy, Clone)] #[repr(C)] pub struct ErlNifResourceTypeInit { - dtor: *const ErlNifResourceDtor, - stop: *const ErlNifResourceStop, // at ERL_NIF_SELECT_STOP event - down: *const ErlNifResourceDown, // enif_monitor_process - members: c_int, - dyncall: *const ErlNifResourceDynCall, + pub dtor: *const ErlNifResourceDtor, + pub stop: *const ErlNifResourceStop, // at ERL_NIF_SELECT_STOP event + pub down: *const ErlNifResourceDown, // enif_monitor_process + pub members: c_int, + pub dyncall: *const ErlNifResourceDynCall, } /// See [ErlNifSelectFlags](http://erlang.org/doc/man/erl_nif.html#ErlNifSelectFlags) in the Erlang docs. diff --git a/rustler_tests/lib/rustler_test.ex b/rustler_tests/lib/rustler_test.ex index 56558d75..77ca2790 100644 --- a/rustler_tests/lib/rustler_test.ex +++ b/rustler_tests/lib/rustler_test.ex @@ -67,6 +67,11 @@ defmodule RustlerTest do def resource_make_immutable(_), do: err() def resource_immutable_count(), do: err() + def monitor_resource_make(), do: err() + def monitor_resource_monitor(_, _), do: err() + def monitor_resource_down_called(_), do: err() + def monitor_resource_demonitor(_), do: err() + def resource_make_with_binaries(), do: err() def resource_make_binaries(_), do: err() diff --git a/rustler_tests/native/rustler_test/src/lib.rs b/rustler_tests/native/rustler_test/src/lib.rs index 0c151dbc..73250eb3 100644 --- a/rustler_tests/native/rustler_test/src/lib.rs +++ b/rustler_tests/native/rustler_test/src/lib.rs @@ -20,6 +20,5 @@ mod test_tuple; rustler::init!("Elixir.RustlerTest", [deprecated, usage], load = load); fn load(env: rustler::Env, _: rustler::Term) -> bool { - test_resource::on_load(env); - true + test_resource::on_load(env) } diff --git a/rustler_tests/native/rustler_test/src/test_resource.rs b/rustler_tests/native/rustler_test/src/test_resource.rs index a2e57d34..2ede9954 100644 --- a/rustler_tests/native/rustler_test/src/test_resource.rs +++ b/rustler_tests/native/rustler_test/src/test_resource.rs @@ -1,27 +1,49 @@ -use rustler::{Binary, Env, ResourceArc}; -use std::sync::{OnceLock, RwLock}; +use rustler::{Binary, Env, LocalPid, Monitor, Resource, ResourceArc}; +use std::sync::{Mutex, OnceLock, RwLock}; pub struct TestResource { test_field: RwLock, } +struct TestMonitorResourceInner { + mon: Option, + down_called: bool, +} + +pub struct TestMonitorResource { + inner: Mutex, +} + +impl Resource for TestMonitorResource { + const IMPLEMENTS_DOWN: bool = true; + + fn down<'a>(&'a self, _env: Env<'a>, _pid: LocalPid, mon: Monitor) { + let mut inner = self.inner.lock().unwrap(); + assert!(Some(mon) == inner.mon); + inner.down_called = true; + } +} + /// This one is designed to look more like pointer data, to increase the /// chance of segfaults if the implementation is wrong. +#[derive(Debug)] pub struct ImmutableResource { a: u32, b: u32, } +impl Resource for ImmutableResource {} + pub struct WithBinaries { a: [u8; 10], b: Vec, } pub fn on_load(env: Env) -> bool { - rustler::resource!(TestResource, env); - rustler::resource!(ImmutableResource, env); - rustler::resource!(WithBinaries, env); - true + rustler::resource!(TestResource, env) + && env.register::().is_ok() + && env.register::().is_ok() + && rustler::resource!(WithBinaries, env) } #[rustler::nif] @@ -82,12 +104,24 @@ pub fn resource_immutable_count() -> u32 { get_count().load(Ordering::SeqCst) as u32 } +#[rustler::nif] +pub fn monitor_resource_make() -> ResourceArc { + TestMonitorResource { + inner: Mutex::new(TestMonitorResourceInner { + mon: None, + down_called: false, + }), + } + .into() +} + #[rustler::nif] pub fn resource_make_with_binaries() -> ResourceArc { - ResourceArc::new(WithBinaries { + WithBinaries { a: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], b: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - }) + } + .into() } #[rustler::nif] @@ -108,3 +142,26 @@ pub fn resource_make_binaries( resource.make_binary(env, |_| get_static_bin()), ) } + +#[rustler::nif] +pub fn monitor_resource_monitor( + env: Env, + resource: ResourceArc, + pid: LocalPid, +) { + let mut inner = resource.inner.lock().unwrap(); + inner.mon = env.monitor(&resource, &pid); + assert!(inner.mon.is_some()); + inner.down_called = false; +} + +#[rustler::nif] +pub fn monitor_resource_down_called(resource: ResourceArc) -> bool { + resource.inner.lock().unwrap().down_called +} + +#[rustler::nif] +pub fn monitor_resource_demonitor(env: Env, resource: ResourceArc) -> bool { + let inner = resource.inner.lock().unwrap(); + env.demonitor(&resource, inner.mon.as_ref().unwrap()) +} diff --git a/rustler_tests/test/resource_test.exs b/rustler_tests/test/resource_test.exs index c05db688..80c924fa 100644 --- a/rustler_tests/test/resource_test.exs +++ b/rustler_tests/test/resource_test.exs @@ -44,4 +44,39 @@ defmodule RustlerTest.ResourceTest do assert slice == vec assert vec == static end + + test "monitor resource" do + resource = RustlerTest.monitor_resource_make() + parent = self() + + spawn(fn -> + RustlerTest.monitor_resource_monitor(resource, self()) + send(parent, :done) + end) + + receive do + :done -> :ok + end + + :timer.sleep(10) + assert RustlerTest.monitor_resource_down_called(resource) == true + end + + test "monitor resource demonitor" do + resource = RustlerTest.monitor_resource_make() + parent = self() + + spawn(fn -> + RustlerTest.monitor_resource_monitor(resource, self()) + RustlerTest.monitor_resource_demonitor(resource) + send(parent, :done) + end) + + receive do + :done -> :ok + end + + :timer.sleep(10) + assert RustlerTest.monitor_resource_down_called(resource) == false + end end