theme | style | paginate | backgroundColor | marp |
---|---|---|---|---|
uncover |
li {
font-size: 28px;
letter-spacing: 1px;
}
p.quote {
line-height: 38px;
}
q {
font-size: 32px;
letter-spacing: 1px;
}
cite {
text-align: right;
font-size: 28px;
margin-top: 12px;
margin-bottom: 128px;
}
|
true |
true |
- Native Implemented Function
- Интерфейс, през който BEAM зарежда и извиква C функции от динамична библиотека (.dll / .so)
- При други езици се нарича FFI - foreign function interface
- По-бърз код, отколкото е възможно на чист Elixir
- Пример как Discord използват Rust в Elixir, за да имплементират бърз и ефективна структура от данни Sorted Set
- Използване на библиотека, която съществува само за C (или някой друг език)
- Например библиотеки за криптография
#include <erl_nif.h>
#include <string.h>
static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
const char* greeting = "Здравей от C!";
const size_t greeting_len = strlen(greeting);
ERL_NIF_TERM new_binary;
unsigned char* new_binary_data = enif_make_new_binary(env, greeting_len, &new_binary);
memcpy(new_binary_data, greeting, greeting_len);
return new_binary;
}
static ErlNifFunc nif_funcs[] = {
{"hello", 0, hello}
};
ERL_NIF_INIT(Elixir.HelloC, nif_funcs, NULL, NULL, NULL, NULL)
- Дефинираме функции от следния тип:
(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -> ERL_NIF_TERM
- Оказваме коя native функция на коя Erlang функция съответства.
Например искаме C фунцкията
hello
да се достъпва в Elixir катоhello/0
static ErlNifFunc nif_funcs[] = {
{"hello", 0, hello}
};
- Извикваме макрото
ERL_NIF_INIT
, което генерира допълнителна информация, нужна на BEAM за да зареди библиотеката. Това включва името на Erlang/Elixir модула.
defmodule HelloC do
@on_load :load_nifs
def load_nifs do
:ok = :erlang.load_nif("./nif/_build/libhello", 0)
end
def hello, do: :erlang.nif_error(:nif_not_loaded)
end
- Дефинираме модул, който ще държи nif-овете. Nif-овете винаги се асоциират с модул. Библиотеката ще стои заредена докато съответния модул е зареден.
- Дефинираме самите функции с имплементация по подразбиране. При успешно зареждане на библиотеката те ще се заместят със съответната native фунцкция.
- Извикваме
:erlang.load_nif(<път до библиотеката>, 0)
, което връща:ok
ако библиотеката се зареди успешно
- Може да се използва
@on_load: <function>
за да се зареди библиотеката при зареждане на модула @on_load
създава нов процес в който изпълнява подадената функция- Процесът умира когато функцията завърши
- При успешно зарешдане функцията трябва да върне
:ok
- Ако друг процес се опита да извика функция от този модул, той ще бъде спрян докато
@on_load
функцията не завърши (това работи само при първоначалното зареждане, но не при code change)
https://www.erlang.org/doc/man/erl_nif.html
- NIF-овете ефективно разширяват кода на виртуалната машина
- Изпълняват се в незащитена среда, затова трябва да сме изключително внимателни, когато ги използваме
- Ако NIF "гръмне" - ще убие цялата виртуална машина
- Ако NIF предизвика memory corruption - много лошо
- Ако NIF се изпълнява твърде дълго време - може да възпрепядства scheduler-ите на BEAM
- Библиотека на Rust за писане на erlang-ски NIF-ове
- Елиминира голяма част от boilerplate кода за интеграция с BEAM
- Фокус върху безопасност - няма начин да се предизвика краш в BEAM
- https://docs.rs/rustler/
- https://hexdocs.pm/rustler/
- Език от ниско ниво - директен контрол над генерирания код, над паметта, над алокациите
- Език от високо ниво - декларативен стил на писане, силна типова система, union типове, pattern matching
- Модерен език с модерен tool-инг
- Напълно безопасен език - предизвикването на UB е невъзможно
- Добавяме elixir-ската част от библиотеката като dependency
- Това включва генератор, който ще ни създаде rust-ска библиотека в
native/<име>/
defp deps do
[
{:rustler, "~> 0.28.0"},
]
end
mix deps.get
mix rustler.new
#[rustler::nif]
fn hello() -> &'static str {
"Здравей от Rust!"
}
rustler::init!("Elixir.HelloRust", [hello]);
- Редът
use Rustler, ...
навързва двата проекта - При компилиране на Elixir кода автоматично ще се компилира и rust кода
defmodule HelloRust do
use Rustler, otp_app: :hello_rust, crate: :hello
def hello, do: :erlang.nif_error(:nif_not_loaded)
end
#[rustler::nif]
fn my_func<'a>(env: Env<'a>, arg1: Term<'a>, arg2: Term<'a>) -> Term<'a> { ... }
- В най-простия си вариант, един NIF в Rusltler получава средата
Env
и списък от термовеTerm
и връщаTerm
. #[rustler::nif]
е макрос, който генерира нужнуя код за извикването на тази функция от C API-то.Env
е необходим за създаване на нови термове, както и повечето неща които wrap-ват функции от C API-то. Този аргумент може да се пропусне, ако не е неоходим. *Term
в Rustler съдържа не самоERL_NIF_TERM
, но и референция къмEnv
, което позволява да му се викат методи без да се подаваenv
навсякъде.
fn my_func(arg1: Atom, arg2: i32) -> String { ... }
- NIF-а също може директно да приема и връща типове, които могат да се конвертират от и до
Term
. - В такъв случай библиотеката се грижи за конвертирането.
- Ако подадените аргументи се различават от очаквания тип се хвърля erlang-ска грешка
- Rustler също поддържа derive макроси, които генерират конвертиране от rust-ска структура до еликсирски map/struct/exception. Също така от rust-ски enum-и до еликсирски tuple-и.
- Поддържа се и автоматично конвертиране от стандартните rust-ски типове
Option
иResult
Option
- Some(val) -> val
- None -> :nil
Result
- Ok(val) -> {:ok, val}
- Err(e) -> {:error, e}
- Атомите могат да се дефинират предварително, за да не се налага да се конструират от низ всеки път
rustler::atoms! {
ok,
error,
unknown_term,
foo,
bar,
baz,
}
Term
-овете, подадени като аргументи на NIF функция са валидни само по времето на тази функция- След края на функцията GC може да ги изтрие по всяко време
- В Rustler това е указано чрез lifetime анотации
- Опитът да се задържи
Term
за по-дълго от текущата функция би довел до компилационна грешка
- За да се запази терм за по-дълго може:
- да се създаде
OwnedEnv
- отделен heap, който не е асоцииран с процес и да се копира терма там - да се сериализира до External Text Format на Erlang. Това връща
OwnedBinary
, което може по-късно да се десериализира доTerm
в някойEnv
.
- да се създаде
- Ресурсите позволяват NIF-овете да работят със собствени типове, а не само с Erlang-ски термове
- Трябва да се регистрира типът на ресурса и след това могат да се създават обекти от този тип
- За всеки обект виртуалната машина заделя памет и връща handle към тази памет
- Този handle може да бъде превърнат в Erlang-ски терм и върнат от NIF-а
- Термът е opaque от гледна точка на Erlang/Elixir (връща се референция)
- Може да бъде запазен и препращан, но не и използван директно
- Може да бъде подаден като аргумент на NIF, откъдето може да се достъпи оригиналната структура
- Типът на ресурса трябва да бъде регистриран по време на зареждане на библиотеката
- Възможно е да подадем функции, които ще се извикат при определено събитие, свързано с nif библиотеката
- Събитията са
load
,upgrade
,unload
- Тип на ресурс се дефинира по време на
load
илиupgrade
- В C API-то функциите се подават на макрото
ERL_NIF_INIT
ERL_NIF_INIT(MODULE, nif_funcs, load, NULL, upgrade, unload)
- Rustler за момента поддържа само
load
функцията
rustler::init!("MODULE", nif_funcs, load = load);
- Паметта за ресурс обектите се контролира от Erlang
- Паметта се алокира от Erlang при създаване на обекта
- Всеки обект съдържа reference counter. Когато броят референции (от Erlang и от NIF библиотеката) стигне нула се извиква деструктор, зададен за съответния тип, и се освобождава паметта.
- Чрез ресурси можем да имплементираме mutable състояние в Elixir.
- В C това просто би работило, но не и в Rust.
- Rust има правило, че една стойност не може да е едновременно споделена и mutable.
ResourceArc<T>
ни позволява да вземем само константна референция&T
към вътрешността, защото ресурса може да бъде копиран и споделян между множество elixir-ски процеси.
- За да можем да модифицираме ресурса трябва да вземем
&mut T
, но за целта трябва да докажем, че имаме ексклузивен достъп до стойността.-
- Трябва да се подсигурим, че ресурса се достъпва само от един процес от Elixir. За целта можем да използваме
GenServer
- Трябва да се подсигурим, че ресурса се достъпва само от един процес от Elixir. За целта можем да използваме
-
- Трябва да покажем на Rust, че имаме ексклузивен достъп. За целта можем да използваме
Mutex
илиSpinLock
, но е важно никога да не блокираме, опитвайки се да заключим мутекса. Т.е. използваме самоMutex::try_lock
, но не иMutex::lock
- Трябва да покажем на Rust, че имаме ексклузивен достъп. За целта можем да използваме
-
- NIF-овете трябва да са сравнително кратки, за да не блокират BEAM
- Документацията препоръчва да не се надхвърля 1 милисекунда
- При нужда от по-дълго време за изпълнение има няколко варианта
- Работата се разделя на малки парчета
- Използва се
enif_schedule_nif
, за да се schedule-не извикването на функция - Тази функция изпълнява едно парче работа и извиква
enif_schedule_nif
отново, докато цялата работа не е свършена - Това не се поддържа от Rustler все още.
- Нещо подобно може да се имплементира, ако разбиването на задачата се имплементира на ниво Elixir.
- Native библиотека пуска отделна нишка на ОС, която изпълнява задачата
- И може би поддържа thread pool от такива нишки
- NIF-ът връща веднага
- Истинският резултат се изпраща като съобщение с
env.send
#[rustler::nif(schedule = "DirtyCpu")]
pub fn my_lengthy_work() -> i64 {
let duration = Duration::from_millis(100);
std::thread::sleep(duration);
42
}
- Задава се с флаг, че въпросния NIF е "мръсен"
- DirtyIO или DirtyCPU, в зависимост дали операцията блокира заради IO или е тежка откъм процесорен ресурс
- BEAM изпълнява такива "мръсни" NIF-ове в отделен thread pool с отделни scheduler-и
- При Rustler това се оказва чрез аргумент към
rustler::nif
макрото