⚠️ Важно: Перед внедрением новых языков в проект сначала убедитесь, что код на 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?
- Компиляция: Нативный код (C/Rust/Zig) компилируется в динамическую библиотеку (
.so
,.dll
) - Загрузка: BEAM загружает библиотеку при старте модуля через
:erlang.load_nif/2
- Прямое выполнение: Функции выполняются в том же потоке, что и вызывающий 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?
- Запуск: Elixir запускает внешнюю программу как отдельный OS-процесс
- Обмен данными: Коммуникация через стандартные потоки (stdin/stdout)
- Изоляция: Падение внешней программы не влияет на BEAM
✅ Преимущества
- Полная безопасность: Изоляция гарантирует стабильность BEAM
- Языковая агностичность: Работает с любым языком программирования
- Простота отладки: Внешнюю программу можно тестировать отдельно
- Отсутствие зависимостей: Не требует специфичных для BEAM библиотек
❌ Недостатки
- Высокие накладные расходы: ~100-500 μs на вызов
- Сериализация: Требуется преобразование данных (обычно в JSON)
- Блокирующие вызовы: По умолчанию блокирует вызывающий процесс
🚗 Port Drivers — золотая середина
Port Drivers — производительная альтернатива Ports, но более сложная. Это драйверы на языке C, встроенные непосредственно в адресное пространство BEAM и работающие в отдельных потоках.
Как работают Port Drivers?
- Загрузка: BEAM загружает C-библиотеку в свое адресное пространство
- Выделение потока: Драйвер работает в отдельном потоке
- Асинхронность: Обмен данными через очередь сообщений
✅ Преимущества
- Скорость: Значительно быстрее обычных портов
- Безопасность: Меньший риск чем у 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-кейсы здесь опущены, чтобы сохранить баланс между теорией и применением.
🔍 Критерии выбора метода интеграции
Прежде чем перейти к примерам, определим ключевые факторы выбора:
- Производительность: Насколько быстро работает вызов
- Безопасность: Риск для стабильности BEAM
- Сложность реализации: Легко ли внедрить решение
- Поддержка языка: Насколько хорошо язык работает с 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
📊 Сводная таблица оптимальных методов
Язык | Оптимальный метод | Альтернативы | Когда использовать |
---|---|---|---|
C | NIF, Port Drivers | Ports | Максимальная производительность |
Rust | Rustler (NIF) | - | Безопасность + производительность |
Python | Ports | gRPC | Интеграция с ML/научными библиотеками |
Go | C-обёртки (NIF), gRPC | Ports | Использование Go-экосистемы |
Zig | Zigler (NIF) | - | Современная альтернатива C |
😴 Заключение
Интеграция Elixir с нативным кодом открывает новые горизонты для разработчиков, позволяя сочетать преимущества BEAM (масштабируемость, отказоустойчивость) с производительностью и библиотеками других языков. В статье мы рассмотрели ключевые методы интеграции: NIF, Dirty NIF, Ports, Port Drivers, Rustler, Zigler и gRPC, а также их оптимальные сценарии использования.
Помните, что выбор метода интеграции должен основываться на конкретных требованиях вашего проекта к производительности, безопасности и простоте разработки.
От автора
Благодарю вас за внимание к данной статье! Надеюсь, вам было интересно узнать про способы интеграции других языков в Elixir. Из всех вариантов мне показался наиболее сложным Port Drivers, потому что создание одного такого драйвера само по себе превращается в настоящий квест, успешно завершённый лишь после нескольких часов подбора подходящих методов на C. В остальном, интеграция других языков в Elixir оказалась вовсе несложной, скорее даже увлекательной.
Если вы обнаружили неточности в материале или у вас есть интересные дополнения — пожалуйста, напишите об этом в комментариях. Конструктивная обратная связь всегда ценна 😎
Для тех, кому интересно глубже погрузиться в мир технологий, архитектуры и разработки, приглашаю заглянуть в мой телеграм-канал 🖥, где я делюсь рецензиями на технические книги, полезными материалами по Elixir и не только 🤤