Skip to content

Commit

Permalink
Merge pull request #635 from rusterlium/dyncall
Browse files Browse the repository at this point in the history
Add unsafe dyncall support
  • Loading branch information
filmor authored Aug 5, 2024
2 parents d7e8e53 + d77b6fd commit 73357df
Show file tree
Hide file tree
Showing 16 changed files with 229 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
4 changes: 4 additions & 0 deletions rustler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ pub mod serde;

#[cfg(feature = "serde")]
pub use crate::serde::SerdeTerm;

pub mod sys {
pub use rustler_sys::*;
}
23 changes: 23 additions & 0 deletions rustler/src/resource/arc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,29 @@ impl<'a> Env<'a> {
pub fn demonitor<T: Resource>(&self, resource: &ResourceArc<T>, 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<T> Deref for ResourceArc<T>
Expand Down
5 changes: 5 additions & 0 deletions rustler/src/resource/error.rs
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion rustler/src/resource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 51 additions & 1 deletion rustler/src/resource/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ impl Registration {
}
.maybe_add_destructor_callback::<T>()
.maybe_add_down_callback::<T>()
.maybe_add_dyncall_callback::<T>()
}

pub const fn with_name(self, name: &'static str) -> Self {
Expand Down Expand Up @@ -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<T: Resource>(self) -> Self {
self
}

#[cfg(feature = "nif_version_2_16")]
const fn maybe_add_dyncall_callback<T: Resource>(self) -> Self {
if T::IMPLEMENTS_DYNCALL {
Self {
init: ErlNifResourceTypeInit {
dyncall: resource_dyncall::<T> 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.
Expand Down Expand Up @@ -162,6 +185,19 @@ unsafe extern "C" fn resource_down<T: Resource>(
res.down(env, pid, mon);
}

#[cfg(feature = "nif_version_2_16")]
unsafe extern "C" fn resource_dyncall<T: Resource>(
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::<T>(obj);
let res = &*(aligned as *const T);

res.dyncall(env, call_data);
}

pub unsafe fn open_resource_type(
env: *mut ErlNifEnv,
name: &[u8],
Expand All @@ -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() {
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions rustler/src/resource/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +54,10 @@ pub trait Resource: Sized + Send + Sync + 'static {
/// by `ResourceArc<T>::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)]
Expand Down
11 changes: 11 additions & 0 deletions rustler_tests/lib/resource_dyncall.ex
Original file line number Diff line number Diff line change
@@ -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
28 changes: 17 additions & 11 deletions rustler_tests/lib/rustler_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions rustler_tests/native/resource_dyncall/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
32 changes: 32 additions & 0 deletions rustler_tests/native/resource_dyncall/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use rustler::{Env, LocalPid, Resource, ResourceArc};

#[repr(C)]
struct Params {
a: i64,
b: i64,
c: i64,
sent_to: Option<LocalPid>,
}

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> {
ResourceWithDyncall { pid }.into()
}

rustler::init!("Elixir.ResourceDyncall");
2 changes: 2 additions & 0 deletions rustler_tests/native/rustler_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions rustler_tests/native/rustler_test/src/test_dyncall.rs
Original file line number Diff line number Diff line change
@@ -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<LocalPid>,
}

#[rustler::nif]
pub fn perform_dyncall<'a>(
env: Env<'a>,
resource: Term<'a>,
a: i64,
b: i64,
c: i64,
) -> Option<LocalPid> {
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
}
22 changes: 22 additions & 0 deletions rustler_tests/test/resource_dyncall_test.exs
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions rustler_tests/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Raise an error if RustlerTest can't be loaded
Code.ensure_loaded!(RustlerTest)

ExUnit.start()

defmodule SerdeRustlerTests.Helpers do
Expand Down

0 comments on commit 73357df

Please sign in to comment.