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:

  1. Kompilacja - kod natywny (C/Rust) kompiluje się do biblioteki współdzielonej (.so na Linuxie, .dylib na macOS)
  2. Ładowanie - BEAM ładuje bibliotekę do pamięci przez erlang:load_nif/2
  3. Rejestracja - każda funkcja NIF jest mapowana na odpowiadającą jej funkcję Elixira/Erlanga
  4. Wywołanie - gdy Elixir wywołuje funkcję, BEAM przekazuje sterowanie bezpośrednio do natywnego kodu
  5. 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:

MechanizmWydajnośćBezpieczeństwoZłożoność
NIFNajwyższa (brak narzutu)Niskie (crash = crash BEAM)Średnia
NIF w Rust (Rustler)NajwyższaWysokie (borrow checker)Niska
PortŚrednia (serialization overhead)Wysokie (osobny proces OS)Niska
Port DriverWysokaNiskie (crash = crash BEAM)Wysoka
:os.cmdNiska (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.