Elixir and Rust is an efficient combine · Fly
This publish is about utilizing Rust with Elixir and the way simply it may be achieved! If you wish to deploy your Phoenix LiveView app proper now, then try how you can get started. You could possibly be up and working in minutes.
Problem
We need to perform a CPU intensive or system level programming task and there are just no good solutions in hex.pm, on this instance let’s fake there are no good ways to do image processing with Elixir.
As is commonly the case, there IS a top quality Rust library referred to as image that claims to be simply the answer! However shoot, our complete utility is written in Elixir already, and we actually do not know how you can use Rust that nicely.
How can Elixir flip to Rust code for high-performance operations?
Solution
Enter rustler, this library is designed to make utilizing Rust and its package deal ecosystem trivial. Let’s dive in!
Following the getting began information, first add rustler to our combine.exs
file:
As soon as we run combine deps.get
use the built-in combine activity to generate our empty rust venture:
That is the identify of the Elixir module the NIF module will probably be registered to.
Module identify > MyApp.RustImage
That is the identify used for the generated Rust crate. The default is almost definitely wonderful.
Library identify (myapp_rustimage) > rust_image
* creating native/rust_image/.cargo/config.toml
* creating native/rust_image/README.md
* creating native/rust_image/Cargo.toml
* creating native/rust_image/src/lib.rs
* creating native/rust_image/.gitignore
Able to go! See /Customers/me/initiatives/my_app/native/rust_image/README.md for additional directions.
You must go open up that README.md
, however I will prevent the trouble, we have to make an Elixir module in lib/my_app/rust_image.ex
that has the next contents:
defmodule MyApp.RustImage do
use Rustler, otp_app: :my_app, crate: "rust_image"
# When your NIF is loaded, it should override this perform.
def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
finish
And from then on out we’re able to do some Rust. The default generator provides us an add/2
perform carried out in native/rust_image/src/lib.rs
let’s have a look
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
a + b
}
rustler::init!("Elixir.MyApp.RustImage", [add]);
What Is a NIF?
Native Implemented Functions are the BEAM’s method of allowing processes to directly call native functions. They normally have a ton of boilerplate and you need to be serious about cleaning up your memory, handling errors and being safe. Luckily that’s Rust’s entire thing! For example here is the Erlang NIF tutorial. We did not must do any of that!
Our hyper optimized code will add two integers of dimension i64 and return the end result. Observe the Rustler particular components right here:
#[rustler::nif]
is a macro that tells Rustler to show this perform as a NIF.rustler::init!("Elixir.MyApp.RustImage", [add]);
This initializes the Erlang NIF runtime in order that the beam can put theadd/2
perform on theElixir.MyApp.RustImage
module and substitute the stub we left.
That is wonderful. To see if this works, lets hearth up iex -S combine
iex(1)> MyApp.RustImage.add(100, 20)
120
If all the pieces labored the primary time, it is best to have seen cargo constructing the app in launch mode and succeeding earlier than opening the iex time period. For those who did not have already got Rust put in it will have proven an error, you’ll be able to set up Rust the usual way.
Rustler is even good and can recompile mechanically, depart iex open and alter our lib.rs
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
a + b + 1
}
Save after which open that working iex session once more:
iex(2)> r(MyApp.RustImage)
... truncated output of cargo doing it's factor an possibly some beam warnings
{:reloaded, [MyApp.RustImage]}
iex(3)> MyApp.RustImage.add(1,1)
3
Unimaginable! We get the identical workflow and good bits of working with Elixir, with minimal fussing about with Rust.
Images
First add our image dependency to our Cargo.toml
file:
[dependencies]
rustler = "0.27.0"
image = "0.24.6"
Then alter our lib.rs
to create a function that accepts an input
path, an output
path and quality
and changes any image to a JPEG with our set quality.
use image::io::Reader as ImageReader;
use image::codecs::jpeg::JpegEncoder;
use std::fs::File;
#[rustler::nif]
fn jpg(input: String, output: String, quality: i64) -> Result<String, String> {
let img = ImageReader::open(&input).unwrap().decode().unwrap();
let out_file = std::fs::File::create(&output).unwrap();
let mut jpg = JpegEncoder::new_with_quality(&out_file, quality as u8);
jpg.encode_image(&img).unwrap();
Ok(output.to_string())
}
// add code...
rustler::init!("Elixir.MyApp.RustImage", [add, jpg]);
We also want to update our RustImage module to include a stub for jpg/3
, but that’s left as an exercise to the reader.
Now let’s try it out! iex -S mix
iex(1)> MyApp.RustImage.jpg("input.png", "output.jpeg", 75)
{:ok, "output.jpeg"}
And boom! We’ve converted a PNG to a JPEG with 75% quality.
Being a Good BEAM Citizen
There is one more thing we should consider here, and that’s CPU load. While this function likely runs near instantly on our laptop, when deployed it might take longer on shared CPU/RAM.
And because the BEAM runs our code directly, and it will lock an the runtime until it has completed running. What we mean by directly is that when using a NIF the beam will treat it like any other code, with the major caveat that it can’t prempt the Rust code automatically.
On the BEAM this is a big issue since the entire runtime expects to be able to switch contexts between millions of processes at any time.
Luckily the Rustler and BEAM teams have thought of this and given us a solution. Simply change that macro on top of jpeg
to this
-- #[rustler::nif]
++ #[rustler::nif(schedule = "DirtyCpu")]
This tells the Rustler and BEAM to automagically schedule this in a way that won’t block the entire world while it works. Again amazing, this is called a DirtyNif and is far more troublesome to work with when you find yourself manually utilizing this through C.
Deployment
Deploying this to Fly.io with Docker isn’t as automagic, we need to make some small changes so that our Docker environment can build Rust. First, update the Dockerfile by adding a build step right before our Elixir build step:
#... ARG stuff..
FROM rust:1.68.0 as rust
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
workdir /app
COPY native/rust_images ./
RUN cargo rustc --release
#..Elixir builder.....
# compile assets
RUN mix assets.deploy
#NEW STUFF
COPY --from=rust /app/target/release/librust_images.so priv/native/librust_images.so
#/NEW
# Compile the release
RUN mix compile
Then update our config/prod.exs
adding the following line:
config :my_app, MyApp.RustImage,
crate: :rust_image,
skip_compilation?: true,
load_from: {:my_app, "priv/native/librust_image"}
What we did here is build the library in its own Docker builder context, so it runs in parallel with the rest of our Docker steps and can be cache’d easily. Then we told Rustler to skip compiling and to load it directly from our where we put it.
And we’re all set, simply fly deploy
and you’re off!
Discussion
We have only really scratched the surface about what is possible using the power of NIFs and Rust together. From loading massive datasets to do science to connecting through WebRTC, the Rust group has constructed out a powerful suite of packages and instruments which are additionally now obtainable to us. And Rustler makes it doable!