Skip to content

@lakutata/ffi ​

Native libraries for the lakutata framework: the C functions of .so, .dylib and .dll files called from Node.js, the main process of Electron and Bun, without compiling anything. A library is declared with the C declarations of its headers (structs, unions, enums, typedefs, pointers to functions, prototypes, #define), and each library is a component of the application. It is built on koffi. The lakutata package does not re-export this package: import it from @lakutata/ffi.

Installation ​

shell
npm install lakutata @lakutata/ffi
  • koffi comes with a package of prebuilt binaries for each platform, installed with it; npm may warn that its install script was not run: it is not needed.
  • Node.js ^22.13.0 || >=24.11.0, the main process of Electron, or Bun. Its peer dependency @lakutata/core is installed with lakutata.

Usage ​

A library ​

A subclass of NativeLibrary: @Library gives its file names (one by platform, or one completed as each platform names libraries) and its declarations, @Native binds a function to a property, @NativeVariable a variable.

typescript
import {Application} from 'lakutata'
import {buildNativeLibraryOptions, Library, Native, NativeLibrary, NativeVariable, type Out, type Pointer} from '@lakutata/ffi'

@Library({
    names: {darwin: 'libsqlite3.dylib', linux: 'libsqlite3.so.0', win32: 'winsqlite3.dll'},
    declarations: `
        typedef struct sqlite3 sqlite3;
        #define SQLITE_OK 0
        int sqlite3_close(sqlite3 *db);
    `
})
export class SQLite extends NativeLibrary {
    @Native('int sqlite3_open(const char *filename, _Out_ sqlite3 **db)')
    public readonly open: (filename: string, db: Out<Pointer>) => number

    //Runs in a thread of the pool; the error message it gives is freed by sqlite3_free
    @Native('int sqlite3_exec(sqlite3 *db, const char *sql, void *callback, void *context, _Out_ char **error)', {async: true, free: 'sqlite3_free'})
    public readonly exec: (db: Pointer, sql: string, callback: null, context: null, error: Out<string>) => Promise<number>

    //The function of the declarations named as the property
    @Native()
    public readonly sqlite3_close: (db: Pointer) => number

    @NativeVariable('extern const char sqlite3_version[]')
    public readonly version: string
}

Application
    .run(() => ({
        id: 'store.app',
        name: 'Store',
        components: {
            sqlite: buildNativeLibraryOptions(SQLite)
        }
    }))
    .onLaunched(async (app: Application): Promise<void> => {
        const sqlite: SQLite = await app.getObject('sqlite')
        const db: Out<Pointer> = [null]
        if (sqlite.open(':memory:', db) !== Number(sqlite.constants.SQLITE_OK)) throw new Error('open failed')
        const error: Out<string> = [null]
        if (await sqlite.exec(db[0]!, 'CREATE TABLE t (id INTEGER)', null, null, error)) console.error(error[0])
        sqlite.sqlite3_close(db[0]!)
        console.log(sqlite.version)
    })
  • Where it is found: the path of the configuration (alone, when given), the directories of search in the configuration and in @Library, the directory native of the resources of a packaged application, then where the system looks for libraries. c is the C library on each system (musl included), m its mathematical functions.
  • The values: numbers; strings for char *; bigints for pointers and for every 64-bit integer (size_t, long, int64_t…), or numbers with {int64: 'number'}; objects for structs and unions, their bitfields by their names; an array of one element for an _Out_ integer or pointer (Out<T>).
  • Callbacks: a JS function given for a pointer to a function is valid during the call; callback(type, fn) makes one the library keeps, whose exceptions are error events of the component.
  • Destroyed with the application: its properties throw, the asynchronous calls running are awaited, its callbacks released, the library unloaded.

Without the container ​

typescript
import {loadNativeLibrary, type NativeLibraryHandle} from '@lakutata/ffi'

const libc: NativeLibraryHandle = loadNativeLibrary('c', 'typedef struct { int quot; int rem; } div_t;')
const div = libc.func<(numerator: number, denominator: number) => {quot: number, rem: number}>('div_t div(int numerator, int denominator)')
const snprintf = libc.func<(buffer: Buffer, size: number, format: string, ...args: unknown[]) => number>('int snprintf(char *buffer, size_t size, const char *format, ...)')
console.log(div(17, 5))
//The types of the extra arguments of a variadic function, by their C names
const buffer: Buffer = Buffer.alloc(32)
snprintf(buffer, buffer.length, '%s=%d', 'const char *', 'count', 'int', 42)
await libc.close()

Packaging ​

The bundles, the executables and the installers of Electron made by lakutata package carry koffi and its binary for the platform. A library shipped with the application goes in the resources, in native ("lakutata": {"resources": [{"from": "vendor/lib", "to": "native"}]} in the package.json). On Alpine, make the executables with Bun (--runtime bun).

API ​

ExportWhat it is
NativeLibraryThe component of a library: its functions and variables on its properties; func, variable, callback, read, write, sizeOf, offsetOf, alloc, free, errno, constants, file, pending; the error event of its callbacks
buildNativeLibraryOptions, NativeLibraryConfigIts declaration in the components, and its options: path, search, keepLoaded, drainTimeout
Library, LibraryOptionsClass decorator: the library of a NativeLibrary subclass: names, search, declarations, crt
NativeProperty decorator: a function (a prototype, a declared name, or the name of the property) and its options: async, free, int64
NativeVariableProperty decorator: a variable the library exports
loadNativeLibrary, NativeLibraryHandle, NativeLibraryOptionsA library without the container: func, function, variable, callback, declare, read, write, sizeOf, offsetOf, alloc, free, errno, drain, close
NativeFunctionOptions, NativeVariableAccessor, NativeCallback, LibraryNames, Pointer, OutThe options of a function, a variable, a persistent callback (pointer, release()), the names of a library, a pointer (a bigint), an output parameter
readCString, readWString, readBuffer, viewBuffer, readValue, readArray, writeValue, allocMemory, freeMemory, sizeOfNative memory, with the built-in types (viewBuffer does not copy; Electron refuses it)
lastErrno, lastWindowsErrorThe errno of the last call (on Windows, the one of the C library given), the error of the Windows API

The typings document every declaration, with examples.

Errors ​

ErrorWhen
NativeDeclarationExceptionDeclarations or a prototype cannot be read (with their line and column: a type not declared, #if, a macro with arguments, long double…), a function is not in the library, a va_list is passed, a class has no @Library
NativeLibraryNotFoundExceptionThe library cannot be loaded: its message lists each file tried and why
NativeLibraryClosedExceptionA function, a variable or a callback of a closed library is used
NativeMemoryExceptionA null pointer or a negative length is given to the memory functions, viewBuffer in Electron
TypeError (from koffi)An argument of the wrong type
RangeErrorA 64-bit integer that is not a safe integer, for a function declared with {int64: 'number'}

A C function called with a wrong prototype, or given a wrong pointer, ends the process: no exception can catch it.

See also ​

  • The guide on the website: FFI.
  • doc/en/FFI.md in the lakutata package (Chinese version in doc/zh): the guide to native libraries.
  • doc/en/NativeAddons.md in the lakutata package: native addons written in C++ or Rust, for what a library of C functions cannot give.
  • koffi: the FFI the package is built on.
  • @lakutata/core (lakutata): components, Application and dependency injection.

Released under the MIT License.