NIF w Elixirze - kiedy sięgać po natywny kod i jakie problemy rozwiązuje
Elixir na BEAM to platforma do budowy systemów współbieżnych, odpornych na awarie, działających bez przerwy. Ale jest klasa problemów, przy których BEAM mówi: "to nie moja działka". Surowe obliczenia numeryczne, operacje na dużych binariach, kryptografia - tu potrzebujesz kodu, który działa bliżej sprzętu. I dokładnie do tego służą NIF-y.
Czym jest NIF
NIF - Native Implemented Function - to funkcja napisana w języku kompilowanym do kodu maszynowego (C, C++, Rust), która jest ładowana bezpośrednio do maszyny wirtualnej BEAM i wywoływana z Elixira jak zwykła funkcja modułu.
Kluczowe słowo: bezpośrednio. NIF nie komunikuje się z BEAM przez pipe, socket ani port. Działa w tym samym procesie systemowym co BEAM, współdzieli pamięć, ma dostęp do struktur danych maszyny wirtualnej. To daje ekstremalną wydajność - ale też ekstremalną odpowiedzialność.
# Z perspektywy Elixira - NIF wygląda jak zwykła funkcja
defmodule Crypto do
use Rustler, otp_app: :my_app, crate: "my_crypto"
def hash_argon2(_password), do: :erlang.nif_error(:nif_not_loaded)
end
# Wywołanie - zero różnicy w API
hash = Crypto.hash_argon2("moje_hasło")Programista wywołujący Crypto.hash_argon2/1 nie wie (i nie musi wiedzieć), że pod spodem działa natywny kod. Interfejs jest czystym Elixirem. Implementacja jest natywna.
Jak NIF działa od środka
Gdy BEAM ładuje moduł z NIF-em, dzieje się następujący proces:
- Kompilacja - kod natywny (C/Rust) kompiluje się do biblioteki współdzielonej (
.sona Linuxie,.dylibna macOS) - Ładowanie - BEAM ładuje bibliotekę do pamięci przez
erlang:load_nif/2 - Rejestracja - każda funkcja NIF jest mapowana na odpowiadającą jej funkcję Elixira/Erlanga
- Wywołanie - gdy Elixir wywołuje funkcję, BEAM przekazuje sterowanie bezpośrednio do natywnego kodu
- Konwersja - argumenty Elixira (termy) są konwertowane na typy natywne i z powrotem
Cały ten proces jest przezroczysty dla reszty aplikacji. Inne procesy BEAM nie wiedzą, że gdzieś w systemie wykonuje się natywny kod.
Kiedy NIF ma sens - realne use cases
1. Kryptografia
To najpowszechniejszy przypadek użycia NIF-ów. Operacje kryptograficzne to czyste obliczenia numeryczne - dokładnie to, w czym BEAM jest słaby, a natywny kod błyszczy.
Hashowanie haseł z Argon2/bcrypt - hashowanie jednego hasła w czystym Elixirze trwałoby sekundy. NIF robi to w 200-500ms (celowo wolno, bo to hashowanie hasła) bez obciążania schedulera BEAM.
Szyfrowanie AES/ChaCha20 - szyfrowanie megabajtów danych w locie. Standardowa biblioteka :crypto w Erlangu to NIF opakowujący OpenSSL. Używasz go za każdym razem, gdy wywołujesz HTTPS.
Weryfikacja podpisów cyfrowych - Ed25519, ECDSA, RSA. Każdy JWT token w Twojej aplikacji Phoenix jest weryfikowany przez NIF.
Prawda jest taka, że jeśli używasz Elixira w produkcji, już korzystasz z NIF-ów - :crypto, :ssl, :zlib to NIF-y dostarczane z Erlang/OTP.
2. Parsowanie i przetwarzanie danych
Parsowanie dużych plików binarnych to operacja, w której BEAM traci do natywnego kodu o rząd wielkości:
CSV/JSON na dużą skalę - parsowanie pliku CSV z milionem wierszy. W czystym Elixirze to minuty. NIF z biblioteką csv w Rust - sekundy. Biblioteka jsonrs (wrapper na serde_json z Rust) jest 2-5x szybsza niż Jason w czystym Elixirze.
Protocol Buffers / MessagePack - serializacja i deserializacja binarnych formatów. Przy dużych wolumenach wiadomości (systemy IoT, telemetria) każda mikrosekunda się liczy.
Parsowanie logów - regex na gigabajtowych plikach logów. Silnik regex z Rust (regex crate) jest jednym z najszybszych na świecie i dostępny przez NIF.
3. Przetwarzanie obrazów
Generowanie miniaturek, watermarki, konwersja formatów, OCR - to operacje intensywne obliczeniowo, które w czystym Elixirze byłyby niepraktycznie wolne:
Resize i kompresja - biblioteka image w Rust obsługuje JPEG, PNG, WebP, AVIF. Przez NIF wywoływana jedną funkcją z Elixira.
Generowanie PDF-ów - faktury, raporty, etykiety wysyłkowe. Biblioteki printpdf i typst w Rust generują dokumenty wielokrotnie szybciej niż rozwiązania w czystym Elixirze.
Przetwarzanie wideo - ekstrakcja klatek, transkodowanie, analiza metadanych. ExWebRTC używa NIF-ów do przetwarzania strumieni mediów w czasie rzeczywistym.
4. Kompresja i dekompresja
Standardowa biblioteka :zlib w Erlangu to NIF. Ale nowsze algorytmy kompresji oferują lepsze wyniki:
Zstandard (zstd) - kompresja Facebooka, 3-5x szybsza od gzip przy podobnym współczynniku kompresji. Dostępna przez NIF.
Brotli - kompresja Google, optymalna dla treści webowych. Używana w HTTP/2 i HTTP/3.
LZ4 - ekstremalnie szybka kompresja/dekompresja. Idealna dla cache'owania i komunikacji między węzłami BEAM.
5. Obliczenia numeryczne i ML
Ekosystem Nx (Numerical Elixir) to doskonały przykład NIF-ów w działaniu:
EXLA - kompiluje operacje tensorowe do natywnego kodu przez Google XLA. Trening modeli ML w Elixirze z wydajnością jak w Pythonie z TensorFlow.
Polars przez Explorer - analiza danych (odpowiednik Pandas). Silnik Polars w Rust, interfejs w Elixirze. Operacje na milionach wierszy w milisekundach.
Statystyka i sygnały - FFT, interpolacja, regresja. Operacje, które w czystym Elixirze są możliwe, ale zbyt wolne dla produkcyjnych obciążeń.
6. Walidacja i silniki reguł
Złożone reguły biznesowe w systemach ERP/CRM, które muszą być ewaluowane na dużych zbiorach danych:
Silnik cen - cena zależy od kategorii klienta, grupy rabatowej, daty, regionu, waluty i 50 wyjątków. Ewaluacja dla 100 000 produktów w Rust trwa ułamek tego, co w Elixirze.
Walidacja schematów - JSON Schema, XML Schema na dużych dokumentach. NIF z Rust waliduje megabajtowe dokumenty w milisekundach.
Ryzyka i pułapki NIF-ów
NIF-y to potężne narzędzie, ale z potęgą przychodzi odpowiedzialność. Oto ryzyka, o których musisz wiedzieć:
Problem schedulera
BEAM ma ścisłą zasadę: żadna operacja nie powinna blokować schedulera dłużej niż 1 milisekundę. Scheduler BEAM obsługuje tysiące procesów Elixira przez preemptive scheduling - przełącza się między nimi co ~4000 redukcji (operacji). Ale NIF to natywny kod - BEAM nie może go przerwać.
Jeśli NIF działa 500ms, blokuje scheduler na pół sekundy. Wszystkie procesy Elixira przypisane do tego schedulera czekają. Przy 8 schedulerach (typowy serwer 8-rdzeniowy) i jednym zablokowanym tracisz 12.5% przepustowości systemu.
Rozwiązanie: dirty schedulers. BEAM udostępnia osobne pule wątków dla długotrwałych operacji:
- Dirty CPU schedulers - dla operacji intensywnych obliczeniowo (hashowanie, kompresja)
- Dirty IO schedulers - dla operacji blokujących na I/O (dostęp do plików, wywołania sieciowe)
# Rustler automatycznie wspiera dirty schedulers
#[rustler::nif(schedule = "DirtyCpu")]
fn heavy_computation(data: Binary) -> Binary {
// Ta funkcja nie zablokuje normalnego schedulera
process(data)
}Crash NIF-a = crash BEAM-a
To największe ryzyko. NIF działa w tym samym procesie systemowym co BEAM. Jeśli NIF napisany w C zrobi segfault (buffer overflow, null pointer dereference, use-after-free), cała maszyna wirtualna BEAM pada. Nie jeden proces Elixira - cały system. Supervisor tree nie pomoże, bo nie ma czego restartować.
To zaprzeczenie filozofii "let it crash". W normalnym Elixirze proces może upaść i zostać zrestartowany. NIF, który crashuje, zabiera ze sobą wszystko.
Rozwiązanie: pisz NIF-y w Rust. Borrow checker gwarantuje w czasie kompilacji, że nie ma wiszących wskaźników, podwójnego zwalniania pamięci ani wyścigów danych. Kod się nie skompiluje, jeśli zawiera potencjalne naruszenie pamięci. Biblioteka Rustler sprawia, że pisanie NIF-ów w Rust jest prostsze niż w C.
Memory leak
NIF alokuje pamięć natywną, która nie jest zarządzana przez garbage collector BEAM. Jeśli NIF zaalokuje pamięć i jej nie zwolni, masz wyciek pamięci, który będzie rósł do momentu, aż system operacyjny zabije proces BEAM (OOM killer).
W Rust ten problem praktycznie nie istnieje dzięki systemowi ownership - pamięć jest automatycznie zwalniana, gdy zmienna wychodzi z zakresu.
NIF vs alternatywy
NIF to nie jedyny sposób na uruchomienie natywnego kodu z Elixira. Oto porównanie:
| Mechanizm | Wydajność | Bezpieczeństwo | Złożoność |
|---|---|---|---|
| NIF | Najwyższa (brak narzutu) | Niskie (crash = crash BEAM) | Średnia |
| NIF w Rust (Rustler) | Najwyższa | Wysokie (borrow checker) | Niska |
| Port | Średnia (serialization overhead) | Wysokie (osobny proces OS) | Niska |
| Port Driver | Wysoka | Niskie (crash = crash BEAM) | Wysoka |
| :os.cmd | Niska (fork/exec) | Wysokie (osobny proces OS) | Najniższa |
Port to bezpieczna alternatywa: uruchamia natywny kod w osobnym procesie systemowym, komunikuje się przez stdin/stdout. Jeśli natywny kod crashuje, BEAM dalej działa. Ale płacisz za to serializacją danych na granicy procesów.
Kiedy Port zamiast NIF?
- Gdy natywny kod jest niestabilny lub niedojrzały
- Gdy przesyłasz niewiele danych (narzut serializacji jest pomijalny)
- Gdy operacja trwa długo i nie potrzebujesz minimalnej latencji
Kiedy NIF?
- Gdy liczy się każda mikrosekunda (kryptografia, real-time)
- Gdy przesyłasz duże ilości danych (binaria, obrazy)
- Gdy wywołujesz natywny kod tysiące razy na sekundę
Przykłady NIF-ów w ekosystemie Elixira
Nie musisz pisać NIF-ów od zera. Ekosystem Elixira jest pełen bibliotek, które używają NIF-ów pod spodem:
:crypto- kryptografia (OpenSSL) - dostarczany z Erlang/OTP:zlib- kompresja gzip - dostarczany z Erlang/OTP- Explorer - analiza danych (Polars w Rust)
- Nx + EXLA - operacje tensorowe (Google XLA)
- Bcrypt - hashowanie haseł
- Argon2 - hashowanie haseł (nowszym algorytmem)
- Jason - parser JSON (choć w czystym Elixirze, wariant NIF to
jsonrs) - ExWebRTC - WebRTC z kodekami w Rust
- Ecto SQL - adapter bazy danych (parsowanie protokołu PostgreSQL)
- Comeonin - framework do hashowania haseł, delegujący do NIF-ów
Podsumowanie
NIF to nie hack ani obejście. To świadomy mechanizm architektoniczny BEAM, który istnieje od początku Erlanga. Pozwala łączyć to, co BEAM robi najlepiej (współbieżność, niezawodność, dystrybucja) z tym, co robi najlepiej natywny kod (surowa wydajność obliczeniowa).
Zasada jest prosta:
- Logika biznesowa, routing, real-time, procesy → Elixir
- Ciężkie obliczenia, parsowanie, kryptografia → NIF w Rust
Nie pisz NIF-ów dla operacji, które Elixir obsługuje wystarczająco szybko. Ale gdy napotkasz wąskie gardło obliczeniowe - NIF w Rust przez Rustler to najprostsza i najbezpieczniejsza droga do wydajności bare metal, bez rezygnowania z niezawodności BEAM.
Masz system w Elixirze i podejrzewasz, że NIF mógłby rozwiązać Twoje wąskie gardło wydajnościowe? Porozmawiajmy - pomożemy zidentyfikować, gdzie natywny kod da największy zysk.