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

feat: add result-returning send variants to Env and OwnedEnv #563

Merged
merged 2 commits into from
Sep 26, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ See [`UPGRADE.md`](./UPGRADE.md) for additional help when upgrading to newer ver
* Mark `use Rustler` module configuration as compile-time
* Bump Rust edition to 2021
* Make `:rustler` a compile-time-only dependency (#516, #559)
* Return `Result<(), SendError>` from all `send` functions (#239, #563)

## [0.29.1] - 2023-06-30

Expand Down
10 changes: 10 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ This document is intended to simplify upgrading to newer versions by extending t
options on `use Rustler` or configuring the module in your `config/*.exs`
files.

Additionally, `Env::send` and `OwnedEnv::send_and_clear` will now return a
`Result`. Updating will thus introduce warnings about unused `Result`s. To
remove the warnings without changing behaviour, the `Result`s can be "used" as
```rust
let _ = env.send(...)
```
Neither the `Ok` nor the `Err` case carry additional information so far. An
error is returned if either the receiving or the sending process is dead. See
also [enif\_send](https://www.erlang.org/doc/man/erl_nif.html#enif_send).

## 0.28 -> 0.29

`RUSTLER_NIF_VERSION` is deprecated and will not be considered anymore for 0.30.
Expand Down
37 changes: 29 additions & 8 deletions rustler/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl<'a, 'b> PartialEq<Env<'b>> for Env<'a> {
}
}

///
#[derive(Clone, Copy, Debug)]
pub struct SendError;

impl<'a> Env<'a> {
/// Create a new Env. For the `_lifetime_marker` argument, pass a
/// reference to any local variable that has its own lifetime, different
Expand Down Expand Up @@ -71,12 +75,15 @@ impl<'a> Env<'a> {
///
/// * The current thread is *not* managed by the Erlang VM.
///
/// The result indicates whether the send was successful, see also
/// [enif\_send](https://www.erlang.org/doc/man/erl_nif.html#enif_send).
///
/// # Panics
///
/// Panics if the above rules are broken (by trying to send a message from
/// an `OwnedEnv` on a thread that's managed by the Erlang VM).
///
pub fn send(self, pid: &LocalPid, message: Term<'a>) {
pub fn send(self, pid: &LocalPid, message: Term<'a>) -> Result<(), SendError> {
let thread_type = unsafe { rustler_sys::enif_thread_type() };
let env = if thread_type == rustler_sys::ERL_NIF_THR_UNDEFINED {
ptr::null_mut()
Expand All @@ -93,8 +100,14 @@ impl<'a> Env<'a> {
};

// Send the message.
unsafe {
rustler_sys::enif_send(env, pid.as_c_arg(), ptr::null_mut(), message.as_c_arg());
let res = unsafe {
rustler_sys::enif_send(env, pid.as_c_arg(), ptr::null_mut(), message.as_c_arg())
};

if res == 1 {
Ok(())
} else {
Err(SendError)
}
}

Expand Down Expand Up @@ -164,7 +177,7 @@ impl<'a> Env<'a> {
///
/// fn send_string_to_pid(data: &str, pid: &LocalPid) {
/// let mut msg_env = OwnedEnv::new();
/// msg_env.send_and_clear(pid, |env| data.encode(env));
/// let _ = msg_env.send_and_clear(pid, |env| data.encode(env));
/// }
///
/// There's no way to run Erlang code in an `OwnedEnv`. It's not a process. It's just a workspace
Expand Down Expand Up @@ -198,13 +211,15 @@ impl OwnedEnv {
/// The environment is cleared as though by calling the `.clear()` method.
/// To avoid that, use `env.send(pid, term)` instead.
///
/// The result is the same as what `Env::send` would return.
///
/// # Panics
///
/// Panics if called from a thread that is managed by the Erlang VM. You
/// can only use this method on a thread that was created by other
/// means. (This curious restriction is imposed by the Erlang VM.)
///
pub fn send_and_clear<F>(&mut self, recipient: &LocalPid, closure: F)
pub fn send_and_clear<F>(&mut self, recipient: &LocalPid, closure: F) -> Result<(), SendError>
where
F: for<'a> FnOnce(Env<'a>) -> Term<'a>,
{
Expand All @@ -214,11 +229,17 @@ impl OwnedEnv {

let message = self.run(|env| closure(env).as_c_arg());

unsafe {
rustler_sys::enif_send(ptr::null_mut(), recipient.as_c_arg(), *self.env, message);
}
let res = unsafe {
rustler_sys::enif_send(ptr::null_mut(), recipient.as_c_arg(), *self.env, message)
};

self.clear();

if res == 1 {
Ok(())
} else {
Err(SendError)
}
}

/// Free all terms in this environment and clear it for reuse.
Expand Down
2 changes: 1 addition & 1 deletion rustler/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ where
{
let pid = env.pid();
S::spawn(move || {
OwnedEnv::new().send_and_clear(&pid, |env| {
let _ = OwnedEnv::new().send_and_clear(&pid, |env| {
match panic::catch_unwind(|| thread_fn(env)) {
Ok(term) => term,
Err(err) => {
Expand Down
1 change: 1 addition & 0 deletions rustler_tests/lib/rustler_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ defmodule RustlerTest do
def threaded_sleep(_), do: err()

def send_all(_, _), do: err()
def send(_, _), do: err()
def whereis_pid(_), do: err()
def sublists(_), do: err()

Expand Down
1 change: 1 addition & 0 deletions rustler_tests/native/rustler_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ rustler::init!(
test_thread::threaded_fac,
test_thread::threaded_sleep,
test_env::send_all,
test_env::send,
test_env::whereis_pid,
test_env::sublists,
test_codegen::tuple_echo,
Expand Down
14 changes: 11 additions & 3 deletions rustler_tests/native/rustler_test/src/test_env.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustler::env::{OwnedEnv, SavedTerm};
use rustler::env::{OwnedEnv, SavedTerm, SendError};
use rustler::types::atom;
use rustler::types::list::ListIterator;
use rustler::types::LocalPid;
Expand All @@ -9,12 +9,20 @@ use std::thread;
#[rustler::nif]
pub fn send_all<'a>(env: Env<'a>, pids: Vec<LocalPid>, msg: Term<'a>) -> Term<'a> {
for pid in pids {
env.send(&pid, msg);
let _ = env.send(&pid, msg);
}

msg
}

#[rustler::nif]
pub fn send<'a>(env: Env<'a>, pid: LocalPid, msg: Term<'a>) -> Atom {
match env.send(&pid, msg) {
Ok(()) => atom::ok(),
Err(SendError) => atom::error(),
}
}

#[rustler::nif]
pub fn whereis_pid<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
let result = env.whereis_pid(term);
Expand Down Expand Up @@ -45,7 +53,7 @@ pub fn sublists<'a>(env: Env<'a>, list: Term<'a>) -> NifResult<Atom> {
thread::spawn(move || {
// Use `.send()` to get a `Env` from our `OwnedEnv`,
// run some rust code, and finally send the result back to `pid`.
owned_env.send_and_clear(&pid, |env| {
let _ = owned_env.send_and_clear(&pid, |env| {
let result: NifResult<Term> = (|| {
let reversed_list = saved_reversed_list.load(env);
let iter: ListIterator = reversed_list.decode()?;
Expand Down
19 changes: 19 additions & 0 deletions rustler_tests/test/env_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ defmodule RustlerTest.EnvTest do
assert nil == RustlerTest.whereis_pid("not a PID")
assert nil == RustlerTest.whereis_pid(:not_a_registered_name)
end

test "send_error" do
task =
Task.async(fn ->
receive do
:exit -> :ok
end
end)

# A send to an alive process from an alive process should not return an
# error
assert :ok == RustlerTest.send(task.pid, :msg)
assert :ok == RustlerTest.send(task.pid, :exit)
Task.await(task)

# Once the target process is down, sends should error
assert :error == RustlerTest.send(task.pid, :msg)
assert :error == RustlerTest.send(task.pid, :msg)
end
end
Loading