rustler_elixir_fun/
lib.rs

1use rustler::*;
2use rustler::types::LocalPid;
3use rustler::types::tuple::make_tuple;
4use rustler::error::Error;
5use rustler_sys;
6use std::mem::MaybeUninit;
7use rustler::wrapper::ErlNifPid;
8use std::sync::{Mutex, Condvar};
9use std::time::Duration;
10use rustler_stored_term::StoredTerm;
11use rustler_stored_term::StoredTerm::{AnAtom, Tuple};
12
13use crate::ElixirFunCallResult::*;
14
15pub struct ManualFuture {
16    mutex: Mutex<Option<StoredTerm>>,
17    cond: Condvar,
18}
19
20impl ManualFuture {
21    pub fn new() -> ManualFuture {
22        ManualFuture {mutex: Mutex::new(None), cond: Condvar::new()}
23    }
24
25    pub fn wait_until_filled(& self, timeout: Duration) -> Option<StoredTerm> {
26        let (mut guard, wait_timeout_result) = self.cond.wait_timeout_while(
27            self.mutex.lock().unwrap(),
28            timeout,
29            |pending| { pending.is_none() }
30        ).expect("ManualFuture's Mutex was unexpectedly poisoned");
31        if wait_timeout_result.timed_out() {
32            None
33        } else {
34            let val = guard.take().unwrap();
35            Some(val)
36        }
37    }
38    pub fn fill(&self, value: StoredTerm) {
39        let mut started = self.mutex.lock().unwrap();
40        *started = Some(value);
41        self.cond.notify_all();
42    }
43}
44
45// pub fn load(env: Env, _info: Term) -> bool {
46//     // rustler::resource!(ManualFuture, env);
47//     true
48// }
49
50mod atoms {
51    rustler::atoms! {
52        ok,
53        error,
54        exception,
55        exit,
56        throw,
57        timeout,
58    }
59}
60
61/// Attempts to turn `name` into a LocalPid
62/// Uses [`enif_whereis_pid`](https://www.erlang.org/doc/man/erl_nif.html#enif_whereis_pid) under the hood.
63///
64/// NOTE: Current implementation is very dirty, as we use transmutation to build a struct whose internals are not exposed by Rustler itself.
65/// There is an open PR on Rustler to add support properly: https://github.com/rusterlium/rustler/pull/456
66pub fn whereis_pid<'a>(env: Env<'a>, name: Term<'a>) -> Result<LocalPid, Error> {
67    let mut enif_pid = MaybeUninit::uninit();
68
69    if unsafe { rustler_sys::enif_whereis_pid(env.as_c_arg(), name.as_c_arg(), enif_pid.as_mut_ptr()) } == 0 {
70        Err(Error::Term(Box::new("No pid registered under the given name.")))
71    } else {
72        // Safety: Initialized by successful enif call
73        let enif_pid = unsafe {enif_pid.assume_init()};
74
75        // Safety: Safe because `LocalPid` has only one field.
76        // NOTE: Dirty hack, but there is no other way to create a LocalPid exposed from `rustler`.
77        let pid = unsafe { std::mem::transmute::<ErlNifPid, LocalPid>(enif_pid) };
78        Ok(pid)
79    }
80}
81
82fn send_to_elixir<'a>(env: Env<'a>, pid: Term<'a>, value: Term<'a>) -> Result<(), Error> {
83    let pid : LocalPid = pid.decode().or_else(|_| whereis_pid(env, pid))?;
84
85    env.send(&pid, value);
86    Ok(())
87}
88
89#[derive(Clone)]
90/// The result of calling a function on the Elixir side.
91///
92/// This enum exists because we want to handle all possible failure scenarios correctly.
93///
94/// ElixirFunCallResult implements the `rustler::types::Encoder` trait,
95/// to allow you to convert the result back into a `Term<'a>` representation for easy debugging.
96///
97/// However, more useful is usually to pattern-match in Rust on the resulting values instead,
98/// and only encode the inner `StoredTerm` afterwards.
99pub enum ElixirFunCallResult {
100    /// The happy path: The function completed successfully. In Elixir, this looks like `{:ok, value}`
101    Success(StoredTerm),
102    /// An exception was raised. In Elixir, this looks like `{:error, {:exception, some_exception}}`
103    ExceptionRaised(StoredTerm),
104    /// The code attempted to exit the process using a call to `exit(val)`. In Elixir, this looks like `{:error, {:exit, val}}`
105    Exited(StoredTerm),
106    /// A raw value was thrown using `throw(val)`. In Elixir, this looks like `{:error, {:thrown, val}}`
107    ValueThrown(StoredTerm),
108    /// The function took too long to complete. In Elixir, this looks like `{:error, :timeout}`
109    TimedOut,
110}
111
112impl Encoder for ElixirFunCallResult {
113    fn encode<'a>(&self, env: Env<'a>) -> Term<'a> {
114        let result = match self {
115            Success(term) => Ok(term),
116            ExceptionRaised(term) => Err(make_tuple(env, &[atoms::exception().to_term(env), term.encode(env)])),
117            Exited(term) => Err(make_tuple(env, &[atoms::exit().to_term(env), term.encode(env)])),
118            ValueThrown(term) => Err(make_tuple(env, &[atoms::throw().to_term(env), term.encode(env)])),
119            TimedOut => Err(atoms::timeout().to_term(env))
120        };
121
122        result.encode(env)
123    }
124}
125
126/// Will run `fun` with the parameters `parameters`
127/// on the process indicated by `pid_or_name`.
128///
129/// 'Raises' an ArgumentError (e.g. returns `Err(Error::BadArg)` on the Rust side) if `fun` is not a function or `parameters` is not a list.
130///
131/// Even with proper parameters, the function call itself might fail.
132/// All various scenarios are handled by the `ElixirFunCallResult` type.
133///
134/// # Notes
135///
136/// - It waits for a maximum of 5000 milliseconds before returning an `Ok(TimedOut)`.
137/// - Be sure to register any NIF that calls this function as a 'Dirty CPU NIF'! (by using `#[rustler::nif(schedule = "DirtyCpu")]`).
138///   This is important for two reasons:
139///     1. calling back into Elixir might indeed take quite some time.
140///     2. we want to prevent schedulers to wait for themselves, which might otherwise sometimes happen.
141pub fn apply_elixir_fun<'a>(env: Env<'a>, pid_or_name: Term<'a>, fun: Term<'a>, parameters: Term<'a>) -> Result<ElixirFunCallResult, Error> {
142    apply_elixir_fun_timeout(env, pid_or_name, fun, parameters, Duration::from_millis(5000))
143}
144
145/// Works the same as `apply_elixir_fun` but allows customizing the timeout to wait for the function to return.
146pub fn apply_elixir_fun_timeout<'a>(env: Env<'a>, pid_or_name: Term<'a>, fun: Term<'a>, parameters: Term<'a>, timeout: Duration) -> Result<ElixirFunCallResult, Error> {
147    if !fun.is_fun() {
148        return Err(Error::BadArg)
149    }
150
151    if !parameters.is_list() {
152        return Err(Error::BadArg)
153    }
154
155    // let future = ResourceArc::new(ManualFuture::new());
156    // let fun_tuple = rustler::types::tuple::make_tuple(env, &[fun, parameters, future.encode(env)]);
157    let future = ManualFuture::new();
158    let future_ptr : *const ManualFuture = &future;
159    let raw_future_ptr = future_ptr as usize;
160    let fun_tuple = rustler::types::tuple::make_tuple(env, &[fun, parameters, raw_future_ptr.encode(env)]);
161    send_to_elixir(env, pid_or_name, fun_tuple)?;
162
163    match future.wait_until_filled(timeout) {
164        None => Ok(TimedOut),
165        Some(result) => Ok(parse_fun_call_result(env, result))
166    }
167}
168
169fn parse_fun_call_result<'a>(env: Env<'a>, result: StoredTerm) -> ElixirFunCallResult {
170    match result {
171        Tuple(ref tuple) =>
172            match &tuple[..] {
173                [AnAtom(ok), value] if ok == &rustler::types::atom::ok() => Success(value.clone()),
174                [AnAtom(error), Tuple(ref error_tuple)] if error == &atoms::error() => {
175                    match &error_tuple[..] {
176                        [AnAtom(exception), problem] if exception == &atoms::exception() => ExceptionRaised(problem.clone()),
177                        [AnAtom(exit), problem] if exit == &atoms::exit() => Exited(problem.clone()),
178                        [AnAtom(throw), problem] if throw == &atoms::throw() => ValueThrown(problem.clone()),
179                        _ => panic!("RustlerElixirFun's function wrapper returned an unexpected error tuple result: {:?}", result.encode(env))
180                    }
181                },
182                _ => panic!("RustlerElixirFun's function wrapper returned an unexpected tuple result: {:?}", result.encode(env))
183            },
184        _ => panic!("RustlerElixirFun's function wrapper returned an unexpected result: {:?}", result.encode(env))
185    }
186}