From adcecd06d1b5ad20bdb85e13a9448b98700bd6a8 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 9 Jul 2024 19:37:34 +0200 Subject: [PATCH 1/3] Add resource dyncall support --- rustler/src/resource/arc.rs | 23 ++++++++++++ rustler/src/resource/error.rs | 5 +++ rustler/src/resource/mod.rs | 2 +- rustler/src/resource/registration.rs | 52 +++++++++++++++++++++++++++- rustler/src/resource/traits.rs | 7 ++++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/rustler/src/resource/arc.rs b/rustler/src/resource/arc.rs index b935c3fd..a6827cf6 100644 --- a/rustler/src/resource/arc.rs +++ b/rustler/src/resource/arc.rs @@ -172,6 +172,29 @@ impl<'a> Env<'a> { pub fn demonitor(&self, resource: &ResourceArc, mon: &Monitor) -> bool { resource.demonitor(Some(*self), mon) } + + #[cfg(feature = "nif_version_2_16")] + pub unsafe fn dynamic_resource_call( + self, + module: crate::Atom, + name: crate::Atom, + resource: Term<'a>, + call_data: *mut rustler_sys::c_void, + ) -> Result<(), super::DynamicResourceCallError> { + let res = rustler_sys::enif_dynamic_resource_call( + self.as_c_arg(), + module.as_c_arg(), + name.as_c_arg(), + resource.as_c_arg(), + call_data, + ); + + if res == 0 { + Ok(()) + } else { + Err(super::DynamicResourceCallError) + } + } } impl Deref for ResourceArc diff --git a/rustler/src/resource/error.rs b/rustler/src/resource/error.rs index e686aa50..7fe312b5 100644 --- a/rustler/src/resource/error.rs +++ b/rustler/src/resource/error.rs @@ -1,3 +1,8 @@ /// Indicates that a resource has not been registered successfully #[derive(Clone, Copy, Debug)] pub struct ResourceInitError; + +/// Indicates that a dynamic resource call failed +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub struct DynamicResourceCallError; diff --git a/rustler/src/resource/mod.rs b/rustler/src/resource/mod.rs index 977a38b5..d8023603 100644 --- a/rustler/src/resource/mod.rs +++ b/rustler/src/resource/mod.rs @@ -13,7 +13,7 @@ mod traits; mod util; pub use arc::ResourceArc; -pub use error::ResourceInitError; +pub use error::*; pub use monitor::Monitor; pub use registration::Registration; pub use traits::Resource; diff --git a/rustler/src/resource/registration.rs b/rustler/src/resource/registration.rs index 2c761bcd..ab45bd6f 100644 --- a/rustler/src/resource/registration.rs +++ b/rustler/src/resource/registration.rs @@ -65,6 +65,7 @@ impl Registration { } .maybe_add_destructor_callback::() .maybe_add_down_callback::() + .maybe_add_dyncall_callback::() } pub const fn with_name(self, name: &'static str) -> Self { @@ -104,6 +105,28 @@ impl Registration { } } + #[cfg(not(feature = "nif_version_2_16"))] + #[allow(clippy::extra_unused_type_parameters)] + const fn maybe_add_dyncall_callback(self) -> Self { + self + } + + #[cfg(feature = "nif_version_2_16")] + const fn maybe_add_dyncall_callback(self) -> Self { + if T::IMPLEMENTS_DYNCALL { + Self { + init: ErlNifResourceTypeInit { + dyncall: resource_dyncall:: as *const rustler_sys::ErlNifResourceDynCall, + members: max(self.init.members, 4), + ..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. @@ -162,6 +185,19 @@ unsafe extern "C" fn resource_down( res.down(env, pid, mon); } +#[cfg(feature = "nif_version_2_16")] +unsafe extern "C" fn resource_dyncall( + env: *mut ErlNifEnv, + obj: *mut c_void, + call_data: *mut c_void, +) { + let env = Env::new_internal(&env, env, EnvKind::Callback); + let aligned = align_alloced_mem_for_struct::(obj); + let res = &*(aligned as *const T); + + res.dyncall(env, call_data); +} + pub unsafe fn open_resource_type( env: *mut ErlNifEnv, name: &[u8], @@ -175,7 +211,7 @@ pub unsafe fn open_resource_type( let res = { let mut tried = MaybeUninit::uninit(); - rustler_sys::enif_open_resource_type_x(env, name_p, &init, flags, tried.as_mut_ptr()) + OPEN_RESOURCE_TYPE(env, name_p, &init, flags, tried.as_mut_ptr()) }; if res.is_null() { @@ -185,6 +221,20 @@ pub unsafe fn open_resource_type( } } +type OpenResourceTypeFn = unsafe extern "C" fn( + *mut ErlNifEnv, + *const c_char, + *const ErlNifResourceTypeInit, + ErlNifResourceFlags, + *mut ErlNifResourceFlags, +) -> *const ErlNifResourceType; + +#[cfg(feature = "nif_version_2_16")] +static OPEN_RESOURCE_TYPE: OpenResourceTypeFn = rustler_sys::enif_init_resource_type; + +#[cfg(not(feature = "nif_version_2_16"))] +static OPEN_RESOURCE_TYPE: OpenResourceTypeFn = rustler_sys::enif_open_resource_type_x; + const fn max(i: i32, j: i32) -> i32 { if i > j { i diff --git a/rustler/src/resource/traits.rs b/rustler/src/resource/traits.rs index feacdfbf..b1614105 100644 --- a/rustler/src/resource/traits.rs +++ b/rustler/src/resource/traits.rs @@ -35,6 +35,9 @@ pub trait Resource: Sized + Send + Sync + 'static { const IMPLEMENTS_DESTRUCTOR: bool = false; const IMPLEMENTS_DOWN: bool = false; + #[cfg(feature = "nif_version_2_16")] + const IMPLEMENTS_DYNCALL: 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 @@ -51,6 +54,10 @@ pub trait Resource: Sized + Send + Sync + 'static { /// by `ResourceArc::monitor`. #[allow(unused)] fn down<'a>(&'a self, env: Env<'a>, pid: LocalPid, monitor: Monitor) {} + + #[cfg(feature = "nif_version_2_16")] + #[allow(unused)] + unsafe fn dyncall<'a>(&'a self, env: Env<'a>, call_data: *mut rustler_sys::c_void) {} } #[doc(hidden)] From 70c426a5607bbb27dc595c528bdeb767fa5e72d6 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 2 Aug 2024 20:54:21 +0900 Subject: [PATCH 2/3] Add a test for dyncall --- Cargo.toml | 1 + rustler/src/lib.rs | 4 ++ rustler_tests/lib/resource_dyncall.ex | 11 ++++++ rustler_tests/lib/rustler_test.ex | 28 ++++++++------ .../native/resource_dyncall/Cargo.toml | 11 ++++++ .../native/resource_dyncall/src/lib.rs | 32 ++++++++++++++++ rustler_tests/native/rustler_test/src/lib.rs | 2 + .../native/rustler_test/src/test_dyncall.rs | 37 +++++++++++++++++++ rustler_tests/test/resource_dyncall_test.exs | 22 +++++++++++ rustler_tests/test/test_helper.exs | 3 ++ 10 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 rustler_tests/lib/resource_dyncall.ex create mode 100644 rustler_tests/native/resource_dyncall/Cargo.toml create mode 100644 rustler_tests/native/resource_dyncall/src/lib.rs create mode 100644 rustler_tests/native/rustler_test/src/test_dyncall.rs create mode 100644 rustler_tests/test/resource_dyncall_test.exs diff --git a/Cargo.toml b/Cargo.toml index 6b494faa..f857be65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "rustler_tests/native/rustler_serde_test", "rustler_tests/native/dynamic_load", "rustler_tests/native/rustler_compile_tests", + "rustler_tests/native/resource_dyncall", "rustler_benchmarks/native/benchmark", ] default-members = [ diff --git a/rustler/src/lib.rs b/rustler/src/lib.rs index 005c732e..27144ce8 100644 --- a/rustler/src/lib.rs +++ b/rustler/src/lib.rs @@ -81,3 +81,7 @@ pub mod serde; #[cfg(feature = "serde")] pub use crate::serde::SerdeTerm; + +pub mod sys { + pub use rustler_sys::*; +} diff --git a/rustler_tests/lib/resource_dyncall.ex b/rustler_tests/lib/resource_dyncall.ex new file mode 100644 index 00000000..d642ad9d --- /dev/null +++ b/rustler_tests/lib/resource_dyncall.ex @@ -0,0 +1,11 @@ +if RustlerTest.Helper.has_nif_version("2.16") do + defmodule ResourceDyncall do + use Rustler, + otp_app: :rustler_test, + crate: :resource_dyncall + + def new(_), do: err() + + defp err(), do: :erlang.nif_error(:nif_not_loaded) + end +end diff --git a/rustler_tests/lib/rustler_test.ex b/rustler_tests/lib/rustler_test.ex index b09ddb22..f2c7b58c 100644 --- a/rustler_tests/lib/rustler_test.ex +++ b/rustler_tests/lib/rustler_test.ex @@ -2,22 +2,24 @@ defmodule NifNotLoadedError do defexception message: "nif not loaded" end -defmodule RustlerTest.Helper do - def nif_feature_from_running_version() do - [major, minor | _] = - :erlang.system_info(:nif_version) - |> to_string - |> String.split(".") - - "nif_version_#{major}_#{minor}" +defmodule RustlerTest do + defmodule Helper do + @nif_version Version.parse!("#{:erlang.system_info(:nif_version)}.0") + + def nif_feature_from_running_version() do + "nif_version_#{@nif_version.major}_#{@nif_version.minor}" + end + + def has_nif_version(version) do + req = Version.parse_requirement!("~> #{version}") + Version.match?(@nif_version, req) + end end -end -defmodule RustlerTest do use Rustler, otp_app: :rustler_test, crate: :rustler_test, - features: [RustlerTest.Helper.nif_feature_from_running_version()] + features: [Helper.nif_feature_from_running_version()] defp err, do: :erlang.nif_error(:nif_not_loaded) @@ -145,4 +147,8 @@ defmodule RustlerTest do def greeting_person_from_tuple(_tuple), do: err() def append_to_path(_path, _to_append), do: err() + + if Helper.has_nif_version("2.16") do + def perform_dyncall(_res, _a, _b, _c), do: err() + end end diff --git a/rustler_tests/native/resource_dyncall/Cargo.toml b/rustler_tests/native/resource_dyncall/Cargo.toml new file mode 100644 index 00000000..94ca9a67 --- /dev/null +++ b/rustler_tests/native/resource_dyncall/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "resource_dyncall" +version = "0.1.0" +edition = "2021" + +[lib] +name = "resource_dyncall" +crate-type = ["cdylib"] + +[dependencies] +rustler = { path = "../../../rustler", features = ["allocator", "nif_version_2_16"] } diff --git a/rustler_tests/native/resource_dyncall/src/lib.rs b/rustler_tests/native/resource_dyncall/src/lib.rs new file mode 100644 index 00000000..a52b8d36 --- /dev/null +++ b/rustler_tests/native/resource_dyncall/src/lib.rs @@ -0,0 +1,32 @@ +use rustler::{Env, LocalPid, Resource, ResourceArc}; + +#[repr(C)] +struct Params { + a: i64, + b: i64, + c: i64, + sent_to: Option, +} + +struct ResourceWithDyncall { + pid: LocalPid, +} + +#[rustler::resource_impl(name = "resource_with_dyncall")] +impl Resource for ResourceWithDyncall { + unsafe fn dyncall<'a>(&'a self, env: Env<'a>, call_data: *mut rustler::sys::c_void) { + let p = &mut *(call_data as *mut Params); + if let Ok(()) = env.send(&self.pid, (p.a, p.b, p.c)) { + p.sent_to = Some(self.pid); + } else { + p.sent_to = None + } + } +} + +#[rustler::nif] +fn new(pid: LocalPid) -> ResourceArc { + ResourceWithDyncall { pid }.into() +} + +rustler::init!("Elixir.ResourceDyncall"); diff --git a/rustler_tests/native/rustler_test/src/lib.rs b/rustler_tests/native/rustler_test/src/lib.rs index 73250eb3..bb44c685 100644 --- a/rustler_tests/native/rustler_test/src/lib.rs +++ b/rustler_tests/native/rustler_test/src/lib.rs @@ -2,6 +2,8 @@ mod test_atom; mod test_binary; mod test_codegen; mod test_dirty; +#[cfg(feature = "nif_version_2_16")] +mod test_dyncall; mod test_env; mod test_error; mod test_list; diff --git a/rustler_tests/native/rustler_test/src/test_dyncall.rs b/rustler_tests/native/rustler_test/src/test_dyncall.rs new file mode 100644 index 00000000..30f83998 --- /dev/null +++ b/rustler_tests/native/rustler_test/src/test_dyncall.rs @@ -0,0 +1,37 @@ +use rustler::{Env, LocalPid, Term}; + +rustler::atoms! { + module = "Elixir.ResourceDyncall", + resource_name = "resource_with_dyncall", +} + +#[repr(C)] +struct Params { + a: i64, + b: i64, + c: i64, + sent_to: Option, +} + +#[rustler::nif] +pub fn perform_dyncall<'a>( + env: Env<'a>, + resource: Term<'a>, + a: i64, + b: i64, + c: i64, +) -> Option { + let mut params = Params { + a, + b, + c, + sent_to: None, + }; + + unsafe { + let params_ptr = std::ptr::addr_of_mut!(params) as *mut rustler::sys::c_void; + let _ = env.dynamic_resource_call(module(), resource_name(), resource, params_ptr); + } + + params.sent_to +} diff --git a/rustler_tests/test/resource_dyncall_test.exs b/rustler_tests/test/resource_dyncall_test.exs new file mode 100644 index 00000000..ac2b0a18 --- /dev/null +++ b/rustler_tests/test/resource_dyncall_test.exs @@ -0,0 +1,22 @@ +if RustlerTest.Helper.has_nif_version("2.16") do + defmodule RustlerTest.ResourceDyncallTest do + use ExUnit.Case, async: true + + test "perform dyncall" do + pid = self() + + res = ResourceDyncall.new(pid) + + call_res = RustlerTest.perform_dyncall(res, 1, 2, 3) + + assert call_res == pid + + receive do + {1, 2, 3} -> true + after + 50 -> + raise "fail" + end + end + end +end diff --git a/rustler_tests/test/test_helper.exs b/rustler_tests/test/test_helper.exs index 500567d9..9ae71039 100644 --- a/rustler_tests/test/test_helper.exs +++ b/rustler_tests/test/test_helper.exs @@ -1,3 +1,6 @@ +# Raise an error if RustlerTest can't be loaded +Code.ensure_loaded!(RustlerTest) + ExUnit.start() defmodule SerdeRustlerTests.Helpers do From d77b6fdc6450e2b0056c23b1ac09cce0e063e318 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 5 Aug 2024 22:35:34 +0200 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e461373..69553a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ versions. - The resource type name can now be overridden with `#[register_impl(name = "...")]` (#638) - Floats can be decoded from integers (#641, fixes #603) +- Resource types can now implement and use dynamic calls on NIF version 2.16 + (#635) ### Fixed