Spec-Zone.ru › Deno 2

Интерфейс взаимодействия с внешними функциями (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 включает следующие шаги:

  1. Определение интерфейса нативных функций, которые требуется вызвать
  2. Загрузка динамической библиотеки с помощью Deno.dlopen()
  3. Вызов загруженных функций

Вот простой пример загрузки библиотеки 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:

point.c
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.ts
// `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

  1. Всегда закрывайте ресурсы. Закрывайте библиотеки с помощью dylib.close(), а обратные вызовы — с помощью callback.close() после завершения работы.

  2. Отдавайте предпочтение TypeScript. При работе с FFI используйте TypeScript для более эффективной проверки типов.

  3. Оберните вызовы FFI в блоки try/catch, чтобы корректно обрабатывать ошибки.

  4. Будьте предельно осторожны при использовании FFI, поскольку нативный код может обходить песочницу безопасности Deno.

  5. Сведите интерфейс 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();

Примеры

  • Netsaur
  • WebView_deno
  • Deno_sdl2
  • Репозиторий примеров Deno FFI

В этих репозиториях, которые поддерживает сообщество, собраны работающие примеры интеграции 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 и проверяйте, что доверяете используемым нативным библиотекам.

Последнее обновление: 18 июня 2026 г.

© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/fundamentals/ffi

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API