Skip to content

استخدم وحدة bun:ffi المدمجة لاستدعاء المكتبات الأصلية من JavaScript بكفاءة. تعمل مع اللغات التي تدعم C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, إلخ).


استخدام dlopen (bun:ffi)

لطباعة رقم إصدار sqlite3:

ts
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

zig
pub export fn add(a: i32, b: i32) i32 {
  return a + b;
}

للتجميع:

bash
zig build-lib add.zig -dynamic -OReleaseFast

مرر مسارًا إلى المكتبة المشتركة وخريطة من الرموز للاستيراد في dlopen:

ts
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

rust
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

للتجميع:

bash
rustc --crate-type cdylib add.rs

C++

c
#include <cstdint>

extern "C" int32_t add(int32_t a, int32_t b) {
    return a + b;
}

للتجميع:

bash
zig build-lib add.cpp -dynamic -lc -lc++

أنواع FFI

قيم FFIType التالية مدعومة.

FFITypeC Typeالأسماء المستعارة
bufferchar*
cstringchar*
function(void*)(*)()fn، callback
ptrvoid*pointer، void*، char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t، int
i64int64_tint64_t
i64_fastint64_t
u8uint8_tuint8_t
u16uint16_tuint16_t
u32uint32_tuint32_t
u64uint64_tuint64_t
u64_fastuint64_t
f32floatfloat
f64doubledouble
boolbool
charchar
napi_envnapi_env
napi_valuenapi_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 وإضافة بعض الإضافات:

ts
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:

ts
const myString = new CString(ptr);

للتحويل من مؤشر بطول معروف إلى سلسلة JavaScript:

ts
const myString = new CString(ptr, 0, byteLength);

منشئ new CString() ينسخ سلسلة C، لذا من الآمن الاستمرار في استخدام myString بعد تحرير ptr.

ts
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، وقمت بالفعل بتحميل بعض الرموز.

ts
import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* بطريقة ما، حصلت على هذا المؤشر */

const getVersion = new CFunction({
  returns: "cstring",
  args: [],
  ptr: myNativeLibraryGetVersion,
});
getVersion();

إذا كان لديك مؤشرات دوال متعددة، يمكنك تعريفها جميعًا دفعة واحدة باستخدام linkSymbols:

ts
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.

ts
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).

ts
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`:
ts
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 إلى مؤشر:

ts
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

للتحويل من مؤشر إلى ArrayBuffer:

ts
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:

ts
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:

ts
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
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.f64

إدارة الذاكرة

لا تدير bun:ffi الذاكرة نيابةً عنك. يجب تحرير الذاكرة عند الانتهاء منها.

من JavaScript

إذا كنت تريد تتبع متى لم يعد TypedArray مستخدمًا من JavaScript، يمكنك استخدام FinalizationRegistry.

من C, Rust, Zig, إلخ

إذا كنت تريد تتبع متى لم يعد TypedArray مستخدمًا من C أو FFI، يمكنك تمرير دالة استدعاء ومؤشر سياق اختياري إلى toArrayBuffer أو toBuffer. يتم استدعاء هذه الدالة في وقت لاحق، بمجرد أن يحرر جامع القمامة كائن JavaScript ArrayBuffer الأساسي.

التوقيع المتوقع هو نفسه في JavaScriptCore's C API:

c
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
ts
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 بالحجم المكافئ:

ts
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:

ts
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,
);

قراءة المؤشرات

ts
const out = encode_png(
  // سيتم تمرير pixels كمؤشر
  pixels,

  // الأبعاد:
  128,
  128,
);

// بافتراض أنه منتهي بـ 0، يمكن قراءته هكذا:
let png = new Uint8Array(toArrayBuffer(out));

// احفظه على القرص:
await Bun.write("out.png", png);

Bun بواسطة www.bunjs.com.cn تحرير