ffi native dynamic library
ffi native dynamic library
Function
ffi allows BT scripts to directly call C ABI functions in native dynamic libraries, such as .dll for Windows, .so for Linux and .dylib for macOS.
The default BT builds for supported targets already have FFI built in, no additional components need to be installed. The static musl bt in the official Linux package does not include FFI to maintain cross-distro compatibility; bt_app built for the GNU target in the package still contains FFI. Before use, you can use BT.has('ffi') to determine whether the current binary has FFI enabled, then prepare a dynamic library that matches the current operating system and CPU architecture, and confirm the exported name and complete prototype of the function to be called from the dynamic library document or C header file.
FFI is suitable for calling existing native capabilities in long-lived VMs from CLI scripts and desktop apps. It cannot be used in web request scripts.
Quick Start
The following Windows example loads user32.dll, declares the complete signature of MessageBoxW, and closes the dynamic library after calling the function:
user32 = ffi.load('user32.dll', { MessageBoxW: 'i32(ptr, wstr, wstr, u32)' }) button = user32.MessageBoxW( null, 'BT has successfully called user32.dll', 'BT FFI', 64 ) // After clicking "OK", output: 1 print button ffi.close(user32)
The format of the complete signature is:
return_type(parameter_type, parameter_type, ...)
i32(ptr, wstr, wstr, u32) in the example corresponds to the 32-bit integer return value in the C prototype, a pointer, two Windows wide strings, and a 32-bit unsigned integer.
Syntax
library = ffi.load(path) library = ffi.load(path, schema) result = library.ExactExportName(...args) buffer = ffi.buffer(size) closed = ffi.close(library) closed = ffi.close(buffer)
The recommended usage sequence is: load the dynamic library, declare the function with a complete signature, call the function, handle the return value, close the Buffer and the dynamic library.
ffi.load
Loads the dynamic library and returns a FfiLibrary object.
Parameters
| Parameter | Type | Required | Default value | Valid range or optional value | Description |
|---|---|---|---|---|---|
| path | String | Yes | None | Bare library name or dynamic library file path | The bare library name is left to the operating system to find; the path with directory supports the current source code directory, @ project root and absolute path. Cannot be empty or contain NUL. |
| schema | Object | No | None | 0..=128 functions | Declares the functions that are allowed to be called and their signatures. When passed in, only the exact export names listed in the schema can be called; an empty object means no functions are allowed to be called. |
schema fields
| Field | Type | Required | Default value | Valid range or optional value | Description |
|---|---|---|---|---|---|
| Exact export name | String | Required for each function to be called | None | Not empty, no NUL, up to 255 UTF-8 bytes | Field names are case-sensitive. BT does not automatically add, remove or replace the A, W suffixes. |
| Function declaration | String | Yes | None | Full signature or separate return type, up to 512 UTF-8 bytes | Full signature declares up to 16 arguments. It is recommended to always use the full signature. |
Supported signature types
| Type | Available positions | BT parameters | BT return value | Description |
|---|---|---|---|---|
| void | Return type only | None | empty | Native functions have no return value. |
| i8, i16, i32, i64 | Parameters, returns | Int, Bool | Int | Signed integer; parameters must be within the range of the declared width, Bool is converted to 0 or 1. |
| u8, u16, u32, u64 | Parameters, return | Non-negative Int, Bool | Int | Unsigned integer; an error will be reported if it exceeds the declared range. The u64 return value cannot exceed the upper limit of BT Int. |
| isize, usize | Parameters, returns | Int | Int | Pointer width integer; currently supported platforms are checked as 64-bit. |
| f32, f64 | Parameters, Returns | Int, Float | Float | Float; f32 or f64 must be explicitly selected. |
| ptr | Parameters, return | null, FfiPointer, FfiBuffer | FfiPointer, null | FfiBuffer can be passed in directly as a stable address; the native null pointer returns null. |
| cstr | Parameters, Returns | String, null | String, null | UTF-8 NUL terminated string. Immediately copy to BT String upon return, illegal UTF-8 returns null. |
| wstr | Parameters, Returns | String, null | String, null | Windows only; UTF-16 NUL terminated string. Immediately copy to BT String upon return, illegal UTF-16 returns null. |
Return Value
| Type | Description |
|---|---|
| FfiLibrary | A loaded dynamic-library object that calls native functions through library.export_name(...). |
Declare and call function
Complete signature
The complete signature will clarify the number of parameters, the ABI type of each parameter and the return type, which is the safest and easiest to check method of calling. MessageBoxW: 'i32(ptr, wstr, wstr, u32)' in the quick start is the complete signature; the parameterless function is written as cstr(), and the function without return value is written as void(). After calling, String and empty are obtained respectively.
The number, type, and integer range of function parameters must be consistent with the schema. Detectable type errors, out-of-bounds, internal NUL, resource closed, insufficient permissions, or non-existent symbols will all cause errors before entering the native function.
Declaring only the return type
When writing only the return type, BT fixes the return type and infers the parameters according to limited rules on the first call. The following Windows example declares the return value as ptr and uses the String parameter as wstr based on the W suffix of the function name:
user32 = ffi.load('user32.dll', { FindWindowW: 'ptr' }) window = user32.FindWindowW(null, 'BT FFI') // Output: null, or matching window's FfiPointer print window ffi.close(user32)
Omitting schema
Omitting schema allows you to call the legal exact export name, but the return type is fixed to i32. It can only be used if the true prototype of the function exactly conforms to the following rules:
| BT Parameters | Inferred Type | Conditions |
|---|---|---|
| Int | i32 | Must be in the i32 range. |
null | ptr | As a normal null pointer, the string type is not inferred. |
| FfiPointer, FfiBuffer | ptr | The resource must still be valid. |
| String | wstr | Windows only, and the export name ends with uppercase W. |
| ASCII String | cstr pointer | Windows only, and the export name ends with uppercase A. |
Bool, Float, empty, Bytes, Array, Object, etc. cannot be automatically inferred and must use full signatures. After the first successful call, the number of parameters and inferred types of the function are fixed, and subsequent calls must remain consistent.
user32 = ffi.load('user32.dll') screen_width = user32.GetSystemMetrics(0) // Output: current screen width, value greater than 0 print screen_width ffi.close(user32)
If the native return value is not i32, or the function uses parameters such as Bool, Float, 64-bit integer, string, etc., do not omit schema.
FfiPointer
When the native function is declared as ptr, the empty address returns null, and the non-empty address returns FfiPointer.
FfiPointer can only be used for null checking, comparison, or passed back to FFI as ptr parameter. BT does not allow reading, writing, conversion to integers, or pointer arithmetic. It relies on the source dynamic library or FfiBuffer; after the corresponding resource is closed, the Pointer will become invalid immediately.
If the native function returns a pointer that needs to be released, the return type should be declared as ptr, and then the release function provided by the dynamic library should be called. Do not declare the address that needs to be released as cstr or wstr.
ffi.buffer
Creates fixed-length, zero-padded, address-stable, writable memory that is at least 16-byte aligned and returns FfiBuffer. It is suitable for passing to ptr parameters that need to write data by native functions.
Parameters
| Parameter | Type | Required | Default value | Valid range or optional value | Description |
|---|---|---|---|---|---|
| size | Int | Yes | None | 1..=BT_BYTES_LIMIT | The visible byte length of the Buffer; it cannot be expanded after creation. |
Return Value
| Type | Description |
|---|---|
| FfiBuffer | BT managed writable native memory, which can be passed directly to the parameter declared as ptr. |
Method
| Method | Parameters | Required | Default value | Valid range | Return value | Description |
|---|---|---|---|---|---|---|
| len() | None | None | None | None | Int | Returns the visible byte length of the Buffer. |
| ptr(offset) | offset: Int | No | 0 | 0..=len() | FfiPointer | Returns the address of the specified offset; the len() position can only be passed as the end pointer. |
| write(data, offset) | data: Bytes; offset: Int | data is yes; offset is no | offset is 0 | The writing range cannot exceed the bounds | Int | Write Bytes into Buffer and return the number of written bytes. |
| to_bytes(offset, length) | offset: Int; length: Int | No | offset is 0; length is the remaining length | The read range cannot go out of bounds | Bytes | Copy the specified range into ordinary Bytes. |
| to_string(offset) | offset: Int | No | 0 | 0..len() | String, null | Read NUL-terminated UTF-8; illegal UTF-8 returns null, and an error will be reported if NUL is not found. |
| to_wstring(offset) | offset: Int | No | 0 | Windows only; offset must be 2-byte aligned | String, null | Read NUL-terminated UTF-16; illegal UTF-16 returns null. |
The following example has a native function write UTF-8 text:
sdk = ffi.load('./sdk.dll', { sdk_write_text: 'i32(ptr, usize)' }) buffer = ffi.buffer(256) written = sdk.sdk_write_text(buffer, buffer.len()) text = buffer.to_string() bytes = buffer.to_bytes(0, written) // Output: written by native function UTF-8 text print text ffi.close(buffer) ffi.close(sdk)
ffi.close
Turn off FfiLibrary or FfiBuffer. Resources should be actively shut down when they are no longer in use, and resident processes especially need to be released in time.
Parameters
| Parameters | Type | Required | Default value | Valid range or optional value | Description |
|---|---|---|---|---|---|
| value | FfiLibrary, FfiBuffer | Yes | None | The resource returned by ffi.load() or ffi.buffer() | FfiPointer cannot be closed individually. |
Return Value
| Type | Description |
|---|---|
| Bool | Return true when the resource is closed successfully for the first time; return false when the resource has been closed. |
After closing the dynamic library, the functions in it can no longer be called; after closing the Buffer, its methods can no longer be accessed. FfiPointers generated by these resources will also expire at the same time.
Capability and resource status
enabled = BT.has('ffi') stats = BT.stats().ffi // FFI-enabled interpreter output: true; static musl bt output in the official Linux package: false print enabled // Output: number of currently open dynamic libraries print stats.open_libraries
| Field | Type | Description |
|---|---|---|
| enabled | Bool | Whether the current interpreter contains FFI. |
| open_libraries | Int | The number of currently open dynamic libraries. |
| buffers | Int | The number of currently alive FfiBuffers. |
| buffer_bytes | Int | The total number of bytes actually occupied by the current FfiBuffer. |
The same process can open up to 32 dynamic libraries at the same time, keep 256 FfiBuffers alive, and the total buffer size can be up to 64 MiB. A single schema can declare up to 128 functions, and a single complete signature can contain up to 16 parameters; when the schema is omitted, a single dynamic library can use up to 256 different functions.
Applicable platforms
- Windows x64
x86_64-unknown-linux-gnu, without musl)
- macOS Intel
- macOS Apple Silicon
All platforms use the target system's default C ABI. wstr and the ability to infer strings based on the A, W suffixes are only available on Windows.
Notes
- It is preferred to use the complete signature and check the official dynamic library documentation or C header file item by item. Wrong parameter types, return types, or calling conventions can directly lead to process crashes.
- FFI is not a sandbox. Crash, illegal memory access and other native errors in dynamic libraries cannot be safely caught by BT; dynamic libraries that are untrusted or require timeout isolation should be called in independent sub-processes.
- Native calls are synchronous blocking operations. Do not call time-consuming functions in execution paths that require asynchronous non-blocking execution.
- String passed to
cstrorwstris only valid during this synchronous call; native functions must not save this address. FfiBuffer cannot be saved by native functions and used asynchronously after the call is completed.
cstr and wstr return NUL-terminated strings that are only guaranteed to be readable by native documents. BT will copy the text immediately, but will not release the address for the native library.
- BOOL for Windows x64 is typically declared as i32, handles as ptr, and strings for the W API as wstr.
-
ffiis controlled by theffipermissions inBT_PERMISSION_ALLOWandBT_PERMISSION_DENY;ffi.close()is always allowed to execute, ensuring that existing resources can still be released after the permissions are tightened. - When distributing BT executable files containing FFI by yourself, you should fulfill the corresponding obligations according to the licenses you use; the BT build process will not generate additional license bypass files.
examples/ffi-user32/.