Node.js 26.1.0 (май 2026) получил встроенный экспериментальный модуль node:ffi, который позволяет вызывать функции из динамических библиотек (DLL, SO, dylib) прямо из JavaScript — без написания обёрток на C++ и без компиляции нативных аддонов. Это открывает доступ к тысячам существующих нативных библиотек, но требует осторожности: неправильный указатель или неверная сигнатура мгновенно «уронят» процесс.

Зачем FFI обычному разработчику
Представьте, что вам нужно воспользоваться мощной C-библиотекой — например, системной математикой libm, графикой raylib или даже SQLite. Раньше в Node.js был только один «официальный» путь: написать нативный аддон на C++ с использованием N-API или node-addon-api, скомпилировать его под каждую платформу и поддерживать совместимость с разными версиями Node.js. Это требовало знаний C++, инструментов сборки (node-gyp, CMake) и немало терпения.
FFI (Foreign Function Interface, «интерфейс внешних функций») меняет правила игры. Это механизм, позволяющий языку высокого уровня (в нашем случае JavaScript) напрямую вызывать функции из скомпилированных библиотек, зная лишь их сигнатуры — имена, типы аргументов и возвращаемые значения. Никакого промежуточного C++-кода, никакой компиляции при установке пакета.
До Node.js 26.1.0 разработчики использовали сторонние пакеты вроде ffi-napi или koffi, но это были пользовательские решения с собственными ограничениями. Теперь же в ядре Node.js появился родной node:ffi — официальный, встроенный и интегрированный с Permission Model.
Что привёз релиз 26.1.0
7 мая 2026 года вышла версия Node.js 26.1.0 (Current), и её главная изюминка — модуль node:ffi. Ключевые факты:
- Флаг: API скрыто за флагом
--experimental-ffi. Без него модуль недоступен. - Безопасность: если включена Permission Model, нужен дополнительный флаг
--allow-ffi. - Статус: экспериментальный (Stability 1). API может измениться в любой следующей версии, использовать в продакшене не рекомендуется.
- Автор: Paolo Insogna.
Модуль построен поверх libffi и использует современные возможности V8 для быстрых вызовов нативного кода [^10^].
Как это работает: три шага до нативной функции
Концепция проста, как ключ в замке:
- Открыть библиотеку — указать путь к
.so,.dylibили.dll. - Описать сигнатуру — сообщить Node.js, какие аргументы принимает функция и что возвращает.
- Вызвать — получить JavaScript-обёртку и работать с ней как с обычной функцией.
Минимальный рабочий пример
// Запускать с: node --experimental-ffi app.mjs
import { dlopen } from 'node:ffi';
// macOS: системная математическая библиотека
const path = '/usr/lib/libSystem.B.dylib';
const { functions } = dlopen(path, {
sqrt: { parameters: ['f64'], result: 'f64' },
});
console.log(functions.sqrt(2)); // 1.4142135623730951Здесь мы загрузили системную библиотеку libSystem.B.dylib, объявили, что функция sqrt принимает одно число с плавающей точкой (f64) и возвращает такое же, и сразу вызвали её. Никакого C++.
Кроссплатформенный путь к библиотеке
Чтобы не хардкодить расширения файлов, модуль предоставляет константу ffi.suffix:
import { suffix } from 'node:ffi';
// 'dylib' на macOS, 'so' на Linux, 'dll' на Windows
const libPath = `./libmyawesome.${suffix}`;Типы данных: словарь для разговора с C
Поскольку JavaScript и C «говорят на разных языках», node:ffi использует текстовые обозначения типов. Вот основные из них:
| Обозначение | Что означает | Пример значения в JS |
i8, int8 | 8-битное знаковое целое | number |
u8, uint8, bool, char | 8-битное беззнаковое / логическое / символ | number (для bool передавайте 0 или 1) |
i16, int16 | 16-битное знаковое целое | number |
u16, uint16 | 16-битное беззнаковое целое | number |
i32, int32 | 32-битное знаковое целое | number |
u32, uint32 | 32-битное беззнаковое целое | number |
i64, int64 | 64-битное знаковое целое | bigint |
u64, uint64 | 64-битное беззнаковое целое | bigint |
f32, float | 32-битное float | number |
f64, double | 64-битное double | number |
pointer, ptr | Указатель | bigint или null |
string, str | C-строка (NUL-terminated UTF-8) | string |
buffer | Указатель на Buffer | Buffer |
arraybuffer | Указатель на ArrayBuffer | ArrayBuffer |
function | Указатель на функцию (callback) | bigint |
Важный нюанс: для 64-битных целых (i64/u64) нужно передавать bigint, а не обычные числа. Для логического типа bool принимаются числа 0 и 1; JavaScript-ные true/false напрямую не подходят.
Управление памятью и указателями
FFI — это мощь, но и ответственность. node:ffi не следит за временем жизни объектов и не проверяет корректность указателей. Официальная документация предупреждает: «Некорректные указатели, неправильные сигнатуры или обращение к памяти после ее освобождения могут привести к сбою процесса или повреждению памяти».
Чтение и запись по адресу
Модуль предоставляет «сырые» хелперы для работы с памятью:
import {
getInt32,
setInt32,
toString,
toBuffer,
} from 'node:ffi';
// Записать 32-битное целое по адресу ptr (bigint) со смещением 0
setInt32(ptr, 0, 42);
// Прочитать обратно
console.log(getInt32(ptr, 0)); // 42
// Прочитать C-строку по указателю
const str = toString(ptr);
// Обёрнуть кусок нативной памяти в Buffer (zero-copy)
const buf = toBuffer(ptr, 1024, false);При использовании toBuffer и toArrayBuffer с copy: false вы получаете zero-copy представление чужой памяти. Если нативная сторона освободит эту память, а JavaScript продолжит с ней работать — последствия непредсказуемы.
Callbacks: JavaScript-функции для C
Иногда нативная библиотека ожидает указатель на функцию-обработчик. node:ffi умеет создавать такие обратные вызовы:
const { DynamicLibrary } = require('node:ffi');
const lib = new DynamicLibrary('./mylib.so');
const callbackPtr = lib.registerCallback(
{ parameters: ['i32'], result: 'i32' },
(value) => value * 2,
);
// callbackPtr — bigint, который можно передать в нативную функциюОграничения строгие: callback должен выполняться в том же системном потоке, не должен бросать исключения, возвращать промисы или вызывать library.close() изнутри себя.
Поддерживаемые платформы
На момент релиза 26.1.0 встроенная libffi поддерживает:
- macOS:
arm64,x64 - Windows:
arm64,x64 - Linux:
arm,arm64,x64 - FreeBSD:
arm,arm64,x64
Другие платформы требуют сборки Node.js с внешней libffi (флаг --shared-ffi). Неофициальная GN-сборка node:ffi не поддерживает.
FFI vs N-API: когда что выбирать
| Критерий | node:ffi | N-API / нативные аддоны |
| Нужно писать C/C++ | Нет | Да |
| Компиляция при установке | Нет | Да (node-gyp/CMake) |
| Скорость | Высокая (прямые вызовы через V8 Fast API) | Максимальная (нет маршаллинга) |
| Безопасность | Низкая (легко сломать память) | Выше (типизация на уровне C++) |
| Сложные структуры | Ограниченно | Полный контроль |
| Стабильность API | Экспериментальная | Стабильная |
Правило большого пальца: если вам нужно быстро вызвать пару функций из готовой .so/.dll — node:ffi идеален. Если вы разрабатываете сложную нативную интеграцию с кастомными структурами данных и жёсткими требованиями к безопасности — N-API по-прежнему надёжнее.
Безопасность и Permission Model
Поскольку FFI открывает прямой доступ к системным вызовам и памяти, он считается привилегированной операцией. Если вы используете Permission Model (флаг --experimental-permission), вызов node:ffi потребует явного разрешения --allow-ffi. Это защищает от сценариев, когда скомпрометированный пакет из npm пытается загрузить произвольный системный код.
Также модуль реализует протокол явного управления ресурсами (Symbol.dispose), что позволяет использовать using для автоматического закрытия библиотек:
{
using handle = dlopen('./mylib.so', { ... });
// библиотека автоматически закроется при выходе из блока
}Что дальше
node:ffi — это эксперимент, но за ним стоит серьёзная инженерная работа и долгая история обсуждений в сообществе Node.js . Если модуль стабилизируется, он кардинально упростит интеграцию с нативным миром: от системных утилит до игровых движков и научных библиотек.
Пока же — пробуйте, изучайте документацию и помните: с большой силой FFI приходит большая ответственность. Начните с простых вызовов, тщательно проверяйте сигнатуры и никогда не предполагайте, что «указатель ещё валиден», если нативная библиотека могла его освободить.