ffi-rs

repository·master·Indexed 43 days ago

https://github.com/zhangyuang/node-ffi-rs

A high-performance Node.js module written in Rust and N-API that provides a Foreign Function Interface (FFI) to call functions from C, C++, and Rust directly from JavaScript without C++ glue code. It supports complex data types, C structs, pointers via DataType.External, and asynchronous callbacks using threadsafe_functions. Features include the ability to run FFI tasks in new threads, errno reporting, and in-place Buffer modification.

Tokens
12.3K
Snippets
38
Records
49
Agent score
55%

What's inside ffi-rs

  1. Overview of ffi-rs features and performance

    master

    ffi-rs is a high-performance replacement for the unmaintained node-ffi module. Key features include:

    • High Performance: Significantly faster than ffi-napi (benchmarks show ~318,467 ops/s vs ~2,028 ops/s).
    • Improved Developer Experience: Better type hinting, simpler API interfaces, and support for more data types between Node.js and C.
    • Memory Management: Supports in-place data modification and provides multiple ways to handle pointer types directly.
    • No ref dependency: Unlike older FFI modules, you do not need the ref package to handle pointers.
    • Concurrency: Supports running FFI tasks in new threads.
    • Error Handling: Supports outputting errno information.
  2. How to handle arrays and arrayConstructor

    master

    When a function returns an array, you must use arrayConstructor to specify the type and the exact length. Providing an incorrect length can cause the program to crash.

    For fixed-length arrays within a C struct (e.g., int array[100]), set dynamicArray: false in the arrayConstructor to ensure correct memory alignment and struct size calculation.

    // Returning an array
    const result = load({
        library: 'libsum',
        funcName: 'createArrayi32',
        retType: arrayConstructor({
            type: DataType.I32Array,
            length: 100
        }),
        paramsType: [DataType.I32Array, DataType.I32],
        paramsValue: [myArray, 100],
    });
    
    // Using a static array inside a struct
    const personSchema = {
        staticBytes: arrayConstructor({
            type: DataType.U8Array,
            length: 16,
            dynamicArray: false
        })
    };
  3. Manage pointers with `createPointer`, `unwrapPointer`, and `restorePointer`

    master

    Use DataType.External to pass pointers between Node.js and C.

    • createPointer: Creates a pointer for a specific type (e.g., *const i32).
    • unwrapPointer: Used when you have a pointer to a pointer (e.g., *mut *const char) and need to extract the inner pointer to pass to a function.
    • restorePointer: Converts a pointer back into its original JavaScript value (e.g., converting a char* back to a string).
    • wrapPointer: Used to wrap a pointer returned from a foreign function so it can be processed by restorePointer.
    • freePointer: Manually frees memory associated with a pointer. You must specify the pointerType (e.g., PointerType.RsPointer or PointerType.CPointer).
    const { createPointer, unwrapPointer, restorePointer, freePointer, DataType, PointerType } = require('ffi-rs');
    
    // 1. Create a pointer
    const pointer = createPointer({
        paramsType: [DataType.I32],
        paramsValue: [100],
    });
    
    // 2. Use unwrapPointer for double pointers (pointer to pointer)
    load({
        library: "libsum",
        funcName: "receiveStringPointer",
        paramsType: [DataType.External],
        paramsValue: unwrapPointer(pointer),
        retType: DataType.Void,
    });
    
    // 3. Free the pointer when done
    freePointer({
        paramsType: [DataType.I32],
        paramsValue: pointer,
        pointerType: PointerType.RsPointer
    });
  4. Manage memory and prevent leaks

    master

    Memory management in ffi-rs involves two main areas:

    1. Rust-allocated memory: Parameters allocated on the heap in Rust (like String) are automatically freed by ffi-rs after the call.
    2. C-allocated memory: Return values allocated on the heap in C (like char*) are not automatically freed by default, as some libraries manage their own memory.

    To control this behavior:

    • To prevent ffi-rs from freeing Rust-allocated parameters: Set freeResultMemory: false in the load options.
    • To handle C-allocated return values: If you use DataType.External for paramsType or retType, you must manually release the memory using freePointer when it is no longer needed.
  5. How to modify Buffer data in-place

    master

    The latest versions of ffi-rs support in-place modification of Node.js Buffer objects. If a C function accepts a char* and modifies it, passing a Buffer via DataType.U8Array will allow the changes to persist in the JavaScript Buffer object.

    const arr = Buffer.alloc(200);
    load({
        library: 'libsum',
        funcName: 'modifyData',
        retType: DataType.I32,
        paramsType: [DataType.U8Array],
        paramsValue: [arr]
    });
    console.log(arr); // The buffer content has been updated by the C function
  6. Define C structs and handle static arrays

    master

    To use C structs, you must define the fields in the exact order they are defined in the C source.

    ffi-rs defaults to dynamic arrays for struct fields. If your struct contains a fixed-length (static) array (e.g., uint8_t staticBytes[16]), you must use arrayConstructor and set the ffiTypeTag to FFITypeTag.StackArray to ensure correct size and alignment.

    // Example configuration for a static array within a struct field
    staticBytes: arrayConstructor({
      type: DataType.U8Array,
      length: 16,
      ffiTypeTag: FFITypeTag.StackArray
    }),
  7. How to work with pointers and DataType.External

    master

    To pass or receive raw pointers between Node.js and C, use DataType.External. This wraps the pointer in a way that is compatible with both environments.

    Key functions for pointer manipulation:

    • createPointer: Wraps data into a pointer (e.g., creating a *mut i32 from a number).
    • restorePointer: Recovers data from a pointer returned by a C function.
    • wrapPointer: Creates a multi-level pointer (e.g., turning *mut c_char into *mut *mut c_char).
    • unwrapPointer: Extracts the inner pointer from a multi-level pointer.
    • freePointer: Manually releases memory for pointers using DataType.External (required if freeResultMemory: false was used or when handling External types).
    const { load, DataType, createPointer, unwrapPointer } = require('ffi-rs');
    
    // 1. Getting a pointer from C
    const ptr = load({
        library: 'libsum',
        funcName: 'concatenateStrings',
        retType: DataType.External,
        paramsType: [DataType.String, DataType.String],
        paramsValue: ['a', 'b'],
    });
    
    // 2. Passing a pointer to C
    const string = load({
        library: 'libsum',
        funcName: 'getStringFromPtr',
        retType: DataType.String,
        paramsType: [DataType.External],
        paramsValue: [ptr],
    });
  8. Manage memory for FFI calls

    master

    Memory management in ffi-rs involves two sides:

    1. Rust-allocated parameters: ffi-rs automatically frees memory allocated in the Rust environment (like String).
    2. C-allocated return values: ffi-rs does not automatically free return values allocated on the C side (to avoid breaking libraries that manage their own memory).

    To manage memory correctly:

    • Prevent automatic release: If you want ffi-rs to leave the return value memory alone, set freeResultMemory: false in the load options (this is the default).
    • Manual release: If you use DataType.External for parameters or return types, you must use freePointer to release that memory when it is no longer in use to prevent leaks.
  9. Handle C++ class types using DataType.External

    master

    To interact with C++ classes, wrap the C++ types in C-compatible functions (e.g., a factory function that returns a pointer). In ffi-rs, treat these class pointers as DataType.External.

    When using DataType.External, you are responsible for manual memory management. Use freePointer to release the memory when the pointer is no longer needed.

    // 1. Get a pointer to the C++ instance
    const classPointer = load({
        library: "libsum",
        funcName: "createMyClassFromC",
        retType: DataType.External,
        paramsType: [DataType.String, DataType.I32],
        paramsValue: ["classString", 26],
    });
    
    // 2. Use the pointer in other functions
    load({
        library: "libsum",
        funcName: "printMyClass",
        retType: DataType.External,
        paramsType: [DataType.External],
        paramsValue: [classPointer],
    });
    
    // 3. Manually free the memory
    freePointer({
        paramsType: [DataType.External],
        paramsValue: [classPointer],
        pointerType: PointerType.CPointer
    });
  10. Pass JavaScript functions as callbacks to C

    master

    You can pass JavaScript functions to C functions as callbacks. ffi-rs creates a threadsafe_function from the JS function, meaning the JS function will be called asynchronously and the Node.js process will not automatically exit.

    To manage memory correctly for these callbacks, it is recommended to use createPointer to create the function pointer for manual memory management, and use freePointer to release the function memory when it is no longer needed.

    const testFunction = () => {
        const func = (a, b, c, d, e, f, g) => {
            // ... callback logic ...
            
            // Release function memory when no longer in use
            freePointer({
                paramsType: [
                    funcConstructor({
                        paramsType: [
                            DataType.I32,
                            DataType.Boolean,
                            DataType.String,
                            DataType.Double,
                            arrayConstructor({ type: DataType.StringArray, length: 2 }),
                            arrayConstructor({ type: DataType.I32Array, length: 3 }),
                            personType,
                        ],
                        retType: DataType.Void,
                    })
                ],
                paramsValue: funcExternal
            })
        };
    
        // Recommended: use createPointer for manual memory management
        const funcExternal = createPointer({
            paramsType: [
                funcConstructor({
                    paramsType: [
                        DataType.I32,
                        DataType.Boolean,
                        DataType.String,
                        DataType.Double,
                        arrayConstructor({ type: DataType.StringArray, length: 2 }),
                        arrayConstructor({ type: DataType.I32Array, length: 3 }),
                        personType,
                    ],
                    retType: DataType.Void,
                })
            ],
            paramsValue: [func]
        });
    
        load({
            library: "libsum",
            funcName: "callFunction",
            retType: DataType.Void,
            paramsType: [DataType.External],
            paramsValue: unwrapPointer(funcExternal),
        });
    }