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" 입니다
// 편의를 위해 사용할 수 있지만 필수는 아닙니다
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

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 타입별칭
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 인자는 TypedArrayDataView 여야 합니다.


문자열

JavaScript 문자열과 C 스타일 문자열은 다르며, 이는 네이티브 라이브러리와 함께 문자열 사용을 복잡하게 만듭니다.

JavaScript 문자열과 C 문자열의 차이점은 무엇인가요?">

JavaScript 문자열:

  • UTF16(문자당 2 바이트) 또는 잠재적으로 latin1, JavaScript 엔진 및 사용된 문자에 따라 다름
  • length 가 별도로 저장됨
  • 불변

C 문자열:

  • UTF8(문자당 1 바이트), 일반적으로
  • 길이가 저장되지 않음. 대신 문자열은 null 로 종료되며 길이는 첫 번째 \0 의 인덱스입니다
  • 가변

이를 해결하기 위해 bun:ffi 는 JavaScript 의 내장 String 을 확장하여 null 종료 문자열을 지원하고 몇 가지 추가 기능을 제공하는 CString 을 내보냅니다:

ts
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 문자열로 변환하려면:

ts
const myString = new CString(ptr);

알려진 길이의 포인터에서 JavaScript 문자열로 변환하려면:

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

new CString() 생성자는 C 문자열을 복제하므로 ptr 이 해제된 후에도 myString 을 계속 사용해도 안전합니다.

ts
my_library_free(myString.ptr);

// myString 이 복제본이므로 안전합니다
console.log(myString);

returns 에서 사용될 때 FFIType.cstring 은 포인터를 JavaScript string 으로 강제 변환합니다. args 에서 사용될 때 FFIType.cstringptr 과 동일합니다.


함수 포인터

NOTE

비동기 함수는 아직 지원되지 않습니다

JavaScript 에서 함수 포인터를 호출하려면 CFunction 을 사용합니다. 이는 이미 일부 심볼을 로드한 Node-API(napi) 와 함께 사용할 때 유용합니다.

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() 을 사용하지 않으므로 유효한 포인터를 제공해야 합니다
    // 해당 포인터는 숫자나 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 코드를 호출하려는 경우에 유용합니다.

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` 객체 대신 `JSCallback.prototype.ptr` 을 직접 전달하세요:
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 은 JavaScript 에서 포인터number 로 표현합니다.

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 와 유사하게 동작하지만 DataViewArrayBuffer 를 생성할 필요가 없으므로 일반적으로 더 빠릅니다.

FFITyperead 함수
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 에서

JavaScript 에서 TypedArray 가 더 이상 사용되지 않을 때를 추적하려면 FinalizationRegistry 를 사용할 수 있습니다.

C, Rust, Zig 등에서

C 나 FFI 에서 TypedArray 가 더 이상 사용되지 않을 때를 추적하려면 toArrayBuffertoBuffer 에 콜백과 선택적 컨텍스트 포인터를 전달할 수 있습니다. 이 함수는 가비지 컬렉터가 기본 ArrayBuffer JavaScript 객체를 해제할 때 나중에 호출됩니다.

예상되는 시그니처는 JavaScriptCore 의 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 외부에서.raw 포인터를 사용하는 것은 강력히 권장되지 않습니다. Bun 의 향후 버전에서는 bun:ffi 를 비활성화하는 CLI 플래그를 추가할 수 있습니다.

포인터 정렬

API 가 charu8 외의 크기의 포인터를 기대한다면 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,

  // dimensions:
  128,
  128,
);

포인터 읽기

ts
const out = encode_png(
  // pixels 는 포인터로 전달됩니다
  pixels,

  // dimensions:
  128,
  128,
);

// 0 종료라고 가정하면 이렇게 읽을 수 있습니다:
let png = new Uint8Array(toArrayBuffer(out));

// 디스크에 저장:
await Bun.write("out.png", png);

Bun by www.bunjs.com.cn 편집