# 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: ```bt 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: ```text 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 ```bt 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: ```bt 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. ```bt 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: ```bt 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 ```bt 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 - Linux x64 GNU (`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. - Structures, unions, variadic arguments, callbacks, and pointer arithmetic are not supported, and are not suitable for APIs requiring the UI main thread, COM apartments, native thread callbacks, or asynchronous saving of pointers. - String passed to `cstr` or `wstr` is 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`. - `ffi` is controlled by the `ffi` permissions in `BT_PERMISSION_ALLOW` and `BT_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. - The complete Windows example is at `examples/ffi-user32/`.