Foreign Function Interface (FFI)

The ffi module provides a foreign function interface for ucode, allowing direct interaction with C libraries. It combines a C declaration parser with libffi-based function calling to enable seamless interop between ucode and C.

The module can be imported using the wildcard import syntax:

import * as ffi from 'ffi';

Synopsis

import * as ffi from 'ffi';

// 1. Declare C types and functions
ffi.cdef(`
    struct point { int x; int y; };
    extern char **environ;
`);

// 2. Call C functions via the global C namespace
// Primitive return values are auto-converted to ucode types
let strcmp = ffi.C.wrap('int strcmp(const char *, const char *)');
print(strcmp("hello", "world"), "\n");  // => non-zero (number)

// 3. String return values remain as cdata - use ffi.string() to convert
let getenv = ffi.C.wrap('char *getenv(char *)');
let path_ptr = getenv('PATH');      // Returns char* cdata
let path_str = ffi.string(path_ptr); // Convert to ucode string

// 4. Create C data instances
ffi.cdef('struct point { int x; int y; };');
let p = ffi.ctype('struct point', 10, 20);
print(p.get('x'), p.get('y'), "\n");  // => 10 20

// 5. Access global variables
print(ffi.C.dlsym('environ').get(0), "\n");

// 6. Query type information
print(ffi.sizeof('int'), "\n");        // => 4
print(ffi.alignof('double'), "\n");    // => 8
print(ffi.offsetof('struct point', 'y'), "\n");  // => 4

// 7. Load external libraries
let libz = ffi.dlopen('z');
let zlibVersion = libz.wrap('const char *zlibVersion(void)');
print(zlibVersion().slice(), "\n");  // => "1.2.11" (or similar)

// Use in callbacks (primitives auto-converted)
let qsort = ffi.C.wrap('void qsort(void *, size_t, size_t, int (*)(const void *, const void *))');
let cmp = ffi.C.wrap('int strcmp(const char *, const char *)');
let arr = ffi.ctype('char *[5]', ["zebra", "apple", "banana", "cherry", "date"]);
// cmp() returns ucode number directly (primitives auto-converted)
qsort(arr.ptr(), arr.length(), arr.itemsize(),
      (a, b) => cmp(a.deref('const char *'), b.deref('const char *')));

Memory Management for char* Return Values

When a wrapped C function returns char*, the return value is a cdata pointer object, not an auto-converted ucode string. This design prevents memory leaks and gives you explicit control over memory management.

Converting char* to ucode Strings

Use ffi.string() or slice() to convert a char* cdata to a ucode string:

let getenv = ffi.C.wrap('char *getenv(char *)');

let path_ptr = getenv('PATH');    // Returns char* cdata
let path = ffi.string(path_ptr);  // Convert to ucode string
// or equivalently:
let path = path_ptr.slice();      // slice() without args = string()

Note: Both ffi.string() and slice() create a copy of the C string. The original C memory remains untouched.

Memory Ownership Patterns

Pattern 1: C Manages Memory (No Free Required)

Functions like getenv(), strerror() return pointers to static/internal memory managed by the C library. Do NOT free these.

let getenv = ffi.C.wrap('char *getenv(char *)');

let path_ptr = getenv('PATH');
let path = ffi.string(path_ptr);  // Copies to ucode string

// path_ptr points to C internal memory - DO NOT free
// path is a ucode string - managed by ucode GC

Pattern 2: Caller Must Free (malloc'd Memory)

Functions like strdup(), asprintf(), getline() return malloc'd memory that you must free to avoid leaks.

let strdup = ffi.C.wrap('char *strdup(const char *)');
let free = ffi.C.wrap('void free(void *)');

let ptr = strdup("hello");      // malloc'd by strdup
let str = ffi.string(ptr);      // Copies to ucode string
free(ptr);                       // NOW you can safely free

// str is safe - it's a ucode string copy
// ptr memory is freed - no leak

Key: Keep the cdata pointer until you're done copying, then free it.

Pattern 3: Stack-Allocated Buffers

When C writes into a buffer you provide (e.g., sprintf), the buffer is managed by ucode.

let sprintf = ffi.C.wrap('int sprintf(char *, const char *, ...)');

let buf = ffi.ctype('char[256]');  // ucode-managed array
sprintf(buf, "Hello %s", "World");

let msg = ffi.string(buf);  // Copies to ucode string

// buf is managed by ucode GC - no manual free needed

Substring Operations with slice()

For char* pointers, slice() supports substring extraction:

let getenv = ffi.C.wrap('char *getenv(char *)');
let ptr = getenv('PATH');

// From start to end (same as ffi.string())
let full = ptr.slice();

// From start index to end
let rest = ptr.slice(5);

// Specific range
let part = ptr.slice(0, 10);

// Negative indices (from end)
let last = ptr.slice(-5);

Common Functions Reference

FunctionMemory OwnerPattern
getenv()C (static)No free needed
strerror()C (static)No free needed
strdup()CallerMust free()
asprintf()CallerMust free()
getline()CallerMust free()
sprintf()Caller (buffer)Buffer managed by you
strtok()C (static)No free needed

Best Practices

  1. Always use ffi.string() or slice() when you need a ucode string from char*
  2. Track ownership: Does C manage the memory or do you?
  3. Free after copying: Call free(ptr) only after ffi.string(ptr) or ptr.slice()
  4. Never free static memory: getenv(), strerror() return static pointers

Limitations

  • No vararg closures: wrap() cannot create closures with variable arguments
  • Fixed ABI: Calling convention determined at closure creation time
  • Platform constraints: Some architectures have limited support for certain type combinations

The ffi.C Namespace

ffi.C is a special CLib instance representing the process's global symbol table. It provides access to standard C library functions without explicit dlopen():

// These are equivalent:
let strlen1 = ffi.C.wrap('size_t strlen(const char *)');

ffi.cdef('size_t strlen(const char *);');
let strlen2 = ffi.C.wrap('strlen');

Functions declared via cdef() are automatically registered in ffi.C's symbol table.

Pointer Arithmetic and Memory Access

C data objects (cdata) provide methods for pointer arithmetic and memory access:

Creating Pointers with ptr()

Use ptr() to get a pointer to a cdata value:

let x = ffi.ctype('int', 42);
let px = x.ptr();  // int* pointer to x

// Pass to C functions expecting pointers
ffi.cdef('int atoi(const char *)');
let num = ffi.ctype('char[4]', "123");
let result = atoi(num.ptr());  // => 123

Array Indexing with get() and set()

Access array elements using get(index) and set(index, value):

let arr = ffi.ctype('int[5]', [10, 20, 30, 40, 50]);

// Read elements
let first = arr.get(0);  // => 10 (ucode number)
let third = arr.get(2);  // => 30 (ucode number)

// Modify elements
arr.set(0, 100);
arr.set(4, 200);

// Negative indices work too
let last = arr.get(-1);  // => 200 (ucode number)

Understanding get() vs index()

get() returns converted ucode values, while index() returns raw cdata references. This is the key distinction between the two methods.

get() - Converted Values

The get() method immediately converts C values to ucode types:

let arr = ffi.ctype('int[5]', [10, 20, 30, 40, 50]);

// Returns ucode number directly
let val1 = arr.get(0);      // => 10 (number)
let val2 = arr.get(2);      // => 30 (number)

// Struct field access - returns converted value
ffi.cdef('struct point { int x; int y; };');
let p = ffi.ctype('struct point', 10, 20);
p.get('x');      // => 10 (number)
p.get('y');      // => 20 (number)

index() - Raw cdata References

The index() method returns a cdata reference for further manipulation:

let arr = ffi.ctype('int[5]', [10, 20, 30, 40, 50]);

// Returns cdata reference (unconverted)
let ref1 = arr.index(0);    // => cdata (int)
let ref2 = arr.index(2);    // => cdata (int)

// Convert to ucode value explicitly
ref1.get();     // => 10 (number)

// Or modify through the reference
arr.index(0).set(100);  // Set arr[0] = 100

Pointer Arithmetic

Both methods work with pointers, but return different types:

let ptr = ffi.ctype('int *', arr.ptr());

// index() returns cdata reference
ptr.index(0);   // => cdata at ptr[0]
ptr.index(1);   // => cdata at ptr[1]
ptr.index(0).get();  // => 10 (number)

// get() returns converted value
ptr.get(0);     // => 10 (number)
ptr.get(1);     // => 20 (number)

Path Syntax Support

Both methods support path notation for nested access:

ffi.cdef('struct rect { struct point min; struct point max; };');
let r = ffi.ctype('struct rect', {
    min: {x: 0, y: 0},
    max: {x: 100, y: 100}
});

// get() returns converted value
r.get('min.x');       // => 0 (number)

// index() returns cdata reference
r.index('min.x');     // => cdata (int)
r.index('min.x').get() // => 0 (number)

Practical Guidance

Use get() when:

  • You need the value immediately as a ucode type
  • Reading values for computation: let x = arr.get(i)
  • Accessing struct fields: let y = struct.get('field')
  • Most common use cases

Use index() when:

  • You need a reference for further manipulation
  • Chaining operations: arr.index(i).set(val)
  • Pointer arithmetic with cdata: ptr.index(n).deref()
  • Passing references to other C functions

For writing values:

  • Use set() for both arrays and structs: arr.set(i, val), struct.set('f', val)

For getting pointers (not values):

  • Use ptr() on scalars: x.ptr() gives you int*
  • Arrays are already pointers: arr can be passed to C functions

Pointer Arithmetic via get() and index()

Both get(n) and index(n) work for pointer arithmetic on pointer types:

ffi.cdef('char *strdup(const char *)');
let strdup = ffi.C.wrap('char *strdup(const char *)');

let ptr = strdup("hello world");

// get() returns converted value (number for char)
let first_char = ptr.get(0);    // 'h' (number 104)
let sixth_char = ptr.get(6);    // 'w' (number 119)

// index() returns cdata reference
ptr.index(6);       // => cdata (char)
ptr.index(6).get()  // => 119 (number)

// Get substring from offset
let substring = ffi.string(ptr.get(6));  // "world"

free(ptr);

Path-Based Access for Nested Structures

Use dot notation and array indexing in paths for complex access:

ffi.cdef(`
    struct point { int x; int y; };
    struct rect { struct point min; struct point max; };
`);

let r = ffi.ctype('struct rect', {
    min: {x: 0, y: 0},
    max: {x: 100, y: 100}
});

// Nested field access
r.get('min.x');    // => 0
r.set('max.y', 50);

// Array of structs
ffi.cdef('struct point points[3];');
let arr = ffi.ctype('struct point[3]', [
    {x: 1, y: 2},
    {x: 3, y: 4},
    {x: 5, y: 6}
]);

arr.get('[1].x');  // => 3
arr.set('[2].y', 10);

Dereferencing Pointers with deref()

Use deref(type) to read the value pointed to:

let x = ffi.ctype('int', 42);
let px = x.ptr();

let value = px.deref('int');  // => 42

// With char* pointers
ffi.cdef('char *strdup(const char *)');
let strdup = ffi.C.wrap('char *strdup(const char *)');

let ptr = strdup("hello");
let first_byte = ptr.deref('char');  // => 'h' (as number 104)

free(ptr);

Querying Array Properties

Use length() and itemsize() for array information:

let arr = ffi.ctype('int[10]');

arr.length();   // => 10 (number of elements)
arr.itemsize(); // => 4 (size of each element in bytes)

// Calculate total size
let total = arr.length() * arr.itemsize();  // => 40 bytes

Working with Byte Arrays

For char[] or uint8_t[], use slice() to extract strings:

let buf = ffi.ctype('char[10]', "hello");

// Extract as ucode string
let str = buf.slice();        // => "hello"
let part = buf.slice(0, 3);   // => "hel"

// Or use ffi.string()
let str2 = ffi.string(buf);   // => "hello"

Complete Example: String Manipulation

ffi.cdef(`
    char *strdup(const char *);
    void free(void *);
    size_t strlen(const char *);
`);

let strdup = ffi.C.wrap('char *strdup(const char *)');
let free = ffi.C.wrap('void free(void *)');
let strlen = ffi.C.wrap('size_t strlen(const char *)');

// Create a duplicatable string
let ptr = strdup("hello world");

// Get length
let len = strlen(ptr).get();  // => 11

// Access individual characters via indexing
let first = ptr.get(0);       // 'h'
let sixth = ptr.get(6);       // 'w'

// Extract substrings
let hello = ptr.slice(0, 5);  // "hello"
let world = ptr.slice(6);     // "world"

// Modify in place
ptr.set(5, 0);  // Null-terminate at space

let str = ffi.string(ptr);  // => "hello"

// Clean up
free(ptr);

Classes

ffi.CData

Represents a C data object holding a value of a C type.

ffi.CLib

Represents a handle to a loaded shared library.

Methods

alignof(type) → {number}

Get the alignment requirement of a C type in bytes.

The alignof() function returns the minimum alignment requirement in bytes for a C type. This is useful for understanding structure padding and memory layout.

Parameters:
NameTypeDescription
typestring

The C type declaration.

Throws: Error

Throws an exception if the type is invalid.

Returns: number

The alignment requirement in bytes (typically a power of 2).

Examples
// Get alignment of primitive types
print(ffi.alignof('int'));     // => 4
print(ffi.alignof('double'));  // => 8
// Get alignment of struct
ffi.cdef('struct foo { char a; int b; };');
print(ffi.alignof('struct foo'));  // => 4 (alignment of int member)

cast(type, value) → {CData}

Cast a value to a different C type.

The cast() function converts a value to a specified C type. It supports casts to numbers, enums, and pointers. The cast is performed without intermediate ucode type conversions.

Parameters:
NameTypeDescription
typestring

The target C type declaration.

value*

The value to cast. Can be a ucode value or cdata.

Throws: Error

Throws an exception if the cast is invalid (e.g., casting to a struct).

Returns: CData

A cdata of the target type holding the cast value.

Examples
// Cast number to pointer
let ptr = ffi.cast('void *', 0x1000);
print(ptr.get());  // => 4096
// Cast between pointer types
ffi.cdef('int x;');
let px = ffi.ctype('int *', ffi.ctype('int', 42).ptr());
let pv = ffi.cast('void *', px);
// Cast pointer to integer
let str = ffi.string("hello");
let addr = ffi.cast('uintptr_t', str.ptr());
print(addr.get());  // => address as number
// Cast integer to enum
ffi.cdef('enum color { RED, GREEN, BLUE };');
let c = ffi.cast('enum color', 2);  // => BLUE

cdef(spec) → {CData}

Declare C types and functions.

The cdef() function parses C declaration strings and registers the types with the FFI system. This is required before using types with ctype(), wrapping functions with wrap(), or resolving symbols with dlsym().

Multiple declarations can be provided in a single call, separated by semicolons. The parser supports most C declaration syntax including:

  • Basic types (int, char, float, double, etc.)

  • Type modifiers (const, volatile, unsigned, signed)

  • Pointers and arrays (int *, char **, int[10])

  • Structs and unions (struct foo { ... }, union bar { ... })

  • Enums (enum baz { ... })

  • Function declarations (int foo(int, char *))

  • Typedefs (typedef ...)

  • Extern declarations (extern int var;)

    // Declare a struct type
    ffi.cdef('struct point { int x; int y; };');
    
    // Declare a function
    ffi.cdef('int strcmp(const char *, const char *);');
    
    // Declare multiple items
    ffi.cdef(`
        typedef unsigned int uint32_t;
        struct sockaddr {
            sa_family_t sa_family;
            char sa_data[14];
        };
        extern char **environ;
    `);
    

After declaring types, you can create instances with ctype(), wrap functions with wrap(), or access global variables with dlsym().

Parameters:
NameTypeDescription
specstring

A C declaration string or multiple declarations separated by semicolons.

Throws: Error

Throws an exception if the declaration syntax is invalid.

Returns: CData

A cdata holding the CTypeID handle for the last declared type.

Examples
// Declare struct and create instance
ffi.cdef('struct point { int x; int y; };');
let p = ffi.ctype('struct point', 10, 20);
print(p.get('x'), p.get('y'));
// Declare function and wrap it
ffi.cdef('size_t strlen(const char *);');
let strlen = ffi.C.wrap('size_t strlen(const char *)');
print(strlen("hello").get());

copy(dest, src, lenopt) → {undefined}

Copy memory between pointers.

The copy() function copies memory from a source pointer to a destination pointer. If the source is a ucode string, it copies the string including its null terminator. Otherwise, an explicit length must be provided.

Parameters:
NameTypeDescription
destCData

Destination pointer.

srcstring | CData

Source string or pointer.

lennumber(optional)

Number of bytes to copy. Required if src is not a string.

Returns: undefined

Returns undefined.

Examples
// Copy string (includes null terminator)
let buf = ffi.ctype('char[10]');
ffi.copy(buf, "hello");
// Copy memory with explicit length
let src = ffi.ctype('char[5]', [1, 2, 3, 4, 5]);
let dst = ffi.ctype('char[5]');
ffi.copy(dst, src, 5);

ctype(type, …initopt) → {CData}nullable

Create a C data instance.

The ctype() function creates a new C data object (cdata) of the specified type. It can be called with optional initializer values that will be used to initialize the object.

Usage patterns:

  1. Without initializer: Creates an uninitialized cdata of the given type. For pointer types, the pointer is set to NULL.

  2. With initializer: Creates and initializes a cdata. The initializer values depend on the type:

    • Scalar types: single value (number, boolean)
    • Structs: positional arguments for each field or a ucode object
    • Arrays: individual element values or a string for char arrays
// Primitive type
let x = ffi.ctype('int', 42);
print(x.get());  // => 42

// Struct type with positional arguments
ffi.cdef('struct point { int x; int y; };');
let p1 = ffi.ctype('struct point', 10, 20);
print(p1.get('x'), p1.get('y'));  // => 10 20

// Struct type with object initializer
let p2 = ffi.ctype('struct point', { x: 30, y: 40 });
print(p2.get('x'), p2.get('y'));  // => 30 40

// Nested struct with object initializer
ffi.cdef('struct rect { struct point tl; struct point br; };');
let r = ffi.ctype('struct rect', {
    tl: { x: 0, y: 0 },
    br: { x: 100, y: 200 }
});
let tl = r.get('tl');
print(tl.get('x'), tl.get('y'));  // => 0 0

// Array type
let arr = ffi.ctype('int[3]', 1, 2, 3);
print(arr.get(0), arr.get(1), arr.get(2));  // => 1 2 3

// Char array from string
let buf = ffi.ctype('char[10]', 'hello');
print(buf.deref());  // => "hello"

// Pointer type (uninitialized)
let ptr = ffi.ctype('void *');
Parameters:
NameTypeDescription
typestring

A C type declaration string. Can be a basic type, struct name, array type, pointer type, etc. The type must have been declared via cdef() first.

init*(optional, repeatable)

Optional initializer values.

Throws: Error

Throws an exception if the type declaration is invalid or wrong number of initializers provided.

Returns: CData

A cdata of the specified type, or null if the type cannot be parsed or has invalid size. For typeof() without initializer, returns a CTypeID handle cdata.

Examples
// Create integer
let x = ffi.ctype('int', 42);
print(x.get());
// Create struct
ffi.cdef('struct point { int x; int y; };');
let p = ffi.ctype('struct point', 10, 20);
print(p.get('x'));
// Create array
let arr = ffi.ctype('double[5]', 1.1, 2.2, 3.3, 4.4, 5.5);
print(arr.length());

dlopen(name, globalopt, cdefsopt) → {CLib}nullable

Load a shared library.

The dlopen() function loads a shared library into the process address space and returns a CLib object that can be used to access symbols via dlsym() or wrap().

// Load zlib compression library
let libz = ffi.dlopen('z');

// Load OpenSSL crypto library
let libcrypto = ffi.dlopen('crypto');

// Load absolute path
let custom = ffi.dlopen('/usr/local/lib/mylib.so');

// Use wrap() to get function pointers
let zlibVersion = libz.wrap('const char *zlibVersion(void)');
print(zlibVersion().slice(), "\n");  // => "1.2.11"

On Unix-like systems, the .so extension is automatically appended if omitted. On macOS, .dylib is used. On Windows, .dll is used.

When the optional third argument is provided, dlopen() will:

  • Parse the C definitions to register types and function prototypes
  • Resolve and wrap all declared functions
  • Attach the wrapped functions as methods on the library object
Parameters:
NameTypeDescription
namestring

The library name or path.

globalboolean(optional, default: false)

If true, make symbols available to subsequently loaded libraries.

cdefsstring(optional)

Optional C declaration string containing types and function prototypes. Function declarations will be automatically wrapped and attached to the library object as methods.

Throws: Error

Throws an exception if the library cannot be loaded or if C definitions cannot be parsed.

Returns: CLib

A CLib object representing the loaded library, or null on error. When cdefs is provided, the returned CLib will have wrapped functions attached as methods.

Examples
// Load zlib and call functions
let libz = ffi.dlopen('z');
let zlibVersion = libz.wrap('const char *zlibVersion(void)');
print(zlibVersion().slice());
// Load OpenSSL crypto library
let libcrypto = ffi.dlopen('crypto');
let OpenSSL_version = libcrypto.wrap('const char *OpenSSL_version(int)');
print(OpenSSL_version(0).slice());  // => "OpenSSL 3.0.0..."
// Load library with automatic wrapping
let libssl = ffi.dlopen('ssl', false, `
    typedef void SSL_METHOD;
    const SSL_METHOD *TLS_method(void);
`);
// TLS_method is now directly callable
let method = libssl.TLS_method();
// Load zlib with pre-wrapped functions
let libz = ffi.dlopen('z', false, `
    const char *zlibVersion(void);
    uLong compressBound(uLong sourceLen);
`);
print(libz.zlibVersion().slice());
print(libz.compressBound(1024));

errno(valueopt) → {number}

Get or set the C errno value.

The errno() function retrieves the current value of the C errno variable, or sets it to a new value if an argument is provided.

Parameters:
NameTypeDescription
valuenumber(optional)

Optional value to set errno to.

Returns: number

The current errno value (before any set operation).

Examples
// Get current errno
let err = ffi.errno();
// Set errno
ffi.errno(0);  // Clear errno

fill(dest, len, valueopt) → {undefined}

Fill memory with a byte value.

The fill() function sets len bytes at the destination pointer to the specified fill value. The fill value can be a number, boolean, or string (first character used).

Parameters:
NameTypeDescription
destCData

Destination pointer.

lennumber

Number of bytes to fill.

valuenumber | boolean | string(optional, default: 0)

Fill value. Numbers/booleans use the value directly; strings use the first character's ASCII code.

Returns: undefined

Returns undefined.

Example
// Zero-fill a buffer
let buf = ffi.ctype('char[10]');
ffi.fill(buf, 10, 0);

// Fill with specific byte
ffi.fill(buf, 10, 0xFF);

// Fill with character
ffi.fill(buf, 10, 'A');  // Fills with 65 (ASCII for 'A')

import(libname, cdefs) → {object|null}

Import a C library with automatic function wrapping.

This is a convenience function that combines library loading, type declaration, and function wrapping into a single call. It loads the specified library, parses the C definitions, and returns an object with all functions pre-wrapped and ready to call.

Parameters:
NameTypeDescription
libnamestring

The library name or path to load. Can be a bare name (e.g., 'z'), a filename (e.g., 'libcrypto.so.3'), or an absolute path.

cdefsstring

A C declaration string containing function prototypes to import. Only function declarations are wrapped; types, structs, and other declarations are registered but not added to the result object.

Throws: Error

Throws an exception if:

  • The library cannot be loaded
  • The C declarations are syntactically invalid
  • A declared function cannot be resolved in the library
Returns: object | null

An object containing wrapped functions keyed by their symbol names. Returns null if the library cannot be loaded or if parsing fails.

Examples
// Import sqlite3 with all functions
let sqlite3 = ffi.import('sqlite3', `
    const char *sqlite3_libversion(void);
    int sqlite3_libversion_number(void);
    int sqlite3_open(const char *, void **);
    int sqlite3_close(void *);
`);

print("Version: ", sqlite3.sqlite3_libversion(), "\n");
// Import zlib functions
let zlib = ffi.import('z', `
    const char *zlibVersion(void);
    uLong compressBound(uLong sourceLen);
`);

print(zlib.zlibVersion());
print(zlib.compressBound(1024));

offsetof(type, field, bitposopt) → {number}nullable

Get the offset of a struct field in bytes.

The offsetof() function returns the byte offset of a field within a struct. For bitfields, the bit position and bit size are returned in an array passed as the third argument.

Parameters:
NameTypeDescription
typestring

The struct type declaration.

fieldstring

The field name to get the offset of.

bitposarray(optional)

Optional array to receive [bit_position, bit_size] for bitfield members.

Throws: Error

Throws an exception if the type is not a struct or the field is invalid.

Returns: number

The byte offset of the field, or null if the field doesn't exist.

Examples
// Get field offset
ffi.cdef('struct point { int x; int y; };');
print(ffi.offsetof('struct point', 'x'));  // => 0
print(ffi.offsetof('struct point', 'y'));  // => 4
// Get bitfield info
ffi.cdef('struct flags { unsigned int a:4; unsigned int b:4; };');
let bitpos = [];
let offset = ffi.offsetof('struct flags', 'b', bitpos);
print(offset, bitpos[0], bitpos[1]);  // => 0 4 4

sizeof(type, nelemopt) → {number}nullable

Get the size of a C type in bytes.

The sizeof() function returns the size in bytes of a C type or cdata expression. For variable-length arrays, an element count can be provided.

Parameters:
NameTypeDescription
typestring | CData

The C type declaration or cdata expression to measure.

nelemnumber(optional)

For variable-length arrays, the number of elements.

Throws: Error

Throws an exception if the type is invalid or nelem is required but missing.

Returns: number

The size in bytes, or null if the size is unknown.

Examples
// Get size of primitive types
print(ffi.sizeof('int'));     // => 4
print(ffi.sizeof('double'));  // => 8
// Get size of struct
ffi.cdef('struct point { int x; int y; };');
print(ffi.sizeof('struct point'));  // => 8
// Get size of VLA with element count
ffi.cdef('int vla[];');
print(ffi.sizeof('int[]', 10));  // => 40 (10 * sizeof(int))

string(arg, lenopt) → {string|CData}

Convert between ucode strings and C char arrays/pointers.

The string() function has two modes:

  1. String to buffer: Given a ucode string, creates a C char[] buffer containing the string plus null terminator. Returns a cdata that can be passed to C functions expecting char*.

  2. Pointer to string: Given a char* cdata pointer, reads the C string and returns a ucode string. An optional length parameter can be provided to limit the maximum bytes read (reads up to len bytes or until null terminator, whichever comes first).

Parameters:
NameTypeDescription
argstring | CData

A ucode string to convert to char[], or a char* cdata pointer to read.

lennumber(optional)

Optional maximum length for reading C strings (reads up to len bytes or until null terminator).

Throws: Error

Throws an exception if the argument type is invalid.

Returns: string | CData

When given a char* pointer: returns a ucode string. When given a ucode string: returns a char[] cdata buffer.

Examples
// Convert ucode string to char[] buffer
let buf = ffi.string("hello");
// buf is now char[6] cdata (including null terminator)
// Can be passed to C functions expecting char*
// Read C string from char* pointer
ffi.cdef('char *getenv(char *);');
let ptr = ffi.C.wrap('char *getenv(char *)')("PATH");
let path = ffi.string(ptr);
print(path);  // => "/usr/bin:..."
// Read fixed-length string (no null terminator)
ffi.cdef('char *strncpy(char *, const char *, size_t);');
let src = ffi.string("hello world");
let dst = ffi.ctype('char[5]');
ffi.C.wrap('char *strncpy(char *, const char *, size_t)')(dst, src, 5);
let short_str = ffi.string(dst, 5);  // => "hello" (no null terminator)

typeof(type) → {CData}

Get the CTypeID for a C type.

The typeof() function returns a CTypeID handle for the specified type. This is useful for storing type references or passing to other FFI functions.

Parameters:
NameTypeDescription
typestring

The C type declaration.

Throws: Error

Throws an exception if the type declaration is invalid.

Returns: CData

A cdata holding the CTypeID handle (an integer type ID).

Examples
// Get type ID for struct
ffi.cdef('struct point { int x; int y; };');
let point_type = ffi.typeof('struct point');
// Get type ID for function pointer
ffi.cdef('int callback(int, char *);');
let cb_type = ffi.typeof('int (*)(int, char *)');