Интерфейс взаимодействия с внешними функциями (FFI)
Интерфейс взаимодействия с внешними функциями Deno (FFI) позволяет коду JavaScript и TypeScript вызывать функции из динамических библиотек, написанных на таких языках, как C, C++ или Rust. Это позволяет напрямую интегрировать производительность и возможности нативного кода в приложения Deno.
Справочная документация по Deno FFI
Введение в FFI
FFI обеспечивает связь между средой выполнения JavaScript в Deno и нативным кодом. Это позволяет:
- Использовать существующие нативные библиотеки в приложениях Deno
- Реализовывать критически важный для производительности код на таких языках, как Rust или C
- Получать доступ к API операционной системы и аппаратным функциям, недоступным напрямую из JavaScript
Реализация FFI в Deno основана на API Deno.dlopen, который загружает динамические библиотеки и создаёт привязки JavaScript для экспортируемых ими функций.
Вопросы безопасности
Для FFI требуется явное разрешение с помощью флага --allow-ffi, поскольку нативный код выполняется за пределами песочницы безопасности Deno:
deno run --allow-ffi my_ffi_script.ts
Важное предупреждение о безопасности: в отличие от кода JavaScript, выполняющегося в песочнице Deno, нативные библиотеки, загруженные через FFI, имеют тот же уровень доступа, что и сам процесс Deno. Это означает, что они могут:
- Получать доступ к файловой системе
- Устанавливать сетевые соединения
- Получать доступ к переменным окружения
- Выполнять системные команды
Всегда проверяйте, что доверяете нативным библиотекам, которые загружаете через FFI.
Основы использования
Основной порядок использования FFI в Deno включает следующие шаги:
- Определение интерфейса нативных функций, которые требуется вызвать
- Загрузка динамической библиотеки с помощью
Deno.dlopen() - Вызов загруженных функций
Вот простой пример загрузки библиотеки C:
const dylib = Deno.dlopen("libexample.so", {
add: { parameters: ["i32", "i32"], result: "i32" },
});
console.log(dylib.symbols.add(5, 3)); // 8
dylib.close();
Загрузка библиотек
Первый аргумент Deno.dlopen() — это путь к динамической библиотеке. Deno передаёт этот путь загрузчику динамических библиотек операционной системы (dlopen в Linux и macOS, LoadLibrary в Windows), поэтому расположение файла определяется правилами ОС, а не механизмом разрешения модулей Deno.
Указать библиотеку можно двумя способами:
-
Путь с разделителем (например,
"./libexample.so"или"/usr/lib/libexample.so") открывает файл по указанному точному расположению. Относительный путь, такой как"./libexample.so", разрешается относительно текущего рабочего каталога процесса, а не расположения файла.ts. Это частая причина ошибок «не удалось открыть библиотеку»: при запуске одного и того же скрипта из другого каталога меняется путь, по которому Deno ищет файл. -
Имя без пути, не содержащее разделителей (например,
"libexample.so"), оставляет поиск за ОС, которая использует стандартные пути поиска. В Linux это означаетLD_LIBRARY_PATHи системный кэш (например,/usr/lib); в macOS — путиDYLD_*; в Windows — каталог исполняемого файла, системные каталоги иPATH. Используйте этот способ для библиотек, уже установленных в системе.
Разрешение пути относительно модуля
Чтобы загрузить библиотеку, находящуюся рядом с исходным файлом, независимо от текущего рабочего каталога, разрешите путь относительно import.meta.url вместо использования простого относительного пути:
// Always points at libexample.so sitting next to this module.
const path = new URL("./libexample.so", import.meta.url).pathname;
const dylib = Deno.dlopen(
path,
{
add: { parameters: ["i32", "i32"], result: "i32" },
} as const,
);
Этот способ также работает в исполняемом файле, созданном с помощью deno compile (см. ниже).
Включение библиотеки в сборку с помощью deno compile
Deno.dlopen требуется реальный файл на диске, поэтому динамическая библиотека не включается в скомпилированный бинарный файл автоматически. Добавьте её явно с помощью флага --include:
deno compile --allow-ffi --include libexample.so main.ts
Во время выполнения Deno распаковывает включённую библиотеку во временный каталог и указывает на неё с помощью import.meta.url. Поэтому модуль, разрешающий путь через new URL("./libexample.so", import.meta.url).pathname (как показано выше), находит включённую копию, и полученный бинарный файл запускается на компьютере, где библиотека не установлена. Если вы не включаете библиотеку в сборку, поставляйте её рядом с исполняемым файлом и загружайте по пути относительно бинарного файла либо используйте описанный выше системный путь поиска.
Обработка ошибок загрузки
Deno.dlopen синхронно выбрасывает исключение, если библиотеку не удаётся загрузить или объявленный символ отсутствует. Чтобы корректно обработать ошибку, оберните вызов в try/catch:
let dylib;
try {
dylib = Deno.dlopen(
"./libexample.so",
{
add: { parameters: ["i32", "i32"], result: "i32" },
} as const,
);
} catch (err) {
// A missing or unreadable file reports "Could not open library: ...".
// A missing function reports "Failed to register symbol <name>: ...".
console.error("Failed to load native library:", err.message);
Deno.exit(1);
}
Сообщение об ошибке позволяет различить два распространённых случая: не удалось открыть сам файл (неверный путь, файл отсутствует или несовместима архитектура) либо файл загрузился, но один из объявленных символов в нём не найден.
Поддерживаемые типы
FFI в Deno поддерживает различные типы данных для параметров и возвращаемых значений:
| Тип FFI | Deno | C | Rust |
|---|---|---|---|
i8 | number |
char / signed char
| i8 |
u8 | number | unsigned char | u8 |
i16 | number | short int | i16 |
u16 | number | unsigned short int | u16 |
i32 | number |
int / signed int
| i32 |
u32 | number | unsigned int | u32 |
i64 | bigint | long long int | i64 |
u64 | bigint | unsigned long long int | u64 |
usize | bigint | size_t | usize |
isize | bigint | size_t | isize |
f32 | number | float | f32 |
f64 | number | double | f64 |
void[1] | undefined | void | () |
pointer | {} | null | void * | *mut c_void |
buffer[2] | TypedArray | null | uint8_t * | *mut u8 |
function[3] | {} | null | void (*fun)() | Option<extern "C" fn()> |
{ struct: [...] }[4] | TypedArray | struct MyStruct | MyStruct |
Начиная с Deno 1.25 тип pointer был разделён на типы pointer и buffer, чтобы пользователи могли воспользоваться оптимизациями для типизированных массивов. Начиная с Deno 1.31 представление pointer в JavaScript стало непрозрачным объектом-указателем или null для нулевых указателей.
- [1] Тип
voidможно использовать только как тип результата. - [2] Тип
bufferпринимает типизированные массивы в качестве параметра, но при использовании как тип результата всегда возвращает объект-указатель илиnull, как и типpointer. - [3] Тип
functionработает точно так же, как типpointer, и в качестве типа параметра, и в качестве типа результата. - [4] Тип
structпредназначен для передачи и возврата структур C по значению (копированием). Массивstructдолжен перечислять типы всех полей структуры по порядку. Заполнение структур выполняется автоматически: упакованные структуры можно определить, добавив необходимое количество полейu8, чтобы избежать заполнения. В качестве структур поддерживаются только типизированные массивы, и структуры всегда возвращаются какUint8Array.
Работа со структурами
Чтобы передать или вернуть структуру C по значению, опишите её расположение с помощью { struct: [...] } — массива, в котором перечислены типы FFI каждого поля в порядке объявления. Значения структур передаются как TypedArray, байты которого соответствуют расположению полей в C, а структуры, возвращаемые по значению, возвращаются как Uint8Array нужной длины. В таблице типов выше на этой странице приведена точная форма массива struct.
Предположим, у вас есть небольшая библиотека C, которая работает с двумерной точкой Point:
typedef struct {
double x;
double y;
} Point;
double distance(Point a, Point b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return __builtin_sqrt(dx * dx + dy * dy);
}
Point midpoint(Point a, Point b) {
Point m;
m.x = (a.x + b.x) / 2.0;
m.y = (a.y + b.y) / 2.0;
return m;
}
Соберите её как динамическую библиотеку. Флаги компилятора и имя выходного файла зависят от платформы:
cc -shared -fPIC -O2 -o libpoint.so point.c
cc -dynamiclib -O2 -o libpoint.dylib point.c
cl /LD /O2 point.c /Fe:point.dll
Затем вызовите её из Deno, указав имя файла для вашей платформы в Deno.dlopen. Обратите внимание, что определение struct — это массив типов полей в порядке объявления, а не объект с именованными полями:
// `Point` mirrors the C `struct Point { double x; double y; }`.
const Point = { struct: ["f64", "f64"] } as const;
const lib = Deno.dlopen(
"./libpoint.so",
{
distance: { parameters: [Point, Point], result: "f64" },
midpoint: { parameters: [Point, Point], result: Point },
} as const,
);
// Build struct values as a TypedArray whose bytes match the C layout.
// Two f64 fields → two slots in a Float64Array.
const a = new Float64Array([1.0, 2.0]); // Point { x: 1.0, y: 2.0 }
const b = new Float64Array([4.0, 6.0]); // Point { x: 4.0, y: 6.0 }
// FFI reads the underlying bytes, so pass the buffer as a Uint8Array view.
const aBytes = new Uint8Array(a.buffer);
const bBytes = new Uint8Array(b.buffer);
console.log("distance =", lib.symbols.distance(aBytes, bBytes));
// A struct returned by value comes back as a Uint8Array sized to the struct.
// Wrap it in a Float64Array to read the fields back out.
const midBytes = lib.symbols.midpoint(aBytes, bBytes);
const mid = new Float64Array(midBytes.buffer);
console.log("midpoint =", { x: mid[0], y: mid[1] });
lib.close();
Запустите программу с разрешением --allow-ffi:
deno run --allow-ffi point.ts
Вы должны увидеть:
distance = 5
midpoint = { x: 2.5, y: 4 }
При работе со структурами следует помнить о нескольких моментах:
-
Расположение соответствует компилятору C. Deno заполняет поля структуры так же, как ваш компилятор C. Если вам нужна упакованная структура, добавьте заполнение явно с помощью полей
u8, как указано в таблице типов выше. -
Порядок полей определяется их позициями. Массив
structсодержит только типы в порядке объявления — на стороне JavaScript имена полей отсутствуют. Передаваемый вами типизированный массив должен располагать поля в том же порядке. -
Возвращаемые структуры представлены байтами. Результат в виде структуры всегда является
Uint8Array; для чтения полей просматривайте его через соответствующийTypedArray(илиDataView).
Работа с обратными вызовами
В качестве обратных вызовов для нативного кода можно передавать функции JavaScript:
const signatures = {
setCallback: {
parameters: ["function"],
result: "void",
},
runCallback: {
parameters: [],
result: "void",
},
} as const;
// Create a callback function
const callback = new Deno.UnsafeCallback(
{ parameters: ["i32"], result: "void" } as const,
(value) => {
console.log("Callback received:", value);
},
);
// Pass the callback to the native library
dylib.symbols.setCallback(callback.pointer);
// Later, this will trigger our JavaScript function
dylib.symbols.runCallback();
// Always clean up when done
callback.close();
Рекомендации по работе с FFI
-
Всегда закрывайте ресурсы. Закрывайте библиотеки с помощью
dylib.close(), а обратные вызовы — с помощьюcallback.close()после завершения работы. -
Отдавайте предпочтение TypeScript. При работе с FFI используйте TypeScript для более эффективной проверки типов.
-
Оберните вызовы FFI в блоки try/catch, чтобы корректно обрабатывать ошибки.
-
Будьте предельно осторожны при использовании FFI, поскольку нативный код может обходить песочницу безопасности Deno.
-
Сведите интерфейс FFI к минимуму, чтобы уменьшить поверхность атаки.
Примеры
Использование библиотеки Rust
Вот пример создания и использования библиотеки Rust с Deno:
Сначала создайте библиотеку Rust:
// lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn fibonacci(n: u32) -> u32 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}
Скомпилируйте её как динамическую библиотеку:
rustc --crate-type cdylib lib.rs
Затем используйте её из Deno:
const libName = {
windows: "./lib.dll",
linux: "./liblib.so",
darwin: "./liblib.dylib",
}[Deno.build.os];
const dylib = Deno.dlopen(
libName,
{
fibonacci: { parameters: ["u32"], result: "u32" },
} as const,
);
// Calculate the 10th Fibonacci number
const result = dylib.symbols.fibonacci(10);
console.log(`Fibonacci(10) = ${result}`); // 55
dylib.close();
Примеры
В этих репозиториях, которые поддерживает сообщество, собраны работающие примеры интеграции FFI с различными нативными библиотеками в разных операционных системах.
Другие подходы к интеграции нативного кода
Хотя FFI в Deno предоставляет прямой способ вызова нативных функций, существуют и другие способы интеграции нативного кода:
Использование Node-API (N-API) с Deno
Deno поддерживает Node-API (N-API) для совместимости с нативными дополнениями Node.js. Это позволяет использовать существующие нативные модули, написанные для Node.js.
Прямая загрузка дополнения Node-API:
import process from "node:process";
process.dlopen(module, "./native_module.node", 0);
Использование пакета npm, который использует дополнение Node-API:
import someNativeAddon from "npm:some-native-addon";
console.log(someNativeAddon.doSomething());
Чем это отличается от FFI?
| Аспект | FFI | Поддержка Node-API |
|---|---|---|
| Настройка | Шаг сборки не требуется | Требуются предварительно скомпилированные бинарные файлы или шаг сборки |
| Переносимость | Зависит от ABI библиотеки | ABI стабилен между версиями |
| Вариант использования | Прямые вызовы библиотеки | Повторное использование дополнений Node.js |
Поддержка Node-API идеально подходит для использования существующих нативных модулей Node.js, тогда как FFI лучше всего подходит для прямых и лёгких вызовов нативных библиотек.
Альтернативы FFI
Прежде чем использовать FFI, рассмотрите следующие альтернативы:
- WebAssembly — для переносимого нативного кода, выполняющегося в песочнице Deno.
- Используйте
Deno.commandдля запуска внешних бинарных файлов и дочерних процессов с контролируемыми разрешениями. - Проверьте, предоставляют ли нужную вам функциональность нативные API Deno.
Возможности FFI в Deno обеспечивают мощную интеграцию с нативным кодом, позволяя оптимизировать производительность и получать доступ к функциональности системного уровня. Однако эта мощь сопряжена с существенными рисками безопасности. Всегда соблюдайте осторожность при работе с FFI и проверяйте, что доверяете используемым нативным библиотекам.
Последнее обновление:
© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/fundamentals/ffi