Now Reading
Elixir and Rust is an efficient combine · Fly

Elixir and Rust is an efficient combine · Fly

2023-04-13 13:29:04

An purple droplet representing Elixir and a red crab, Ferris, representing Rust.
Picture by Annie Ruygt

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]);

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 the add/2 perform on the Elixir.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.

See Also

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!

Fly.io is an effective way to run your Phoenix LiveView app near your customers. It is very easy to get began. You could be working in minutes.

Deploy a Phoenix app today!  



Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top