Используйте встроенный модуль bun:ffi для эффективного вызова нативных библиотек из JavaScript. Он работает с языками, поддерживающими C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin и т.д.).
Использование dlopen (bun:ffi)
Чтобы вывести номер версии sqlite3:
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` — это "dylib", "so" или "dll" в зависимости от платформы
// вам не обязательно использовать "suffix", это просто для удобства
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // функция для вызова
},
} = dlopen(
path, // имя библиотеки или путь к файлу
{
sqlite3_libversion: {
// нет аргументов, возвращает строку
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`Версия SQLite 3: ${sqlite3_libversion()}`);Производительность
Согласно нашему бенчмарку, bun:ffi примерно в 2-6 раз быстрее, чем FFI Node.js через Node-API.
Bun генерирует и компилирует C-привязки по требованию (JIT), которые эффективно преобразуют значения между типами JavaScript и нативными типами. Для компиляции C Bun встраивает TinyCC — небольшой и быстрый компилятор C.
Использование
Zig
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}Для компиляции:
zig build-lib add.zig -dynamic -OReleaseFastПередайте путь к общей библиотеке и карту символов для импорта в dlopen:
import { dlopen, FFIType, suffix } from "bun:ffi";
const { i32 } = FFIType;
const path = `libadd.${suffix}`;
const lib = dlopen(path, {
add: {
args: [i32, i32],
returns: i32,
},
});
console.log(lib.symbols.add(1, 2));Rust
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}Для компиляции:
rustc --crate-type cdylib add.rsC++
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}Для компиляции:
zig build-lib add.cpp -dynamic -lc -lc++Типы FFI
Поддерживаются следующие значения FFIType.
FFIType | Тип C | Псевдонимы |
|---|---|---|
| buffer | char* | |
| cstring | char* | |
| function | (void*)(*)() | fn, callback |
| ptr | void* | pointer, void*, char* |
| i8 | int8_t | int8_t |
| i16 | int16_t | int16_t |
| i32 | int32_t | int32_t, int |
| i64 | int64_t | int64_t |
| i64_fast | int64_t | |
| u8 | uint8_t | uint8_t |
| u16 | uint16_t | uint16_t |
| u32 | uint32_t | uint32_t |
| u64 | uint64_t | uint64_t |
| u64_fast | uint64_t | |
| f32 | float | float |
| f64 | double | double |
| bool | bool | |
| char | char | |
| napi_env | napi_env | |
| napi_value | napi_value |
Примечание: аргументы buffer должны быть TypedArray или DataView.
Строки
Строки JavaScript и C-подобные строки различаются, что усложняет использование строк с нативными библиотеками.
Чем отличаются строки JavaScript и C-строки?
Строки JavaScript:
- UTF16 (2 байта на букву) или потенциально latin1, в зависимости от движка JavaScript и используемых символов
lengthхранится отдельно- Неизменяемы
C-строки:
- UTF8 (1 байт на букву), обычно
- Длина не хранится. Вместо этого строка завершается нулём, что означает, что длина — это индекс первого найденного
\0 - Изменяемы
Чтобы решить эту проблему, bun:ffi экспортирует CString, который расширяет встроенный String JavaScript для поддержки строк с нулевым завершением и добавляет несколько дополнительных возможностей:
class CString extends String {
/**
* Учитывая `ptr`, это автоматически ищет закрывающий символ `\0` и транскодирует из UTF-8 в UTF-16 при необходимости.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
/**
* Указатель на C-строку
*
* Этот экземпляр `CString` — клон строки, поэтому
* безопасно продолжать использовать этот экземпляр после того, как `ptr` был
* освобождён.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}Чтобы преобразовать из указателя на строку с нулевым завершением в строку JavaScript:
const myString = new CString(ptr);Чтобы преобразовать из указателя с известной длиной в строку JavaScript:
const myString = new CString(ptr, 0, byteLength);Конструктор new CString() клонирует C-строку, поэтому безопасно продолжать использовать myString после освобождения ptr.
my_library_free(myString.ptr);
// это безопасно, потому что myString — клон
console.log(myString);При использовании в returns, FFIType.cstring преобразует указатель в string JavaScript. При использовании в args, FFIType.cstring идентичен ptr.
Указатели на функции
NOTE
Асинхронные функции ещё не поддерживаютсяЧтобы вызвать указатель на функцию из JavaScript, используйте CFunction. Это полезно при использовании Node-API (napi) с Bun, и вы уже загрузили некоторые символы.
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* как-то вы получили этот указатель */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();Если у вас есть несколько указателей на функции, вы можете определить их все сразу с помощью linkSymbols:
import { linkSymbols } from "bun:ffi";
// getVersionPtrs определено в другом месте
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
// В отличие от dlopen(), имена здесь могут быть любыми
getMajor: {
returns: "cstring",
args: [],
// Поскольку это не использует dlsym(), вы должны предоставить валидный ptr
// Этот ptr может быть числом или bigint
// Неверный указатель приведёт к сбою программы.
ptr: majorPtr,
},
getMinor: {
returns: "cstring",
args: [],
ptr: minorPtr,
},
getPatch: {
returns: "cstring",
args: [],
ptr: patchPtr,
},
});
const [major, minor, patch] = [lib.symbols.getMajor(), lib.symbols.getMinor(), lib.symbols.getPatch()];Обратные вызовы
Используйте JSCallback для создания функций обратного вызова JavaScript, которые могут быть переданы функциям C/FFI. Функция C/FFI может вызывать код JavaScript/TypeScript. Это полезно для асинхронного кода или когда вы хотите вызвать код JavaScript из C.
import { dlopen, JSCallback, ptr, CString } from "bun:ffi";
const {
symbols: { search },
close,
} = dlopen("libmylib", {
search: {
returns: "usize",
args: ["cstring", "callback"],
},
});
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
});
const str = Buffer.from("wwutwutwutwutwutwutwutwutwutwutut\0", "utf8");
if (search(ptr(str), searchIterator)) {
// найдено совпадение!
}
// Позже:
setTimeout(() => {
searchIterator.close();
close();
}, 5000);Когда вы закончили с JSCallback, вы должны вызвать close() для освобождения памяти.
Экспериментальные потокобезопасные обратные вызовы
JSCallback имеет экспериментальную поддержку потокобезопасных обратных вызовов. Это потребуется, если вы передаёте функцию обратного вызова в другой поток из контекста его создания. Вы можете включить это с помощью необязательного параметра threadsafe.
В настоящее время потокобезопасные обратные вызовы лучше всего работают при запуске из другого потока, который выполняет код JavaScript, т.е. Worker. Будущая версия Bun позволит вызывать их из любого потока (например, новые потоки, созданные вашей нативной библиотекой, о которых Bun не знает).
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Необязательно. По умолчанию `false`
});NOTE
**⚡️ Совет по производительности** — Для небольшого повышения производительности напрямую передавайте `JSCallback.prototype.ptr` вместо объекта `JSCallback`:const onResolve = new JSCallback(arg => arg === 42, {
returns: "bool",
args: ["i32"],
});
const setOnResolve = new CFunction({
returns: "bool",
args: ["function"],
ptr: myNativeLibrarySetOnResolve,
});
// Этот код выполняется немного быстрее:
setOnResolve(onResolve.ptr);
// По сравнению с этим:
setOnResolve(onResolve);Указатели
Bun представляет указатели как number в JavaScript.
Как 64-битный указатель помещается в число JavaScript?
64-битные процессоры поддерживают до 52 бит адресуемого пространства. Числа JavaScript поддерживают 53 бита полезного пространства, поэтому у нас остаётся около 11 бит дополнительного пространства.
Почему не BigInt? BigInt медленнее. Движки JavaScript выделяют отдельный BigInt, что означает, что они не могут поместиться в обычное значение JavaScript. Если вы передадите BigInt в функцию, он будет преобразован в number.
Чтобы преобразовать из TypedArray в указатель:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);Чтобы преобразовать из указателя в ArrayBuffer:
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
// toArrayBuffer принимает `byteOffset` и `byteLength`
// если `byteLength` не предоставлен, предполагается, что это указатель с нулевым завершением
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);Для чтения данных из указателя у вас есть два варианта. Для долгоживущих указателей используйте DataView:
import { toArrayBuffer } from "bun:ffi";
let myDataView = new DataView(toArrayBuffer(myPtr, 0, 32));
console.log(
myDataView.getUint8(0, true),
myDataView.getUint8(1, true),
myDataView.getUint8(2, true),
myDataView.getUint8(3, true),
);Для короткоживущих указателей используйте read:
import { read } from "bun:ffi";
console.log(
// ptr, byteOffset
read.u8(myPtr, 0),
read.u8(myPtr, 1),
read.u8(myPtr, 2),
read.u8(myPtr, 3),
);Функция read ведёт себя аналогично DataView, но обычно быстрее, потому что ей не нужно создавать DataView или ArrayBuffer.
FFIType | Функция read |
|---|---|
| ptr | read.ptr |
| i8 | read.i8 |
| i16 | read.i16 |
| i32 | read.i32 |
| i64 | read.i64 |
| u8 | read.u8 |
| u16 | read.u16 |
| u32 | read.u32 |
| u64 | read.u64 |
| f32 | read.f32 |
| f64 | read.f64 |
Управление памятью
bun:ffi не управляет памятью за вас. Вы должны освободить память, когда закончите с ней.
Из JavaScript
Если вы хотите отследить, когда TypedArray больше не используется из JavaScript, вы можете использовать FinalizationRegistry.
Из C, Rust, Zig и т.д.
Если вы хотите отследить, когда TypedArray больше не используется из C или FFI, вы можете передать обратный вызов и необязательный указатель контекста в toArrayBuffer или toBuffer. Эта функция вызывается позже, когда сборщик мусора освобождает базовый объект JavaScript ArrayBuffer.
Ожидаемая сигнатура такая же, как в C API JavaScriptCore:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);import { toArrayBuffer } from "bun:ffi";
// с deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// это необязательный указатель на обратный вызов
deallocatorContext,
// это указатель на функцию
jsTypedArrayBytesDeallocator,
);
// без deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// это указатель на функцию
jsTypedArrayBytesDeallocator,
);Безопасность памяти
Использование сырых указателей вне FFI крайне не рекомендуется. Будущая версия Bun может добавить флаг CLI для отключения bun:ffi.
Выравнивание указателей
Если API ожидает указатель размером на что-то отличное от char или u8, убедитесь, что TypedArray также имеет этот размер. u64* не совсем то же самое, что [8]u8* из-за выравнивания.
Передача указателя
Там, где функции FFI ожидают указатель, передайте TypedArray эквивалентного размера:
import { dlopen, FFIType } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType также могут быть указаны как строки
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
pixels.subarray(0, 32 * 32 * 2).fill(0);
const out = encode_png(
// pixels будет передан как указатель
pixels,
128,
128,
);Автоматически сгенерированная обёртка преобразует указатель в TypedArray.
Сложный режим
Если вы не хотите автоматическое преобразование или хотите указатель на конкретное смещение байта внутри TypedArray, вы также можете напрямую получить указатель на TypedArray:
import { dlopen, FFIType, ptr } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType также могут быть указаны как строки
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
// это возвращает число! не BigInt!
const myPtr = ptr(pixels);
const out = encode_png(
myPtr,
// размеры:
128,
128,
);Чтение указателей
const out = encode_png(
// pixels будет передан как указатель
pixels,
// размеры:
128,
128,
);
// предполагая, что он завершён 0, его можно прочитать так:
let png = new Uint8Array(toArrayBuffer(out));
// сохранить на диск:
await Bun.write("out.png", png);