Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unsafe dyncall support #635

Merged
merged 3 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading