برنامه نویسی

چگونه و آیا باید از Bun FFI استفاده کنید

Summarize this content to 400 words in Persian Lang

برای رسیدن به چه چیزی تلاش می کنیم

فرض کنید یک برنامه جاوا اسکریپت دارید که بصورت گروهی اجرا می شود و تنگناهایی را شناسایی کرده اید که می خواهید بهینه سازی کنید.بازنویسی آن به زبانی کارآمدتر ممکن است تنها راه حلی باشد که به آن نیاز دارید.

به عنوان یک زمان اجرا JS مدرن، Bun از Interface Function خارجی (FFI) برای فراخوانی کتابخانه های نوشته شده به زبان های دیگری که از C ABI های آشکارسازی پشتیبانی می کنند، مانند C، C++، Rust و Zig پشتیبانی می کند.

در این پست به نحوه استفاده از آن می پردازیم و نتیجه می گیریم که آیا می توان از آن بهره مند شد یا خیر.

چگونه کتابخانه را به جاوا اسکریپت پیوند دهیم

این مثال از Rust استفاده می کند. ایجاد یک کتابخانه مشترک با پیوندهای C در زبان های دیگر متفاوت به نظر می رسد، اما ایده یکسان باقی می ماند.

از سمت JS

Bun از طریق FFI API خود را نشان می دهد bun:ffi ماژول

نقطه ورودی یک است dlopen تابع مسیری را طی می کند که یا مطلق است یا نسبت به فهرست کاری فعلی به فایل کتابخانه (خروجی ساخت با a .so پسوند برای لینوکس، .dylib برای macOS یا .dll برای ویندوز) و یک شی با امضای توابعی که می خواهید وارد کنید.یک شی را با a برمی گرداند close روشی که ممکن است برای بستن کتابخانه زمانی که دیگر به آن نیاز نیست استفاده کنید symbols ویژگی که یک شی حاوی توابعی است که شما انتخاب کرده اید.

import {
dlopen,
FFIType,
read,
suffix,
toArrayBuffer,
type Pointer,
} from “bun:ffi”;

// Both your script and your library don’t typically change their locations
// Use `import.meta.dirname` to make your script independent from the cwd
const DLL_PATH =
import.meta.dirname + `/../../rust-lib/target/release/library.${suffix}`;

function main() {
// Deconstruct object to get functions
// but collect `close` method into object
// to avoid using `this` in a wrong scope
const {
symbols: { do_work },
…dll
} = dlopen(DLL_PATH, {
do_work: {
args: [FFIType.ptr, FFIType.ptr, “usize”, “usize”],
returns: FFIType.void,
},
});

/* … */

// It is unclear whether it is required or recommended to call `close`
// an example says `JSCallback` instances specifically need to be closed
// Note that using `symbols` after calling `close` is undefined behaviour
dll.close();
}

main();

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

انتقال داده از طریق مرز FFI

همانطور که ممکن است متوجه شوید، انواع پشتیبانی شده که bun از طریق FFI می پذیرد محدود به اعداد، از جمله اشاره گر هستند.قابل توجه است size_t یا usize در لیست انواع پشتیبانی شده وجود ندارد، حتی اگر کد آن در نسخه 1.1.34 bun وجود داشته باشد.

Bun هیچ کمکی در انتقال داده های پیچیده تر از رشته C ارائه نمی دهد. یعنی باید خودتان با اشاره گرها کار کنید.

بیایید ببینیم چگونه یک اشاره گر را از جاوا اسکریپت به Rust منتقل کنیم …

{
reconstruct_slice: {
args: [FFIType.ptr, “usize”],
returns: FFIType.void,
},
}

const array = new BigInt64Array([0, 1, 3]);
// Bun automatically converts `TypedArray`s into pointers
reconstruct_slice(array, array.length);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// Reconstruct a `slice` that was initialized in JavaScript
unsafe fn reconstruct_slice(
array_ptr: *const i64,
length: libc::size_t,
) -> &[i64] {
// Even though here it’s not null, it’s good practice to check
assert!(!array_ptr.is_null());
// Unaligned pointer can lead to undefined behaviour
assert!(array_ptr.is_aligned());
// Check that the array doesn’t “wrap around” the address space
assert!(length < usize::MAX / 4);
let _: &[i64] = unsafe { slice::from_raw_parts(array_ptr, length) };
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

… و نحوه برگرداندن اشاره گر از Rust به جاوا اسکریپت.

{
allocate_buffer: {
args: [],
returns: FFIType.ptr,
},
as_pointer: {
args: [“usize”],
returns: FFIType.ptr,
},
}

// Hardcoding this value for 64-bit systems
const BYTES_IN_PTR = 8;

const box: Pointer = allocate_buffer()!;
const ptr: number = read.ptr(box);
// Reading the value next to `ptr`
const length: number = read.ptr(box, BYTES_IN_PTR);
// Hardcoding `byteOffset` to be 0 because Rust guarantees that
// Buffer holds `i32` values which take 4 bytes
// Note how we need to call a no-op function `as_pointer` because
// `toArrayBuffer` takes a `Pointer` but `read.ptr` returns a `number`
const _buffer = toArrayBuffer(as_pointer(ptr)!, 0, length * 4);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

#[no_mangle] pub extern “C” fn allocate_buffer() -> Box<[usize; 2]> {
let buffer: Vec<i32> = vec![0; 10];
let memory: ManuallyDrop<Vec<i32>> = ManuallyDrop::new(buffer);
let ptr: *const i32 = memory.as_ptr();
let length: usize = memory.len();
// Unlike a `Vec`, `Box` is FFI compatible and will not drop
// its data when crossing the FFI
// Additionally, a `Box` where `T` is `Sized` will be a thin pointer
Box::new([ptr as usize, length])
}

#[no_mangle] pub const extern “C” fn as_pointer(ptr: usize) -> usize {
ptr
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Rust نمی داند که JS مالکیت داده های طرف مقابل را در اختیار می گیرد، بنابراین باید صریحاً به آن بگویید واگذار نمی کند داده های روی پشته با استفاده از ManuallyDrop. زبان های دیگری که حافظه را مدیریت می کنند باید کاری مشابه انجام دهند.

مدیریت حافظه

همانطور که می بینیم، امکان تخصیص حافظه در JS و Rust وجود دارد و هیچ کدام نمی توانند با خیال راحت مدیریت حافظه دیگران

بیایید انتخاب کنیم که حافظه خود را کجا و چگونه تخصیص دهید.

در Rust اختصاص دهید

3 روش برای واگذاری پاکسازی حافظه به Rust از JS وجود دارد که همه مزایا و معایب خود را دارند.

استفاده کنید FinalizationRegistry

استفاده کنید FinalizationRegistry برای درخواست پاسخ به پاکسازی در حین جمع‌آوری زباله با ردیابی شی در جاوا اسکریپت.

{
drop_buffer: {
args: [FFIType.ptr],
returns: FFIType.void,
},
}

const registry = new FinalizationRegistry((box: Pointer): void => {
drop_buffer(box);
});
registry.register(buffer, box);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// This call assumes neither the box nor the buffer have been mutated in JS
#[no_mangle] pub unsafe extern “C” fn drop_buffer(raw: *mut [usize; 2]) {
let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
let ptr: *mut i32 = box_[0] as *mut i32;
let length: usize = box_[1];
let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
drop(buffer);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

منفی

جمع آوری زباله ها خاص موتور و غیر قطعی است
تماس برگشتی پاکسازی به هیچ وجه تضمین نمی شود که تماس گرفته شود

استفاده کنید toArrayBuffer's finalizationCallback پارامتر

ردیابی جمع‌آوری زباله را به bun واگذار کنید تا یک تماس پاکسازی مجدد برقرار شود.هنگام انتقال 4 پارامتر به toArrayBuffer4 باید تابع C باشد تا در پاکسازی فراخوانی شود.با این حال، هنگام ارسال 5 پارامتر، پارامتر پنجم تابع است و پارامتر چهارم باید یک نشانگر زمینه باشد که از آن عبور می کند.

{
box_value: {
args: [“usize”],
returns: FFIType.ptr,
},
drop_box: {
args: [FFIType.ptr],
returns: FFIType.void,
},
drop_buffer: {
args: [FFIType.ptr, FFIType.ptr],
returns: FFIType.void,
},
}

// Bun expects the context to specifically be a pointer
const finalizationCtx: Pointer = box_value(length)!;

// Note that despite the presence of these extra parameters in the docs,
// they’re absent from `@types/bun`
//@ts-expect-error see above
const buffer = toArrayBuffer(
as_pointer(ptr)!,
0,
length * 4,
//@ts-expect-error see above
finalizationCtx,
drop_buffer,
);
// Don’t leak the box used to pass buffer through FFI
drop_box(box);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

#[no_mangle] pub unsafe extern “C” fn box_value(value: usize) -> Box<usize> {
Box::new(value)
}

/// # Safety
///
/// This call assumes the box hasn’t been mutated in JS
#[no_mangle] pub unsafe extern “C” fn drop_box(raw: *mut [usize; 2]) {
let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
drop(box_);
}

/// As per bun docs, expected signature like in JavaScriptCore’s `JSTypedArrayBytesDeallocator`
/// https://developer.apple.com/documentation/javascriptcore/jstypedarraybytesdeallocator?language=objc
///
/// # Safety
///
/// This call assumes the buffer hasn’t been mutated in JS
#[no_mangle] pub unsafe extern “C” fn drop_buffer(ptr: *mut i32, len: *mut usize) {
// reconstruct the context box to not leak it
let length: usize = unsafe { *Box::from_raw(len) };
let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
drop(buffer);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

واگذاری منطق به خارج از جاوا اسکریپت

منفی

دیگ بخار زیاد و احتمال نشتی حافظه
حاشیه نویسی نوع برای toArrayBuffer

جمع آوری زباله ها خاص موتور و غیر قطعی است
تماس برگشتی پاکسازی به هیچ وجه تضمین نمی شود که تماس گرفته شود

حافظه را به صورت دستی مدیریت کنید

فقط بعد از اینکه دیگر به آن نیاز ندارید، خودتان آن را رها کنید.خوشبختانه TypeScript بسیار مفید است Disposable رابط برای این و using کلمه کلیدیاین معادل پایتون است with یا سی شارپ using کلمات کلیدی

برای آن به اسناد مراجعه کنید

{
drop_box: {
args: [FFIType.ptr],
returns: FFIType.void,
},
drop_buffer: {
args: [FFIType.ptr, “usize”],
returns: FFIType.void,
},
}

class RustVector implements Disposable {
constructor(
private inner: Int32Array,
private dropBuffer: (ptr: Int32Array, len: number) => void,
) {}
[Symbol.dispose](): void {
this.dropBuffer(this.inner, this.inner.length);
}
}

const buffer = new Int32Array(toArrayBuffer(as_pointer(ptr)!, 0, length * 4));
// Don’t leak the box used to pass buffer through FFI
drop_box(box);
using wrapper = new RustVector(buffer, drop_buffer);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// This call assumes the box hasn’t been mutated in JS
#[no_mangle] pub unsafe extern “C” fn drop_box(raw: *mut [usize; 2]) {
let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
drop(box_);
}

/// # Safety
///
/// This call assumes the buffer hasn’t been mutated in JS
#[no_mangle] pub unsafe extern “C” fn drop_buffer(ptr: *mut i32, length: usize) {
let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
drop(buffer);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

پاکسازی تضمین شده است که اجرا شود
شما کنترل دارید که چه زمانی می خواهید حافظه را حذف کنید

منفی

شی دیگ بخار برای Disposable رابط
حذف دستی حافظه کندتر از استفاده از زباله جمع کن است
اگر می‌خواهید مالکیت بافر را واگذار کنید، باید یک کپی کنید و نسخه اصلی را رها کنید

تخصیص در JS

این بسیار ساده‌تر و ایمن‌تر است زیرا واگذاری برای شما انجام می‌شود.

با این حال، یک اشکال قابل توجه وجود دارد. از آنجایی که نمی‌توانید حافظه جاوا اسکریپت را در Rust مدیریت کنید، نمی‌توانید از ظرفیت بافر عبور کنید زیرا این امر باعث ایجاد یک توزیع می‌شود. این بدان معناست که قبل از ارسال آن به Rust باید اندازه بافر را بدانید.ندانستن تعداد بافرهایی که از قبل نیاز دارید نیز هزینه های زیادی را متحمل می شود زیرا فقط برای تخصیص از طریق FFI به این سو و آن سو می روید.

const CAPACITY = 20;
// The buffer is initialized with zeroes
const buffer = new Int32Array(CAPACITY);
mutate_buffer(buffer, CAPACITY);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// Ensure that the length of the buffer is at least `capacity` size
#[no_mangle] pub unsafe extern “C” fn mutate_buffer(ptr: *mut i32, capacity: usize) {
assert!(!ptr.is_null());
assert!(ptr.is_aligned());
assert!(capacity < usize::MAX / 4);
// Manually drop so that vec doesn’t get cleaned up
let mut buffer: ManuallyDrop<Vec<i32>> =
ManuallyDrop::new(unsafe { Vec::from_raw_parts(ptr, 0, capacity) });
let other: Vec<i32> = vec![/* … */];
assert!(buffer.len() + other.len() <= buffer.capacity());
buffer.extend_from_slice(&other);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حاشیه ای در مورد رشته ها

اگر خروجی مورد انتظار شما از کتابخانه یک رشته است، ممکن است به‌جای یک رشته، ریز بهینه‌سازی را برای برگرداندن بردار u16 در نظر گرفته باشید، زیرا معمولاً موتورهای جاوا اسکریپت از UTF-16 در زیر هود استفاده می‌کنند.

با این حال، این یک اشتباه است زیرا تبدیل رشته شما به رشته C و استفاده از نوع cstring bun کمی سریعتر خواهد بود.در اینجا یک معیار با استفاده از یک کتابخانه معیار خوب انجام شده است mitata

function readStringU16(
create_string_u16: (ptr: Uint16Array, len: number) => void,
): string {
const length = 12;
const buffer = new Uint16Array(length);
create_string_u16(buffer, length)!;
const decoder = new TextDecoder(“utf-16”);
const copy = decoder.decode(buffer);
return copy;
}

function readStringC(create_string_c: () => CString): string {
const cstr = create_string_c();
const copy = cstr.toString();
return copy;
}

const readU16 = readStringU16.bind(null, create_string_u16);
const readC = readStringC.bind(null, create_string_c);
summary(() => {
bench(“read u16”, readU16);
bench(“read c”, readC);
});
await run({ colors: false });

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

const STRING: &str = “Hello, World”;

#[no_mangle] pub unsafe extern “C” fn create_string_u16(ptr: *mut u16, capacity: usize) {
let buffer: &mut [u16] = unsafe { slice::from_raw_parts_mut(ptr, capacity) };
let src: Vec<u16> = STRING.encode_utf16().collect::<Vec<u16>>();
buffer.copy_from_slice(&src);
}

#[no_mangle] pub extern “C” fn create_string_c() -> *const c_char {
ManuallyDrop::new(CString::new(STRING).unwrap()).as_ptr()
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

benchmark avg (min … max) p75 p99 (min … top 1%)
————————————– ——————————-
read u16 1.12 µs/iter 1.21 µs █▂
(905.96 ns … 1.83 µs) 1.79 µs ██▆▅▆▇▇▇▃▂▂▄▃▃▂▂▁▂▂▁▁
read c 727.82 ns/iter 809.01 ns █▂
(590.53 ns … 1.03 µs) 969.92 ns ▆███▆▆▇▃▆▄▄▆▃▄▆▆▃▂▃▂▂

summary
read c
1.54x faster than read u16

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مورد WebAssembly چطور؟

وقت آن است که به فیل در اتاقی که WebAssembly است خطاب کنیم.آیا باید پیوندهای زیبای WASM موجود را به جای برخورد با C ABI انتخاب کنید؟

پاسخ این است احتمالا هیچ کدام.

آیا واقعا ارزشش را دارد؟

معرفی زبان دیگری به پایگاه کد شما به چیزی بیش از یک گلوگاه نیاز دارد تا ارزش آن را از نظر DX و عملکرد عاقلانه داشته باشد.

در اینجا یک معیار برای یک ساده است range عملکرد در JS، WASM و Rust.

// rs.rs
#[no_mangle] pub unsafe extern “C” fn rs_range(ptr: *mut i32, start: i32, end: i32) {
let len: usize = usize::try_from(end – start).unwrap();
let buffer: &mut [i32] = unsafe { slice::from_raw_parts_mut(ptr, len) };
let src: Vec<i32> = (start..end).collect();
buffer.copy_from_slice(&src);
}

// wasm.rs
#[wasm_bindgen] pub fn wa_range(start: i32, end: i32) -> Vec<i32> {
(start..end).collect()
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

function tsRange(start: number, end: number): number[] {
return […Array(end – start).keys()].map((x) => x + start);
}

function wrapWa(
wa_range: (start: number, end: number) => Int32Array,
): (start: number, end: number) => number[] {
return (start, end) => {
return Array.from(wa_range(start, end));
};
}

function wrapRs(
rs_range: (ptr: Int32Array, start: number, end: number) => void,
): (start: number, end: number) => number[] {
return (start, end) => {
const buffer = new Int32Array(end – start);
rs_range(buffer, start, end);
return Array.from(buffer);
};
}

await init();
const waRange = wrapWa(wa_range);
const rsRange = wrapRs(rs_range);

summary(() => {
bench(“ts”, () => tsRange(100, 50000));
bench(“wa”, () => waRange(100, 50000));
bench(“rs”, () => rsRange(100, 50000));
});
await run({ colors: false });

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

benchmark avg (min … max) p75 p99 (min … top 1%)
————————————– ——————————-
ts 1.33 ms/iter 1.25 ms █
(802.90 µs … 5.29 ms) 4.22 ms ▂██▅▂▁▁▁▁▁▁▁▁▁▂▁▁▁▁▁▁
wa 1.58 ms/iter 1.72 ms █
(1.17 ms … 4.36 ms) 4.09 ms ██▄▃▄▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁
rs 1.47 ms/iter 1.43 ms ▂█
(1.17 ms … 4.11 ms) 3.84 ms ██▃▂▂▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
ts
1.1x faster than rs
1.18x faster than wa

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کتابخانه بومی به سختی WASM را شکست می دهد و به طور مداوم در اجرای TypeScript خالص شکست می خورد.

و این برای این آموزش برای/کاوش است bun:ffi ماژول امیدوارم همه ما کمی تحصیل کرده تر از این موضوع فاصله گرفته باشیم. نظرات و سوالات خود را در نظرات به اشتراک بگذارید

برای رسیدن به چه چیزی تلاش می کنیم

فرض کنید یک برنامه جاوا اسکریپت دارید که بصورت گروهی اجرا می شود و تنگناهایی را شناسایی کرده اید که می خواهید بهینه سازی کنید.
بازنویسی آن به زبانی کارآمدتر ممکن است تنها راه حلی باشد که به آن نیاز دارید.

به عنوان یک زمان اجرا JS مدرن، Bun از Interface Function خارجی (FFI) برای فراخوانی کتابخانه های نوشته شده به زبان های دیگری که از C ABI های آشکارسازی پشتیبانی می کنند، مانند C، C++، Rust و Zig پشتیبانی می کند.

در این پست به نحوه استفاده از آن می پردازیم و نتیجه می گیریم که آیا می توان از آن بهره مند شد یا خیر.

چگونه کتابخانه را به جاوا اسکریپت پیوند دهیم

این مثال از Rust استفاده می کند. ایجاد یک کتابخانه مشترک با پیوندهای C در زبان های دیگر متفاوت به نظر می رسد، اما ایده یکسان باقی می ماند.

از سمت JS

Bun از طریق FFI API خود را نشان می دهد bun:ffi ماژول

نقطه ورودی یک است dlopen تابع مسیری را طی می کند که یا مطلق است یا نسبت به فهرست کاری فعلی به فایل کتابخانه (خروجی ساخت با a .so پسوند برای لینوکس، .dylib برای macOS یا .dll برای ویندوز) و یک شی با امضای توابعی که می خواهید وارد کنید.
یک شی را با a برمی گرداند close روشی که ممکن است برای بستن کتابخانه زمانی که دیگر به آن نیاز نیست استفاده کنید symbols ویژگی که یک شی حاوی توابعی است که شما انتخاب کرده اید.

import {
  dlopen,
  FFIType,
  read,
  suffix,
  toArrayBuffer,
  type Pointer,
} from "bun:ffi";

// Both your script and your library don't typically change their locations
// Use `import.meta.dirname` to make your script independent from the cwd
const DLL_PATH =
  import.meta.dirname + `/../../rust-lib/target/release/library.${suffix}`;

function main() {
  // Deconstruct object to get functions
  // but collect `close` method into object
  // to avoid using `this` in a wrong scope
  const {
    symbols: { do_work },
    ...dll
  } = dlopen(DLL_PATH, {
    do_work: {
      args: [FFIType.ptr, FFIType.ptr, "usize", "usize"],
      returns: FFIType.void,
    },
  });

  /* ... */

  // It is unclear whether it is required or recommended to call `close`
  // an example says `JSCallback` instances specifically need to be closed
  // Note that using `symbols` after calling `close` is undefined behaviour
  dll.close();
}

main();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

انتقال داده از طریق مرز FFI

همانطور که ممکن است متوجه شوید، انواع پشتیبانی شده که bun از طریق FFI می پذیرد محدود به اعداد، از جمله اشاره گر هستند.
قابل توجه است size_t یا usize در لیست انواع پشتیبانی شده وجود ندارد، حتی اگر کد آن در نسخه 1.1.34 bun وجود داشته باشد.

Bun هیچ کمکی در انتقال داده های پیچیده تر از رشته C ارائه نمی دهد. یعنی باید خودتان با اشاره گرها کار کنید.

بیایید ببینیم چگونه یک اشاره گر را از جاوا اسکریپت به Rust منتقل کنیم …

{
  reconstruct_slice: {
    args: [FFIType.ptr, "usize"],
    returns: FFIType.void,
  },
}

const array = new BigInt64Array([0, 1, 3]);
// Bun automatically converts `TypedArray`s into pointers
reconstruct_slice(array, array.length);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// Reconstruct a `slice` that was initialized in JavaScript
unsafe fn reconstruct_slice(
    array_ptr: *const i64,
    length: libc::size_t,
) -> &[i64] {
    // Even though here it's not null, it's good practice to check
    assert!(!array_ptr.is_null());
    // Unaligned pointer can lead to undefined behaviour
    assert!(array_ptr.is_aligned());
    // Check that the array doesn't "wrap around" the address space
    assert!(length < usize::MAX / 4);
    let _: &[i64] = unsafe { slice::from_raw_parts(array_ptr, length) };
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

… و نحوه برگرداندن اشاره گر از Rust به جاوا اسکریپت.

{
  allocate_buffer: {
    args: [],
    returns: FFIType.ptr,
  },
  as_pointer: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
}

// Hardcoding this value for 64-bit systems
const BYTES_IN_PTR = 8;

const box: Pointer = allocate_buffer()!;
const ptr: number = read.ptr(box);
// Reading the value next to `ptr`
const length: number = read.ptr(box, BYTES_IN_PTR);
// Hardcoding `byteOffset` to be 0 because Rust guarantees that
// Buffer holds `i32` values which take 4 bytes
// Note how we need to call a no-op function `as_pointer` because
// `toArrayBuffer` takes a `Pointer` but `read.ptr` returns a `number`
const _buffer = toArrayBuffer(as_pointer(ptr)!, 0, length * 4);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

#[no_mangle]
pub extern "C" fn allocate_buffer() -> Box<[usize; 2]> {
    let buffer: Vec<i32> = vec![0; 10];
    let memory: ManuallyDrop<Vec<i32>> = ManuallyDrop::new(buffer);
    let ptr: *const i32 = memory.as_ptr();
    let length: usize = memory.len();
    // Unlike a `Vec`, `Box` is FFI compatible and will not drop
    // its data when crossing the FFI
    // Additionally, a `Box` where `T` is `Sized` will be a thin pointer
    Box::new([ptr as usize, length])
}

#[no_mangle]
pub const extern "C" fn as_pointer(ptr: usize) -> usize {
    ptr
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Rust نمی داند که JS مالکیت داده های طرف مقابل را در اختیار می گیرد، بنابراین باید صریحاً به آن بگویید واگذار نمی کند داده های روی پشته با استفاده از ManuallyDrop. زبان های دیگری که حافظه را مدیریت می کنند باید کاری مشابه انجام دهند.

مدیریت حافظه

همانطور که می بینیم، امکان تخصیص حافظه در JS و Rust وجود دارد و هیچ کدام نمی توانند با خیال راحت مدیریت حافظه دیگران

بیایید انتخاب کنیم که حافظه خود را کجا و چگونه تخصیص دهید.

در Rust اختصاص دهید

3 روش برای واگذاری پاکسازی حافظه به Rust از JS وجود دارد که همه مزایا و معایب خود را دارند.

استفاده کنید FinalizationRegistry

استفاده کنید FinalizationRegistry برای درخواست پاسخ به پاکسازی در حین جمع‌آوری زباله با ردیابی شی در جاوا اسکریپت.

{
  drop_buffer: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
}

const registry = new FinalizationRegistry((box: Pointer): void => {
  drop_buffer(box);
});
registry.register(buffer, box);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// This call assumes neither the box nor the buffer have been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_buffer(raw: *mut [usize; 2]) {
    let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
    let ptr: *mut i32 = box_[0] as *mut i32;
    let length: usize = box_[1];
    let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
    drop(buffer);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

منفی

  • جمع آوری زباله ها خاص موتور و غیر قطعی است
  • تماس برگشتی پاکسازی به هیچ وجه تضمین نمی شود که تماس گرفته شود

استفاده کنید toArrayBuffer's finalizationCallback پارامتر

ردیابی جمع‌آوری زباله را به bun واگذار کنید تا یک تماس پاکسازی مجدد برقرار شود.
هنگام انتقال 4 پارامتر به toArrayBuffer4 باید تابع C باشد تا در پاکسازی فراخوانی شود.
با این حال، هنگام ارسال 5 پارامتر، پارامتر پنجم تابع است و پارامتر چهارم باید یک نشانگر زمینه باشد که از آن عبور می کند.

{
  box_value: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
  drop_box: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
  drop_buffer: {
    args: [FFIType.ptr, FFIType.ptr],
    returns: FFIType.void,
  },
}

// Bun expects the context to specifically be a pointer
const finalizationCtx: Pointer = box_value(length)!;

// Note that despite the presence of these extra parameters in the docs,
// they're absent from `@types/bun`
//@ts-expect-error see above
const buffer = toArrayBuffer(
  as_pointer(ptr)!,
  0,
  length * 4,
  //@ts-expect-error see above
  finalizationCtx,
  drop_buffer,
);
// Don't leak the box used to pass buffer through FFI
drop_box(box);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

#[no_mangle]
pub unsafe extern "C" fn box_value(value: usize) -> Box<usize> {
    Box::new(value)
}

/// # Safety
///
/// This call assumes the box hasn't been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_box(raw: *mut [usize; 2]) {
    let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
    drop(box_);
}

/// As per bun docs, expected signature like in JavaScriptCore's `JSTypedArrayBytesDeallocator`
/// https://developer.apple.com/documentation/javascriptcore/jstypedarraybytesdeallocator?language=objc
///
/// # Safety
///
/// This call assumes the buffer hasn't been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_buffer(ptr: *mut i32, len: *mut usize) {
    // reconstruct the context box to not leak it
    let length: usize = unsafe { *Box::from_raw(len) };
    let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
    drop(buffer);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

  • واگذاری منطق به خارج از جاوا اسکریپت

منفی

  • دیگ بخار زیاد و احتمال نشتی حافظه
  • حاشیه نویسی نوع برای toArrayBuffer
  • جمع آوری زباله ها خاص موتور و غیر قطعی است
  • تماس برگشتی پاکسازی به هیچ وجه تضمین نمی شود که تماس گرفته شود

حافظه را به صورت دستی مدیریت کنید

فقط بعد از اینکه دیگر به آن نیاز ندارید، خودتان آن را رها کنید.
خوشبختانه TypeScript بسیار مفید است Disposable رابط برای این و using کلمه کلیدی
این معادل پایتون است with یا سی شارپ using کلمات کلیدی

برای آن به اسناد مراجعه کنید

{
  drop_box: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
  drop_buffer: {
    args: [FFIType.ptr, "usize"],
    returns: FFIType.void,
  },
}

class RustVector implements Disposable {
  constructor(
    private inner: Int32Array,
    private dropBuffer: (ptr: Int32Array, len: number) => void,
  ) {}
  [Symbol.dispose](): void {
    this.dropBuffer(this.inner, this.inner.length);
  }
}

const buffer = new Int32Array(toArrayBuffer(as_pointer(ptr)!, 0, length * 4));
// Don't leak the box used to pass buffer through FFI
drop_box(box);
using wrapper = new RustVector(buffer, drop_buffer);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// This call assumes the box hasn't been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_box(raw: *mut [usize; 2]) {
    let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
    drop(box_);
}

/// # Safety
///
/// This call assumes the buffer hasn't been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_buffer(ptr: *mut i32, length: usize) {
    let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
    drop(buffer);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جوانب مثبت

  • پاکسازی تضمین شده است که اجرا شود
  • شما کنترل دارید که چه زمانی می خواهید حافظه را حذف کنید

منفی

  • شی دیگ بخار برای Disposable رابط
  • حذف دستی حافظه کندتر از استفاده از زباله جمع کن است
  • اگر می‌خواهید مالکیت بافر را واگذار کنید، باید یک کپی کنید و نسخه اصلی را رها کنید

تخصیص در JS

این بسیار ساده‌تر و ایمن‌تر است زیرا واگذاری برای شما انجام می‌شود.

با این حال، یک اشکال قابل توجه وجود دارد.
از آنجایی که نمی‌توانید حافظه جاوا اسکریپت را در Rust مدیریت کنید، نمی‌توانید از ظرفیت بافر عبور کنید زیرا این امر باعث ایجاد یک توزیع می‌شود. این بدان معناست که قبل از ارسال آن به Rust باید اندازه بافر را بدانید.
ندانستن تعداد بافرهایی که از قبل نیاز دارید نیز هزینه های زیادی را متحمل می شود زیرا فقط برای تخصیص از طریق FFI به این سو و آن سو می روید.

const CAPACITY = 20;
// The buffer is initialized with zeroes
const buffer = new Int32Array(CAPACITY);
mutate_buffer(buffer, CAPACITY);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

/// # Safety
///
/// Ensure that the length of the buffer is at least `capacity` size
#[no_mangle]
pub unsafe extern "C" fn mutate_buffer(ptr: *mut i32, capacity: usize) {
    assert!(!ptr.is_null());
    assert!(ptr.is_aligned());
    assert!(capacity < usize::MAX / 4);
    // Manually drop so that vec doesn't get cleaned up
    let mut buffer: ManuallyDrop<Vec<i32>> =
        ManuallyDrop::new(unsafe { Vec::from_raw_parts(ptr, 0, capacity) });
    let other: Vec<i32> = vec![/* ... */];
    assert!(buffer.len() + other.len() <= buffer.capacity());
    buffer.extend_from_slice(&other);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حاشیه ای در مورد رشته ها

اگر خروجی مورد انتظار شما از کتابخانه یک رشته است، ممکن است به‌جای یک رشته، ریز بهینه‌سازی را برای برگرداندن بردار u16 در نظر گرفته باشید، زیرا معمولاً موتورهای جاوا اسکریپت از UTF-16 در زیر هود استفاده می‌کنند.

با این حال، این یک اشتباه است زیرا تبدیل رشته شما به رشته C و استفاده از نوع cstring bun کمی سریعتر خواهد بود.
در اینجا یک معیار با استفاده از یک کتابخانه معیار خوب انجام شده است mitata

function readStringU16(
  create_string_u16: (ptr: Uint16Array, len: number) => void,
): string {
  const length = 12;
  const buffer = new Uint16Array(length);
  create_string_u16(buffer, length)!;
  const decoder = new TextDecoder("utf-16");
  const copy = decoder.decode(buffer);
  return copy;
}

function readStringC(create_string_c: () => CString): string {
  const cstr = create_string_c();
  const copy = cstr.toString();
  return copy;
}

const readU16 = readStringU16.bind(null, create_string_u16);
const readC = readStringC.bind(null, create_string_c);
summary(() => {
  bench("read u16", readU16);
  bench("read c", readC);
});
await run({ colors: false });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

const STRING: &str = "Hello, World";

#[no_mangle]
pub unsafe extern "C" fn create_string_u16(ptr: *mut u16, capacity: usize) {
    let buffer: &mut [u16] = unsafe { slice::from_raw_parts_mut(ptr, capacity) };
    let src: Vec<u16> = STRING.encode_utf16().collect::<Vec<u16>>();
    buffer.copy_from_slice(&src);
}

#[no_mangle]
pub extern "C" fn create_string_c() -> *const c_char {
    ManuallyDrop::new(CString::new(STRING).unwrap()).as_ptr()
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

benchmark              avg (min … max) p75   p99    (min … top 1%)
-------------------------------------- -------------------------------
read u16                  1.12 µs/iter   1.21 µs █▂
                 (905.96 ns … 1.83 µs)   1.79 µs ██▆▅▆▇▇▇▃▂▂▄▃▃▂▂▁▂▂▁▁
read c                  727.82 ns/iter 809.01 ns  █▂
                 (590.53 ns … 1.03 µs) 969.92 ns ▆███▆▆▇▃▆▄▄▆▃▄▆▆▃▂▃▂▂

summary
  read c
   1.54x faster than read u16
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مورد WebAssembly چطور؟

وقت آن است که به فیل در اتاقی که WebAssembly است خطاب کنیم.
آیا باید پیوندهای زیبای WASM موجود را به جای برخورد با C ABI انتخاب کنید؟

پاسخ این است احتمالا هیچ کدام.

آیا واقعا ارزشش را دارد؟

معرفی زبان دیگری به پایگاه کد شما به چیزی بیش از یک گلوگاه نیاز دارد تا ارزش آن را از نظر DX و عملکرد عاقلانه داشته باشد.

در اینجا یک معیار برای یک ساده است range عملکرد در JS، WASM و Rust.

// rs.rs
#[no_mangle]
pub unsafe extern "C" fn rs_range(ptr: *mut i32, start: i32, end: i32) {
    let len: usize = usize::try_from(end - start).unwrap();
    let buffer: &mut [i32] = unsafe { slice::from_raw_parts_mut(ptr, len) };
    let src: Vec<i32> = (start..end).collect();
    buffer.copy_from_slice(&src);
}

// wasm.rs
#[wasm_bindgen]
pub fn wa_range(start: i32, end: i32) -> Vec<i32> {
    (start..end).collect()
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

function tsRange(start: number, end: number): number[] {
  return [...Array(end - start).keys()].map((x) => x + start);
}

function wrapWa(
  wa_range: (start: number, end: number) => Int32Array,
): (start: number, end: number) => number[] {
  return (start, end) => {
    return Array.from(wa_range(start, end));
  };
}

function wrapRs(
  rs_range: (ptr: Int32Array, start: number, end: number) => void,
): (start: number, end: number) => number[] {
  return (start, end) => {
    const buffer = new Int32Array(end - start);
    rs_range(buffer, start, end);
    return Array.from(buffer);
  };
}

await init();
const waRange = wrapWa(wa_range);
const rsRange = wrapRs(rs_range);

summary(() => {
  bench("ts", () => tsRange(100, 50000));
  bench("wa", () => waRange(100, 50000));
  bench("rs", () => rsRange(100, 50000));
});
await run({ colors: false });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

benchmark              avg (min … max) p75   p99    (min … top 1%)
-------------------------------------- -------------------------------
ts                        1.33 ms/iter   1.25 ms  █                   
                 (802.90 µs … 5.29 ms)   4.22 ms ▂██▅▂▁▁▁▁▁▁▁▁▁▂▁▁▁▁▁▁
wa                        1.58 ms/iter   1.72 ms  █                   
                   (1.17 ms … 4.36 ms)   4.09 ms ██▄▃▄▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁
rs                        1.47 ms/iter   1.43 ms ▂█                   
                   (1.17 ms … 4.11 ms)   3.84 ms ██▃▂▂▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
  ts
   1.1x faster than rs
   1.18x faster than wa
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کتابخانه بومی به سختی WASM را شکست می دهد و به طور مداوم در اجرای TypeScript خالص شکست می خورد.

و این برای این آموزش برای/کاوش است bun:ffi ماژول امیدوارم همه ما کمی تحصیل کرده تر از این موضوع فاصله گرفته باشیم.
نظرات و سوالات خود را در نظرات به اشتراک بگذارید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا