استخدم وحدة 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 مرات من Node.js FFI عبر Node-API.
يقوم Bun بإنشاء وتجميع روابط C في الوقت الفعلي تحول القيم بين أنواع 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 Type | الأسماء المستعارة |
|---|---|---|
| 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 بايت لكل حرف)، عادةً
- الطول غير مخزن. بدلاً من ذلك، السلسلة منتهية بـ null مما يعني أن الطول هو فهرس أول
\0تجده - قابلة للتغيير
لحل هذه المشكلة، تصدر bun:ffi CString الذي يوسع String المدمج في JavaScript لدعم السلاسل المنتهية بـ null وإضافة بعض الإضافات:
class CString extends String {
/**
* بالنظر إلى `ptr`، سيبحث هذا تلقائيًا عن حرف الإغلاق `\0` وينقل الترميز من UTF-8 إلى UTF-16 إذا لزم الأمر.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
/**
* ptr إلى سلسلة C
*
* مثيل `CString` هذا هو نسخة من السلسلة، لذا
* من الآمن الاستمرار في استخدام هذا المثيل بعد تحرير `ptr`.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}للتحويل من مؤشر سلسلة منتهي بـ null إلى سلسلة 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`، يُفترض أنه مؤشر منتهي بـ null
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 الأساسي.
التوقيع المتوقع هو نفسه في JavaScriptCore's C API:
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.
محاذاة المؤشر
إذا كانت واجهة برمجة التطبيقات تتوقع مؤشرًا بحجم شيء آخر غير 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);