3279 Words

Картинка

⚠️ Важно: Перед внедрением новых языков в проект сначала убедитесь, что код на Elixir был максимально оптимизирован. В большинстве случаев грамотная оптимизация текущего кода значительно улучшает производительность приложения и иногда даёт лучшие результаты, нежели добавление другого языка программирования.

Elixir и Erlang — отличные языки для разработки масштабируемых и отказоустойчивых систем. Но иногда нужно выжать максимум производительности или использовать библиотеку, доступную лишь на другом языке. Если вы попали именно в такую ситуацию или просто хотите узнать, как совместить два ваших любимых языка программирования, эта статья для вас!


🔍 Когда нужна интеграция с нативным кодом

Прежде чем углубляться в технические детали, важно понять, когда стоит рассматривать интеграцию с нативным кодом:

1. Вычислительно-интенсивные задачи

BEAM создан для конкурентности, а не для вычислений. Если ваше узкое место — CPU-bound операции:

  • 🧮 Математические вычисления — Линейная алгебра или операции с матрицами
  • 🔐 Криптография — Шифрование/Дешифрование больших объёмов данных
  • 🎨 Обработка медиа — Изображения, видео, аудио
  • 🧠 Машинное обучение - инференс моделей, векторные операции

2. Аппаратное взаимодействие

Когда требуется низкоуровневый доступ к оборудованию:

  • 📟 Embedded-системы — Raspberry Pi, микроконтроллеры (Для этого есть Nerves в Elixir, но его иногда может не хватить)
  • 🎮 Специфические драйверы — нестандартные устройства
  • 📊 GPU-вычисления — CUDA, OpenCL

3. Переиспользование существующего кода

  • 🏛️ Проверенные временем библиотеки на C/C++
  • 📚 Экосистемные преимущества других языков (например, Python для ML)
  • 🔧 Избегание “изобретения велосипеда”

4. Профилирование и узкие места

Типичные признаки, что пора задуматься о нативном коде:

# До оптимизации - 1000мс
defmodule SlowModule do
  def process_data(data) do
    # Потенциальное узкое место для нативной оптимизации
    Enum.reduce(data, 0, fn x, acc -> complex_calculation(x) + acc end)
  end

  defp complex_calculation(x) do
    # Представьте CPU-интенсивные вычисления
    # которые плохо масштабируются в BEAM
  end
end

# Вы исчерпали возможности оптимизации на Elixir и всё ещё медленно?
# Возможно, пора подключать нативный код!

Нативные расширения превращают Elixir в универсальный инструмент, где BEAM решает всё, кроме критических вычислений. Это открывает двери к ML, hardware, видеообработке и всему, где важна скорость.


⚖️ Сравнение методов интеграции

Выбор правильного метода интеграции критически важен для успеха проекта. Вот сравнительная таблица методов интеграции с их плюсами и минусами:

МетодСкоростьБезопасность BEAMСложность реализацииПоддержка языковКоммуникационные затратыАсинхронностьИдеальные сценарии использования
NIF⚡⚡⚡⚡⚡❌ Опасно🔧🔧🔧 СредняяC, C++Нет (прямой вызов)❌ Блокирует планировщикМикросервисы, долгие CPU-bound задачи
Dirty NIF⚡⚡⚡⚡⚠️ Условно безопасно🔧🔧🔧 СредняяC, C++Нет (прямой вызов)✅ Не блокирует основной планировщикДолгие вычисления (>1ms)
Port⚡⚡✅✅ Полностью безопасно🔧 ПростаяЛюбойВысокие (IPC)✅ Процессная изоляцияPython, Go, Bash-скрипты
Port Driver⚡⚡⚡⚡✅ Безопасно🔧🔧🔧🔧 СложнаяC, C++Низкие✅ Выделенный потокОбработка видео/аудио
gRPC⚡⚡✅✅ Полностью безопасно🔧🔧 СредняяЛюбой с поддержкой gRPCСредние (сеть)✅ Отдельный сервисМикросервисная архитектура
Rustler⚡⚡⚡⚡✅ Безопасно🔧🔧 СредняяRustНет (прямой вызов)✅ Поддержка асинхронного APIАльтернатива C NIF
Zigler⚡⚡⚡⚡⚡✅ Безопасно🔧🔧 СредняяZigНет (прямой вызов)✅ Безопаснее стандартных NIFАльтернатива C в NIF

Визуальное сравнение по ключевым метрикам

Скорость:            Безопасность:         Простота:
NIF         █████    Port        █████     Port        █████
Dirty NIF   ████     Dirty NIF   ███       gRPC        ████
Rustler     ████     Rustler     ████      Rustler     ████
Zigler      █████    Zigler      ████      Zigler      ████
Port Driver ████     Port Driver ████      NIF         ███
Port        ██       gRPC        █████     Port Driver █
gRPC        ██       NIF         █         Dirty NIF   ███

⚠️ Важно: Перед внедрением новых языков в проект сначала убедитесь, что код на Elixir был максимально оптимизирован. В большинстве случаев грамотная оптимизация текущего кода значительно улучшает производительность приложения и иногда даёт лучшие результаты, нежели добавление другого языка программирования.

Elixir и Erlang — отличные языки для разработки масштабируемых и отказоустойчивых систем. Но иногда нужно выжать максимум производительности или использовать библиотеку, доступную лишь на другом языке. Если вы попали именно в такую ситуацию или просто хотите узнать, как совместить два ваших любимых языка программирования, эта статья для вас!


🔍 Когда нужна интеграция с нативным кодом

Прежде чем углубляться в технические детали, важно понять, когда стоит рассматривать интеграцию с нативным кодом:

1. Вычислительно-интенсивные задачи

BEAM создан для конкурентности, а не для вычислений. Если ваше узкое место — CPU-bound операции:

  • 🧮 Математические вычисления — Линейная алгебра или операции с матрицами
  • 🔐 Криптография — Шифрование/Дешифрование больших объёмов данных
  • 🎨 Обработка медиа — Изображения, видео, аудио
  • 🧠 Машинное обучение - инференс моделей, векторные операции

2. Аппаратное взаимодействие

Когда требуется низкоуровневый доступ к оборудованию:

  • 📟 Embedded-системы — Raspberry Pi, микроконтроллеры (Для этого есть Nerves в Elixir, но его иногда может не хватить)
  • 🎮 Специфические драйверы — нестандартные устройства
  • 📊 GPU-вычисления — CUDA, OpenCL

3. Переиспользование существующего кода

  • 🏛️ Проверенные временем библиотеки на C/C++
  • 📚 Экосистемные преимущества других языков (например, Python для ML)
  • 🔧 Избегание “изобретения велосипеда”

4. Профилирование и узкие места

Типичные признаки, что пора задуматься о нативном коде:

# До оптимизации - 1000мс
defmodule SlowModule do
  def process_data(data) do
    # Потенциальное узкое место для нативной оптимизации
    Enum.reduce(data, 0, fn x, acc -> complex_calculation(x) + acc end)
  end

  defp complex_calculation(x) do
    # Представьте CPU-интенсивные вычисления
    # которые плохо масштабируются в BEAM
  end
end

# Вы исчерпали возможности оптимизации на Elixir и всё ещё медленно?
# Возможно, пора подключать нативный код!

Нативные расширения превращают Elixir в универсальный инструмент, где BEAM решает всё, кроме критических вычислений. Это открывает двери к ML, hardware, видеообработке и всему, где важна скорость.


⚖️ Сравнение методов интеграции

Выбор правильного метода интеграции критически важен для успеха проекта. Вот сравнительная таблица методов интеграции с их плюсами и минусами:

МетодСкоростьБезопасность BEAMСложность реализацииПоддержка языковКоммуникационные затратыАсинхронностьИдеальные сценарии использования
NIF⚡⚡⚡⚡⚡❌ Опасно🔧🔧🔧 СредняяC, C++Нет (прямой вызов)❌ Блокирует планировщикМикросервисы, долгие CPU-bound задачи
Dirty NIF⚡⚡⚡⚡⚠️ Условно безопасно🔧🔧🔧 СредняяC, C++Нет (прямой вызов)✅ Не блокирует основной планировщикДолгие вычисления (>1ms)
Port⚡⚡✅✅ Полностью безопасно🔧 ПростаяЛюбойВысокие (IPC)✅ Процессная изоляцияPython, Go, Bash-скрипты
Port Driver⚡⚡⚡⚡✅ Безопасно🔧🔧🔧🔧 СложнаяC, C++Низкие✅ Выделенный потокОбработка видео/аудио
gRPC⚡⚡✅✅ Полностью безопасно🔧🔧 СредняяЛюбой с поддержкой gRPCСредние (сеть)✅ Отдельный сервисМикросервисная архитектура
Rustler⚡⚡⚡⚡✅ Безопасно🔧🔧 СредняяRustНет (прямой вызов)✅ Поддержка асинхронного APIАльтернатива C NIF
Zigler⚡⚡⚡⚡⚡✅ Безопасно🔧🔧 СредняяZigНет (прямой вызов)✅ Безопаснее стандартных NIFАльтернатива C в NIF

Визуальное сравнение по ключевым метрикам

Скорость:            Безопасность:         Простота:
NIF         █████    Port        █████     Port        █████
Dirty NIF   ████     Dirty NIF   ███       gRPC        ████
Rustler     ████     Rustler     ████      Rustler     ████
Zigler      █████    Zigler      ████      Zigler      ████
Port Driver ████     Port Driver ████      NIF         ███
Port        ██       gRPC        █████     Port Driver █
gRPC        ██       NIF         █         Dirty NIF   ███

🛠️ Немного о механизмах интеграции

🧠 NIFs — максимальная производительность

Native Implemented Functions (NIFs) — самый быстрый способ интеграции, но и самый опасный. Они выполняются напрямую в потоке планировщика BEAM, обеспечивая молниеносную скорость за счет отсутствия накладных расходов.

Как работают NIF?

  1. Компиляция: Нативный код (C/Rust/Zig) компилируется в динамическую библиотеку (.so, .dll)
  2. Загрузка: BEAM загружает библиотеку при старте модуля через :erlang.load_nif/2
  3. Прямое выполнение: Функции выполняются в том же потоке, что и вызывающий Elixir-код

✅ Преимущества

  • Молниеносная скорость: Вызов занимает ~0.1-1 μs (в 100-1000 раз быстрее Ports)
  • Доступ к BEAM API: Прямая работа с термами Erlang
  • Отсутствие сериализации: Нет накладных расходов на кодирование/декодирование данных
  • Распространение: Библиотека идёт вместе с OTP-приложением

❌ Недостатки

  • Риск падения всей ВМ: Ошибка в NIF убьёт весь BEAM
  • Блокировка планировщика: Долгие NIF замораживают многопоточность
  • Сложность отладки: Трудно обнаружить утечки памяти
  • Платформозависимость: Требуется компиляция под каждую архитектуру

Dirty NIFs — безопасная альтернатива

С Erlang/OTP 20+ появились Dirty NIFs — специальный вид NIF, который исполняется в выделенном пуле потоков, что позволяет выполнять долгие вычисления без блокировки планировщика BEAM.

// Определение Dirty NIF
static ErlNifFunc nif_funcs[] = {
    {"long_computation", 1, long_computation_nif, ERL_NIF_DIRTY_CPU}
};

💡 Совет: Используйте ERL_NIF_DIRTY_CPU для CPU-bound операций и ERL_NIF_DIRTY_IO для операций ввода/вывода

🔌 Ports — полная изоляция

Ports — способ для взаимодействия с внешними программами через стандартные потоки ввода-вывода (stdin/stdout). Это самый безопасный способ интеграции, так как внешняя программа запускается в отдельном процессе ОС.

Как работают Ports?

  1. Запуск: Elixir запускает внешнюю программу как отдельный OS-процесс
  2. Обмен данными: Коммуникация через стандартные потоки (stdin/stdout)
  3. Изоляция: Падение внешней программы не влияет на BEAM

✅ Преимущества

  • Полная безопасность: Изоляция гарантирует стабильность BEAM
  • Языковая агностичность: Работает с любым языком программирования
  • Простота отладки: Внешнюю программу можно тестировать отдельно
  • Отсутствие зависимостей: Не требует специфичных для BEAM библиотек

❌ Недостатки

  • Высокие накладные расходы: ~100-500 μs на вызов
  • Сериализация: Требуется преобразование данных (обычно в JSON)
  • Блокирующие вызовы: По умолчанию блокирует вызывающий процесс

🚗 Port Drivers — золотая середина

Port Drivers — производительная альтернатива Ports, но более сложная. Это драйверы на языке C, встроенные непосредственно в адресное пространство BEAM и работающие в отдельных потоках.

Как работают Port Drivers?

  1. Загрузка: BEAM загружает C-библиотеку в свое адресное пространство
  2. Выделение потока: Драйвер работает в отдельном потоке
  3. Асинхронность: Обмен данными через очередь сообщений

✅ Преимущества

  • Скорость: Значительно быстрее обычных портов
  • Безопасность: Меньший риск чем у NIF
  • Асинхронность: Поддержка неблокирующих операций
  • Удобство: Не требует запуска отдельного процесса

❌ Недостатки

  • Сложность: Требует знания C API Erlang
  • Ограниченность: Работает только с C/C++
  • Меньшая документация: Не так много примеров и руководств

🐊 Zigler - Обёртка над NIFs для Zig

Zigler — Библиотека, позволяющая писать нативные расширения на языке Zig. Встраивает компилятор Zig прямо в цикл компиляции Elixir и позволяет напрямую писать код на Zig в модулях Elixir

✅ Преимущества

  • Минимальные накладные расходы на вызов нативного кода
  • Безопасность памяти без сборщика мусора
  • Прямой доступ к низкоуровневым операциям
  • Более простая компиляция в сравнении с другими нативными расширениями
  • Поддержка горячей перезагрузки кода

❌ Недостатки

  • Zig — относительно молодой язык с меньшим сообществом
  • Более высокий порог входа для разработчиков Elixir
  • Ограниченная экосистема библиотек в сравнении с Rust

Rustler - Обёртка над NIFs для Rust

Rustler — это библиотека для создания нативных расширений Erlang/Elixir на языке Rust. Обеспечивает безопасный биндинг между Rust и Erlang/Elixir, позволяя писать NIFs на Rust.

✅ Преимущества

  • Безопасность памяти на уровне компиляции
  • Высокая производительность для вычислительно сложных задач
  • Богатая экосистема пакетов Rust (crates)
  • Защита от сбоев в нативном коде (защита BEAM от падения)
  • Параллельное выполнение без блокировки планировщика BEAM

❌ Недостатки

  • Усложненный процесс сборки и зависимостей
  • Требует знания двух различных парадигм программирования
  • Возможные проблемы с совместимостью версий
  • Потенциальные узкие места на границе сред выполнения

🌐 gRPC и другие протоколы

Для более сложных сценариев взаимодействия с внешними сервисами, особенно в микросервисной архитектуре, gRPC и подобные протоколы предоставляют структурированный и масштабируемый способ интеграции.

✅ Преимущества gRPC для Elixir

  • Схема контрактов: Строгие типы через Protocol Buffers
  • Двунаправленный стриминг: Поддержка потоковой передачи в обе стороны
  • Кросс-платформенность: Поддержка множества языков
  • Производительность: Более эффективен чем REST/JSON

❗️Пример с gRPC был слишком объёмен, чтобы рассмотреть его в рамках этой статьи, из-за чего он не был включён в неё. Подробнее о gRPC на Elixir можно почитать вот тут - elixir-grpc


🧪 Практические примеры интеграции

📚 Цель статьи — дать обзор доступных способов интеграции с нативным кодом. Глубокие реализации и edge-кейсы здесь опущены, чтобы сохранить баланс между теорией и применением.

🔍 Критерии выбора метода интеграции

Прежде чем перейти к примерам, определим ключевые факторы выбора:

  1. Производительность: Насколько быстро работает вызов
  2. Безопасность: Риск для стабильности BEAM
  3. Сложность реализации: Легко ли внедрить решение
  4. Поддержка языка: Насколько хорошо язык работает с BEAM

1. ⚙️ C — максимальная производительность с NIF и Port Drivers

Оптимальные методы:

  • NIF — для мгновенных операций (<1ms)
  • Port Drivers — для долгих или асинхронных задач

Почему именно так?

C имеет прямую совместимость с BEAM, что делает его идеальным для:

  • Критически важных по производительности задач
  • Низкоуровневых операций (работа с железом, GPU)
  • Интеграции с существующими C-библиотеками

Пример 1: Быстрый NIF для хеширования (оптимально для C)

// hash_nif.c
#include <erl_nif.h>
#include <openssl/sha.h>

static ERL_NIF_TERM sha256_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifBinary input;
    if (!enif_inspect_binary(env, argv[0], &input)) {
        return enif_make_badarg(env);
    }

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256(input.data, input.size, hash);

    ErlNifBinary output;
    enif_alloc_binary(SHA256_DIGEST_LENGTH, &output);
    memcpy(output.data, hash, SHA256_DIGEST_LENGTH);

    return enif_make_binary(env, &output);
}

static ErlNifFunc nif_funcs[] = {
    {"sha256", 1, sha256_nif}
};

ERL_NIF_INIT(Elixir.CryptoNif, nif_funcs, NULL, NULL, NULL, NULL)

Elixir часть:

defmodule CryptoNif do
  @on_load :load_nif
  def load_nif do
    :erlang.load_nif(Path.expand("./hash_nif"), 0)
  end

  def sha256(_data), do: raise "NIF not loaded!"
end

Компиляция C-исходника:

Для запуска данного примера вам нужна openssl библиотека

# Для Ubuntu/Debian
sudo apt-get install libssl-dev
# Для CentOS/RHEL
sudo yum install openssl-devel
# Для macOS (если используете Homebrew)
brew install openssl
gcc -shared -fPIC -o hash_nif.so hash_nif.c \
  -I /usr/lib/erlang/erts-15.2.7/include \ # Путь до Erlang зависит от вашей системы
  -I /usr/include/openssl \
  -L /usr/lib/openssl \
  -lssl -lcrypto

Результат:

iex(1)> CryptoNif.sha256("test")
<<159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163, 191, 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8>>

Пример 2: Port Driver для реверса строки

// reverse_driver.c
#include "erl_driver.h"
#include <string.h>

typedef struct {
    ErlDrvPort port;
} DriverData;

static void reverse_and_send(ErlDrvData drv_data, char* buf, ErlDrvSizeT len) {
    DriverData* d = (DriverData*)drv_data;

    for (ErlDrvSizeT i = 0; i < len / 2; i++) {
        char temp = buf[i];
        buf[i] = buf[len - 1 - i];
        buf[len - 1 - i] = temp;
    }

    driver_output(d->port, buf, len);
}

static ErlDrvData driver_start(ErlDrvPort port, char* command) {
    DriverData* d = (DriverData*)driver_alloc(sizeof(DriverData));
    d->port = port;
    return (ErlDrvData)d;
}

static void driver_stop(ErlDrvData drv_data) {
    DriverData* d = (DriverData*)drv_data;
    driver_free(d);
}

static ErlDrvEntry driver_entry = {
    .init = NULL,
    .start = driver_start,
    .stop = driver_stop,
    .output = reverse_and_send,
    .ready_input = NULL,
    .ready_output = NULL,
    .driver_name = "reverse_driver",
    .finish = NULL,
    .handle = NULL,
    .control = NULL,
    .timeout = NULL,
    .outputv = NULL,
    .ready_async = NULL,
    .flush = NULL,
    .call = NULL,
    .extended_marker = ERL_DRV_EXTENDED_MARKER,
    .major_version = ERL_DRV_EXTENDED_MAJOR_VERSION,
    .minor_version = ERL_DRV_EXTENDED_MINOR_VERSION,
    .driver_flags = 0,
    .handle2 = NULL,
    .process_exit = NULL,
    .stop_select = NULL
};


DRIVER_INIT(reverse_driver) {
    return &driver_entry;
}

Elixir часть:

defmodule ReverseString do
  def start() do
    :ok = :erl_ddll.load_driver('./', 'reverse_driver')
    Port.open({:spawn, 'reverse_driver'}, [:binary])
  end

  def reverse(port, string) when is_binary(string) do
    true = Port.command(port, string)
    receive do
      {^port, {:data, result}} -> result
    after
      1000 -> {:error, :timeout}
    end
  end

  def stop(port) do
    Port.close(port)
  end
end

Компиляция C-исходника:

gcc -std=gnu99 -shared -fPIC -o reverse_driver.so reverse_driver.c \
    -I/usr/lib/erlang/erts-15.2.7/include/ # Путь до Erlang зависит от вашей системы

Результат:

iex(1)> port = ReverseString.start
#Port<0.6>
iex(2)> ReverseString.reverse(port, "hello")
"olleh"

2. 🦀 Rust — безопасность и производительность с Rustler

Оптимальный метод: Rustler (специализированная обёртка над NIF)

Почему Rustler?

  • Полная безопасность памяти
  • Удобные макросы для работы с BEAM терминами
  • Автоматическая обработка ошибок
  • Поддержка асинхронных задач

Пример: Параллельная обработка данных

Добавляем в mix.exe Rustler (На данный момент актуальная версия - 0.36.1)

  defp deps do
    [
      {:rustler, "~> 0.36.1"}
    ]
  end

Добавляем Rust-проект в Elixir

mix deps.get
mix rustler.new

This is the name of the Elixir module the NIF module will be registered to.
Module name > RustUtils
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (rustutils) > # Тут просто нажимаем Enter (Ну или переименуйте на более удобное название)
// lib.rs
use rayon::prelude::*;

#[rustler::nif]
fn parallel_double(input: Vec<i64>) -> Vec<i64> {
    input.par_iter().map(|&x| x * 2).collect()
}

rustler::init!("Elixir.RustUtils");

Elixir часть:

defmodule RustUtils do
  use Rustler, otp_app: :rust_utils, crate: "rustutils"

  def parallel_double(_list), do: :erlang.nif_error(:not_loaded)
end

Результат:

RustUtils.parallel_double([1, 2, 3])
# => [2, 4, 6]

3. 🐍 Python — простота интеграции через Ports

Оптимальный метод: Ports

Почему Ports?

  • Полная изоляция процессов
  • Простота отладки
  • Доступ ко всему Python-экосистеме
  • Поддержка долгих операций (ML модели и т.д.)

Пример: Создание своей модели и интеграция с TensorFlow

Создаём модель

# create_model.py
import tensorflow as tf
import numpy as np

model = tf.keras.Sequential([
    tf.keras.layers.Dense(1, input_shape=(3,), use_bias=False)
])

model.set_weights([np.array([[1.0], [1.0], [1.0]])])

model.save("my_model.h5")
print("✅ Model saved to my_model.h5")
#tensorflow_port
import sys
import json
import numpy as np
import tensorflow as tf
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
tf.get_logger().setLevel('ERROR')

model = tf.keras.models.load_model('my_model.h5')

def predict(data):
    try:
        if isinstance(data, str):
            data = json.loads(data)

        input_array = np.array(data, dtype=np.float32).reshape(1, 3)
        prediction = model.predict(input_array)
        return json.dumps({"status": "success", "result": prediction.tolist()})
    except Exception as e:
        return json.dumps({"status": "error", "message": str(e)})

if __name__ == "__main__":
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue

        try:
            response = predict(line)
            sys.stdout.write(response + "\n")
            sys.stdout.flush()
        except Exception as e:
            error = json.dumps({"status": "error", "message": str(e)})
            sys.stdout.write(error + "\n")
            sys.stdout.flush()

Elixir часть:

defmodule TensorflowPort do
  @timeout 5_000

  def start do
    Port.open(
      {:spawn, "python3 tensorflow_port.py"},
      [:binary, :use_stdio, :exit_status, :stderr_to_stdout, {:line, 1024}]
    )
  end

  def predict(port, input_data) do
    input_data
    |> Jason.encode!()
    |> then(&Port.command(port, &1 <> "\n"))

    wait_for_response(port)
  end

  defp wait_for_response(port) do
    receive do
      {^port, {:data, {:eol, line}}} ->
        case Jason.decode(line) do
          {:ok, %{"status" => "success", "result" => result}} -> {:ok, result}
          {:ok, %{"status" => "error", "message" => msg}} -> {:error, msg}
          _ -> wait_for_response(port)
        end

      {^port, {:exit_status, status}} ->
        {:error, "Python process exited with status #{status}"}
    after
      @timeout -> {:error, :timeout}
    end
  end
end

Результат:

Для работы программы нужно будет зайти в Python-окружение и оттуда запустить iex -S mix

(venv)
iex(1)> port = TensorflowPort.start
#Port<0.10>
iex(2)> TensorflowPort.predict(port, [1,2,3])
{:ok, [[6.0]]}

4. 🐹 Go — эффективность через C-обёртки или gRPC

Оптимальные методы:

  • C-обёртки для NIF — когда нужна максимальная производительность
  • gRPC — для сложных взаимодействий и микросервисов

C-обёртка для Go кода (оптимально для производительности)

// fib.go
package main

import "C"

func GoFib(n C.int) C.int {
	a, b := 0, 1
	for i := 0; i < int(n); i++ {
		a, b = b, a+b
	}
	return C.int(a)
}

func main() {}

C-обёртка:

// c_src/go_nif.c
#include <erl_nif.h>
#include "libfib.h"

static ERL_NIF_TERM go_fib_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int n;
    if (!enif_get_int(env, argv[0], &n)) {
        return enif_make_badarg(env);
    }
    return enif_make_int(env, GoFib(n));
}

static ErlNifFunc nif_funcs[] = {
    {"go_fib", 1, go_fib_nif}
};

ERL_NIF_INIT(Elixir.NifGo, nif_funcs, NULL, NULL, NULL, NULL)

Elixir часть:

defmodule NifGo do
  @on_load :load_nif

  def load_nif do
    nif_path = Path.expand("priv/go_nif", File.cwd!())

    # Предварительная загрузка Go-библиотеки
    :erlang.load_nif(Path.expand("priv/libfib", File.cwd!()), 0)

    # Загрузка основной NIF-библиотеки
    :erlang.load_nif(nif_path, 0)
  end

  def go_fib(_n), do: raise("NIF not loaded!")
end

Результат:

go build -buildmode=c-shared -o priv/libfib.so fib.go

gcc -shared -fPIC \
    -I/usr/lib/erlang/erts-15.2.7/include/ \ # Путь до Erlang зависит от вашей системы
    -I./priv \
    -o priv/go_nif.so \
    c_src/go_nif.c \
    ./priv/libfib.so \
    -Wl,-rpath,'$ORIGIN'

LD_LIBRARY_PATH=./priv iex -S mix

iex(1)> NifGo.go_fib(3)
2

⚠️ Если вам не хочется возиться с компиляцией и ABI, можно использовать gRPC или Ports вместо cgo. Это безопаснее, хоть и медленнее.

5. ⚡ Zig — Интеграция через Zigler

Оптимальный метод: Zigler (специализированная обёртка для Zig)

Почему Zigler?

  • Простота как у Rustler
  • Производительность как у C
  • Современные фичи языка (comptime, etc.)
  • Безопаснее чистого C

Пример: Быстрая обработка бинарных данных

Добавьте Zigler в mix.exe:

def deps do
  [
    {:zigler, "~> 0.13.2", runtime: false}
  ]
end

lib/nif_zig.ex:

defmodule NifZig do
  use Zig, otp_app: :zigler

  ~Z"""
  pub fn string_count(string: []u8) i64 {
    return @intCast(string.len);
  }

  pub fn list_sum(array: []f64) f64 {
    var sum: f64 = 0.0;
    for(array) | item | {
      sum += item;
    }
    return sum;
  }
  """
end

Результат:

iex(3)> NifZig.string_count("hello")
5
iex(4)> NifZig.list_sum([1.2,2.3,3.4,4.5])
11.4

📊 Сводная таблица оптимальных методов

ЯзыкОптимальный методАльтернативыКогда использовать
CNIF, Port DriversPortsМаксимальная производительность
RustRustler (NIF)-Безопасность + производительность
PythonPortsgRPCИнтеграция с ML/научными библиотеками
GoC-обёртки (NIF), gRPCPortsИспользование Go-экосистемы
ZigZigler (NIF)-Современная альтернатива C

😴 Заключение

Интеграция Elixir с нативным кодом открывает новые горизонты для разработчиков, позволяя сочетать преимущества BEAM (масштабируемость, отказоустойчивость) с производительностью и библиотеками других языков. В статье мы рассмотрели ключевые методы интеграции: NIF, Dirty NIF, Ports, Port Drivers, Rustler, Zigler и gRPC, а также их оптимальные сценарии использования.

Помните, что выбор метода интеграции должен основываться на конкретных требованиях вашего проекта к производительности, безопасности и простоте разработки.


От автора

Благодарю вас за внимание к данной статье! Надеюсь, вам было интересно узнать про способы интеграции других языков в Elixir. Из всех вариантов мне показался наиболее сложным Port Drivers, потому что создание одного такого драйвера само по себе превращается в настоящий квест, успешно завершённый лишь после нескольких часов подбора подходящих методов на C. В остальном, интеграция других языков в Elixir оказалась вовсе несложной, скорее даже увлекательной.

Если вы обнаружили неточности в материале или у вас есть интересные дополнения — пожалуйста, напишите об этом в комментариях. Конструктивная обратная связь всегда ценна 😎

Для тех, кому интересно глубже погрузиться в мир технологий, архитектуры и разработки, приглашаю заглянуть в мой телеграм-канал 🖥, где я делюсь рецензиями на технические книги, полезными материалами по Elixir и не только 🤤