Qqwy

Qqwy

TypeCheck Core Team

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.

Showing Posts 1 to 3

Qqwy

Qqwy OP

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 OP

TypeCheck Core Team

Version 0.1.0 has been released, both on hex as well as on cargo. (You’ll need both the Elixir library and the Rust library to use it).

Qqwy

Qqwy OP

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.

— All posts loaded —

Where Next? Top

Trending in Announcing Top

wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
handnot2
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application. This library uses Erlang esaml to provide plug enabl...
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
fuelen
Hi all! I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas. You...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mudasobwa
I am seeing a lot of aplications of Argumentum ad Vericundiam in software discussions. They do link some piece of writing and point us to...
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
sergio
It’s not that it’s vocabulary is too advanced. It’s something worse. I get lost trying to follow even a paragraph written by Claude. It’...
New
sorenone
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
akoutmos
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews