一、?問題
我們需要執(zhí)行 CPU 密集型或系統(tǒng)級編程任務,而hex.pm中沒有好的解決方案,在這個例子中,我們假設沒有好的方法可以使用 Elixir 進行圖像處理。
通常情況下,有一個名為image的高質量 Rust 庫聲稱它就是解決方案!但是糟糕,我們的整個應用程序已經用 Elixir 編寫了,我們真的不知道如何很好地使用Rust。
Elixir 如何轉向 Rust 代碼以實現高性能操作?
二、解決方案
輸入rustler,這個庫旨在使使用 Rust 及其包生態(tài)系統(tǒng)變得簡單。讓我們開始吧!
按照入門指南,首先將rustler添加到我們的mix.exs文件中:
一旦我們運行mix deps.get使用內置的 mix 任務來生成我們的空 rust 項目:
mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > MyApp.RustImage
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (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
Ready to go! See /Users/me/projects/my_app/native/rust_image/README.md for further instructions
你應該去打開它README.md,但我會幫你省去麻煩,我們需要制作一個 Elixir 模塊,其中l(wèi)ib/my_app/rust_image.ex包含以下內容:
defmodule MyApp.RustImage do
use Rustler, otp_app: :my_app, crate: "rust_image"
# When your NIF is loaded, it will override this function.
def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
end
從那時起,我們準備好做一些 Rust。默認的生成器給了我們一個add/2實現的函數讓native/rust_image/src/lib.rs我們來看看
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
a + b
}
rustler::init!("Elixir.MyApp.RustImage", [add]);
三、什么是 NIF?
Native Implemented Functions 是 BEAM 允許進程直接調用本地函數的方法。他們通常有大量的樣板文件,你需要認真清理內存、處理錯誤和確保安全。幸運的是,這就是 Rust 的全部!例如這里是 Erlang NIF 教程。我們不需要做任何這些!
我們的超級優(yōu)化代碼將添加兩個大小為 i64 的整數并返回結果。請注意此處 Rustler 的特定部分:
- #[rustler::nif]是一個宏,告訴 Rustler 將此函數公開為 NIF。
- rustler::init!("Elixir.MyApp.RustImage", [add]);這將初始化 Erlang NIF 運行時,以便 beam 可以將add/2函數放在模塊上Elixir.MyApp.RustImage并替換我們留下的存根。
這太棒了??纯催@是否有效,讓我們開火iex -S mix
iex(1)> MyApp.RustImage.add(100, 20)
12
如果第一次一切正常,你應該已經看到 cargo 在發(fā)布模式下構建應用程序并在打開 iex 術語之前成功。如果你還沒有安裝 Rust,它會顯示一個錯誤,你可以按照通常的方式安裝 Rust 。
Rustler 甚至很聰明,會自動重新編譯,保持 iex 打開并更改我們的lib.rs
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
a + b + 1
}
保存然后再次打開正在運行的 iex 會話:
iex(2)> r(MyApp.RustImage)
... truncated output of cargo doing it's thing an maybe some beam warnings
{:reloaded, [MyApp.RustImage]}
iex(3)> MyApp.RustImage.add(1,1)
3
極好的!我們得到了相同的工作流程和與 Elixir 一起工作的好處,而對 Rust 的煩惱最少。
四、圖片
首先將我們的圖像依賴項添加到我們的Cargo.toml文件中:
[dependencies]
rustler = "0.27.0"
image = "0.24.6"
然后改變我們lib.rs創(chuàng)建一個函數,它接受一個input路徑,一個output路徑,quality并將任何圖像更改為具有我們設置質量的 JPEG。
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]);
我們還想更新我們的 RustImage 模塊以包含一個存根jpg/3,但這留給讀者作為練習。
現在讓我們試試吧!iex -S mix
iex(1)> MyApp.RustImage.jpg("input.png", "output.jpeg", 75)
{:ok, "output.jpeg"}
和繁榮!我們已將 PNG 轉換為 JPEG,質量為 75%。
五、做一個良好的 BEAM 公民
我們還應該在這里考慮一件事,那就是 CPU 負載。雖然此功能可能會在我們的筆記本電腦上立即運行,但在部署時可能需要更長時間才能共享 CPU/RAM。
而且因為 BEAM 直接運行我們的代碼,它會鎖定運行時直到它完成運行。我們所說的直接意思是,當使用 NIF 時,beam 會像對待任何其他代碼一樣對待它,主要的警告是它不能自動搶占 Rust 代碼。
在 BEAM 上,這是一個大問題,因為整個運行時都希望能夠隨時在數百萬個進程之間切換上下文。
幸運的是 Rustler 和 BEAM 團隊已經想到了這一點并給了我們一個解決方案。只需將該宏更改為此jpeg即可
-- #[rustler::nif]
++ #[rustler::nif(schedule = "DirtyCpu")]
這告訴 Rustler 和 BEAM 以一種在它工作時不會阻塞整個世界的方式自動安排它。再次令人驚嘆,這被稱為 DirtyNif,當你通過 C 手動使用它時,使用起來會更加困難。
六、部署
使用 Docker 將其部署到 Fly.io 并不是那么自動,我們需要進行一些小的更改,以便我們的 Docker 環(huán)境可以構建 Rust。首先,通過在我們的 Elixir 構建步驟之前添加一個構建步驟來更新 Dockerfile:
#... 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
然后更新我們config/prod.exs添加以下行:
config :my_app, MyApp.RustImage,
crate: :rust_image,
skip_compilation?: true,
load_from: {:my_app, "priv/native/librust_image"}
我們在這里所做的是在其自己的 Docker 構建器上下文中構建庫,因此它與我們其余的 Docker 步驟并行運行并且可以輕松緩存。然后我們告訴 Rustler 跳過編譯并直接從我們放置它的地方加載它。
我們都準備好了,fly deploy你就可以出發(fā)了!
七、討論
關于結合使用 NIF 和 Rust 的強大功能,我們只是真正觸及了皮毛。從加載海量數據集到做科學研究再到通過 WebRTC 進行連接,Rust 社區(qū)已經構建了一套令人印象深刻的包和工具,現在我們也可以使用這些包和工具。Rustler 使之成為可能!
原文鏈接:https://fly.io/phoenix-files/elixir-and-rust-is-a-good-mix/