내장 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" 입니다
// 편의를 위해 사용할 수 있지만 필수는 아닙니다
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // 호출할 함수
},
} = dlopen(
path, // 라이브러리 이름 또는 파일 경로
{
sqlite3_libversion: {
// 인자 없음, 문자열 반환
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`SQLite 3 버전: ${sqlite3_libversion()}`);성능
우리 벤치마크 에 따르면 bun:ffi 는 Node.js FFI(Node-API経由) 보다 약 2-6 배 빠릅니다.
Bun 은 JavaScript 타입과 네이티브 타입 간의 값을 효율적으로 변환하는 C 바인딩을 생성하고 JIT 컴파일합니다. C 를 컴파일하기 위해 Bun 은 작고 빠른 C 컴파일러인 TinyCC 를 임베드합니다.
사용법
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 바이트), 일반적으로
- 길이가 저장되지 않음. 대신 문자열은 null 로 종료되며 길이는 첫 번째
\0의 인덱스입니다 - 가변
이를 해결하기 위해 bun:ffi 는 JavaScript 의 내장 String 을 확장하여 null 종료 문자열을 지원하고 몇 가지 추가 기능을 제공하는 CString 을 내보냅니다:
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;
}null 종료 문자열 포인터에서 JavaScript 문자열로 변환하려면:
const myString = new CString(ptr);알려진 길이의 포인터에서 JavaScript 문자열로 변환하려면:
const myString = new CString(ptr, 0, byteLength);new CString() 생성자는 C 문자열을 복제하므로 ptr 이 해제된 후에도 myString 을 계속 사용해도 안전합니다.
my_library_free(myString.ptr);
// myString 이 복제본이므로 안전합니다
console.log(myString);returns 에서 사용될 때 FFIType.cstring 은 포인터를 JavaScript string 으로 강제 변환합니다. args 에서 사용될 때 FFIType.cstring 은 ptr 과 동일합니다.
함수 포인터
NOTE
비동기 함수는 아직 지원되지 않습니다JavaScript 에서 함수 포인터를 호출하려면 CFunction 을 사용합니다. 이는 이미 일부 심볼을 로드한 Node-API(napi) 와 함께 사용할 때 유용합니다.
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() 을 사용하지 않으므로 유효한 포인터를 제공해야 합니다
// 해당 포인터는 숫자나 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 을 사용하여 C/FFI 함수에 전달할 수 있는 JavaScript 콜백 함수를 생성합니다. C/FFI 함수는 JavaScript/TypeScript 코드를 호출할 수 있습니다. 이는 비동기 코드나 C 에서 JavaScript 코드를 호출하려는 경우에 유용합니다.
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` 객체 대신 `JSCallback.prototype.ptr` 을 직접 전달하세요: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 은 JavaScript 에서 포인터 를 number 로 표현합니다.
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 에서
JavaScript 에서 TypedArray 가 더 이상 사용되지 않을 때를 추적하려면 FinalizationRegistry 를 사용할 수 있습니다.
C, Rust, Zig 등에서
C 나 FFI 에서 TypedArray 가 더 이상 사용되지 않을 때를 추적하려면 toArrayBuffer 나 toBuffer 에 콜백과 선택적 컨텍스트 포인터를 전달할 수 있습니다. 이 함수는 가비지 컬렉터가 기본 ArrayBuffer JavaScript 객체를 해제할 때 나중에 호출됩니다.
예상되는 시그니처는 JavaScriptCore 의 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 외부에서.raw 포인터를 사용하는 것은 강력히 권장되지 않습니다. Bun 의 향후 버전에서는 bun:ffi 를 비활성화하는 CLI 플래그를 추가할 수 있습니다.
포인터 정렬
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,
// dimensions:
128,
128,
);포인터 읽기
const out = encode_png(
// pixels 는 포인터로 전달됩니다
pixels,
// dimensions:
128,
128,
);
// 0 종료라고 가정하면 이렇게 읽을 수 있습니다:
let png = new Uint8Array(toArrayBuffer(out));
// 디스크에 저장:
await Bun.write("out.png", png);