Qqwy

Qqwy

TypeCheck Core Team

RustlerElixirFun: Calling Elixir from Rust

RustlerElixirFun

With Rustler, it became a lot easier to create well-behaving Natively-Implemented-Functions (NIFs).
However, one question which arises from time to time (here, here, here, here, …), is how to call back into Elixir code from within native code: If you pass an anonymous function to a NIF, then how can we use this function inside the NIF?

The short answer: This is not supported.
The longer answer: … But if you get ready to jump through some hoops, you can still make it work!

The gist of it: (Thanks to Erlang Forums user @robashton and GitHub user @tessi’s explanations for inspiration!)

  1. In native code, create a manual ‘future’ (a mutex wrapping a potentially-empty value, and a condition variable).
  2. Wrap this ‘future’ into a reference which can be passed back to Elixir.
  3. Send {fun, params, future_ref} to a particular process (or pool of processes) which you’ve been running.
  4. Block the native code until the future is filled. (Or a timeout is triggered)
  5. In the elixir GenServer, run the function, (handle errors) and once the result is obtained, call a second NIF with the result and the future_ref.
  6. This NIF will ‘fill’ the future with the passed result and immediately return.
  7. Now the original native code will continue.

Or, in a diagram:


Until now, there were a couple of projects which did this manually, but it seemed to make sense to start working on a library to abstract this pattern once and for all.
This way, we can make sure it works well (no edge cases) and as efficient as it might be, with both a single GenServer you might run, as well as a full-fledged pool.

The project can be found at https://github.com/Qqwy/elixir-rustler_elixir_fun.
Work on RustlerElixirFun is still ongoing, but feedback would already be much appreciated.

Most Liked

Qqwy

Qqwy

TypeCheck Core Team

A preliminary benchmark shows that calling a NIF which calls Elixir to run a trivial function which returns through the NIF back to the original caller takes roughly 85 times longer than calling a function directly in Elixir: 9.5µs vs 0.11µs.

I’m pretty sure that this overhead, while not neglegible, can still be considered ‘good enough’ for many projects, as the overhead of 9.5µs will often be overshadowed if there is some actual work (or communication with other processes or IO) going on inside the function.

$ MIX_ENV=bench mix run bench/call_overhead.ex 
Operating System: Linux
CPU Information: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 31.18 GB
Elixir 1.12.0
Erlang 24.0.1

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: 100, 100_000, 100_000_000
Estimated total run time: 1.40 min

Benchmarking apply(fun, param) with input 100 ...
Benchmarking apply(fun, param) with input 100_000 ...
Benchmarking apply(fun, param) with input 100_000_000 ...
Benchmarking apply_elixir_fun(Pool, fun, param) with input 100 ...
Benchmarking apply_elixir_fun(Pool, fun, param) with input 100_000 ...
Benchmarking apply_elixir_fun(Pool, fun, param) with input 100_000_000 ...
Benchmarking apply_elixir_fun(Server, fun, param) with input 100 ...
Benchmarking apply_elixir_fun(Server, fun, param) with input 100_000 ...
Benchmarking apply_elixir_fun(Server, fun, param) with input 100_000_000 ...
Benchmarking fun.(param) with input 100 ...
Benchmarking fun.(param) with input 100_000 ...
Benchmarking fun.(param) with input 100_000_000 ...

##### With input 100 #####
Name                                           ips        average  deviation         median         99th %
apply(fun, param)                           8.65 M       0.116 μs   ±144.37%       0.114 μs       0.178 μs
fun.(param)                                 8.64 M       0.116 μs   ±147.69%       0.114 μs       0.156 μs
apply_elixir_fun(Pool, fun, param)         0.104 M        9.63 μs   ±222.87%        8.54 μs       27.16 μs
apply_elixir_fun(Server, fun, param)       0.101 M        9.95 μs   ±233.02%        8.68 μs       27.38 μs

Comparison: 
apply(fun, param)                           8.65 M
fun.(param)                                 8.64 M - 1.00x slower +0.00003 μs
apply_elixir_fun(Pool, fun, param)         0.104 M - 83.27x slower +9.51 μs
apply_elixir_fun(Server, fun, param)       0.101 M - 86.01x slower +9.83 μs

##### With input 100_000 #####
Name                                           ips        average  deviation         median         99th %
apply(fun, param)                           8.55 M       0.117 μs   ±140.47%       0.115 μs       0.160 μs
fun.(param)                                 8.41 M       0.119 μs   ±131.56%       0.115 μs       0.180 μs
apply_elixir_fun(Server, fun, param)       0.104 M        9.59 μs   ±209.01%        8.53 μs       26.84 μs
apply_elixir_fun(Pool, fun, param)        0.0994 M       10.06 μs   ±179.97%        8.80 μs       27.30 μs

Comparison: 
apply(fun, param)                           8.55 M
fun.(param)                                 8.41 M - 1.02x slower +0.00198 μs
apply_elixir_fun(Server, fun, param)       0.104 M - 82.02x slower +9.47 μs
apply_elixir_fun(Pool, fun, param)        0.0994 M - 86.05x slower +9.94 μs

##### With input 100_000_000 #####
Name                                           ips        average  deviation         median         99th %
fun.(param)                                 8.42 M       0.119 μs   ±114.30%       0.115 μs       0.185 μs
apply(fun, param)                           8.27 M       0.121 μs   ±141.67%       0.118 μs        0.25 μs
apply_elixir_fun(Server, fun, param)       0.103 M        9.75 μs   ±219.29%        8.53 μs       27.54 μs
apply_elixir_fun(Pool, fun, param)        0.0961 M       10.40 μs   ±193.83%        8.94 μs       27.95 μs

Comparison: 
fun.(param)                                 8.42 M
apply(fun, param)                           8.27 M - 1.02x slower +0.00213 μs
apply_elixir_fun(Server, fun, param)       0.103 M - 82.08x slower +9.63 μs
apply_elixir_fun(Pool, fun, param)        0.0961 M - 87.54x slower +10.28 μs

Qqwy

Qqwy

TypeCheck Core Team

Version 0.3.0 has been released.
It now has been thoroughly battle-tested by using it from within ArraysRRBVector. :blush:

Usage has become slightly simpler and less error-prone; on the Rust side an enum is now returned with the various possible results of a function call:

pub enum ElixirFunCallResult {
    /// The happy path: The function completed successfully. In Elixir, this looks like `{:ok, value}`
    Success(StoredTerm),
    /// An exception was raised. In Elixir, this looks like `{:error, {:exception, some_exception}}`
    ExceptionRaised(StoredTerm),
    /// The code attempted to exit the process using a call to `exit(val)`. In Elixir, this looks like `{:error, {:exit, val}}`
    Exited(StoredTerm),
    /// A raw value was thrown using `throw(val)`. In Elixir, this looks like `{:error, {:thrown, val}}`
    ValueThrown(StoredTerm),
    /// The function took too long to complete. In Elixir, this looks like `{:error, :timeout}`
    TimedOut,
}

This way, we can be sure that all edge cases are considered when you use it in your code.

Where Next?

Popular in Announcing Top

Hal9000
Here is my first stab at this. README pasted below. https://github.com/Hal9000/elixir_random Comments and critiques are welcome. Thank...
New
wmnnd
Hi there, for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
bryanjos
Hi, I wanted share a small library we at Revelry Labs made for rendering react components from the server side. There are instructions fo...
New
josevalim
EDIT: since Ecto 3.0 final version is out, this post was amended to use the final versions in the instructions below. Hi everyone, We a...
New
kip
ex_cldr provides localisation and internationalisation support based upon the data from the Unicode CLDR project. Unicode released CLDR ...
407 13300 120
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 11241 107
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36689 110
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement