Python ctypes-like FFI for Node.js using libffi
npm install node-ctypes



Python ctypes for Node.js - A high-performance FFI (Foreign Function Interface) library with full Python ctypes compatibility, built on libffi and N-API.
- ⨠Python ctypes API Compatibility - If you know Python ctypes, you already know node-ctypes! Same syntax, same patterns.
- š High Performance - Up to 50x faster than ffi-napi, with struct operations matching or exceeding koffi performance.
- š§ Complete FFI Support - Structs, unions, bit fields, nested structures, arrays, callbacks, variadic functions, and more.
- š Cross-platform - Works seamlessly on Linux, macOS, and Windows with identical API.
- š Full Python ctypes compatibility - Struct definitions, unions, bit fields, anonymous fields
- š Load shared libraries (.so, .dll, .dylib) with CDLL and WinDLL
- š Variadic functions (printf, sprintf) with automatic detection
- š Callbacks - JavaScript functions callable from C code
- šļø Complex data structures - Nested structs, unions in structs, arrays in structs
- ā” High performance - Eager loading for struct properties, optimized FFI wrapper
- š Transparent API - Pass struct objects directly to FFI functions
- š§ Extended type support - All ctypes types (int8, uint8, int16, uint16, etc.)
- š Memory utilities - readValue/writeValue for direct memory access, enhanced sizeof
- šļø Advanced array support - String initialization, improved proxy behavior
``bash`
npm install node-ctypes
Prebuilt binaries are available for:
- Windows x64, ARM64
- Linux x64, ARM64
- macOS x64, ARM64
#### Prerequisites
- Node.js >= 16
- CMake >= 3.15
- C++ compiler (GCC, Clang, or MSVC)
#### Ubuntu/Debian
`bash`
sudo apt install build-essential cmake
#### macOS
`bash`
brew install cmake
#### Windows
- Install Visual Studio Build Tools
- Install CMake
#### Build
`bash`
npm install
npm run build
If you're familiar with Python ctypes, you'll feel right at home:
Python ctypes:
`python
from ctypes import CDLL, c_int, Structure
libc = CDLL("libc.so.6")
abs_func = libc.abs
abs_func.argtypes = [c_int]
abs_func.restype = c_int
print(abs_func(-42)) # 42
class Point(Structure):
_fields_ = [("x", c_int), ("y", c_int)]
p = Point(10, 20)
print(p.x, p.y) # 10 20
`
node-ctypes (identical patterns!):
`javascript
import { CDLL, c_int, Structure } from 'node-ctypes';
// Traditional syntax (always available)
const libc = new CDLL("libc.so.6"); // Linux
// const libc = new CDLL('msvcrt.dll'); // Windows
// const libc = new CDLL('libc.dylib'); // macOS
const abs = libc.func("abs", c_int, [c_int]);
console.log(abs(-42)); // 42
// Python ctypes-like syntax
const abs_func = libc.abs;
abs_func.argtypes = [c_int];
abs_func.restype = c_int;
console.log(abs_func(-42)); // 42
class Point extends Structure {
static _fields_ = [
["x", c_int],
["y", c_int]
];
}
const p = new Point(10, 20);
console.log(p.x, p.y); // 10 20
`
`javascript
import { CDLL, c_int, c_double, c_char_p, c_size_t } from 'node-ctypes';
// Load libc
const libc = new CDLL('libc.so.6'); // Linux
// const libc = new CDLL('msvcrt.dll'); // Windows
// const libc = new CDLL('libc.dylib'); // macOS
// Traditional syntax
const abs = libc.func('abs', c_int, [c_int]);
console.log(abs(-42)); // 42
// Python ctypes-like syntax (equivalent!)
const abs_func = libc.abs;
abs_func.argtypes = [c_int];
abs_func.restype = c_int;
console.log(abs_func(-42)); // 42
// Call strlen() - string length
const strlen = libc.func('strlen', c_size_t, [c_char_p]);
console.log(strlen('Hello')); // 5n (BigInt)
// Load libm for math functions
const libm = new CDLL('libm.so.6'); // Linux
// const libb = new CDLL('ucrtbase.dll'); // Windows
// const libm = new CDLL('libm.dylib'); // macOS
const sqrt = libm.func('sqrt', c_double, [c_double]);
console.log(sqrt(16.0)); // 4.0
`
`javascript
import { Structure, c_int, c_uint32 } from 'node-ctypes';
// Simple struct - Python-like class syntax
class Point extends Structure {
static _fields_ = [
["x", c_int],
["y", c_int]
];
}
// Create and initialize - direct property access!
const p = new Point(10, 20);
console.log(p.x, p.y); // 10 20
// Modify properties directly
p.x = 100;
console.log(p.x); // 100
// Get struct size
console.log(Point.size); // 8
// Nested structs
class Rectangle extends Structure {
static _fields_ = [
["topLeft", Point],
["bottomRight", Point],
["color", c_uint32]
];
}
const rect = new Rectangle({
topLeft: { x: 0, y: 0 },
bottomRight: { x: 100, y: 200 },
color: 0xff0000
});
console.log(rect.topLeft.x); // 0
console.log(rect.bottomRight.x); // 100
console.log(rect.color); // 16711680
`
`javascript
import { Union, c_int, c_float } from 'node-ctypes';
// Union - all fields share the same memory
class IntOrFloat extends Union {
static _fields_ = [
["i", c_int],
["f", c_float]
];
}
const u = new IntOrFloat();
u.f = 3.14159;
console.log(u.i); // Bit pattern of float as integer
u.i = 42;
console.log(u.f); // 42 reinterpreted as float
`
`javascript
import { Structure, c_uint32 } from 'node-ctypes';
// Bit fields using Python-style syntax: [name, type, bits]
class Flags extends Structure {
static _fields_ = [
["enabled", c_uint32, 1], // 1 bit
["mode", c_uint32, 3], // 3 bits
["priority", c_uint32, 4], // 4 bits
["reserved", c_uint32, 24] // 24 bits
];
}
const flags = new Flags();
flags.enabled = 1;
flags.mode = 5;
flags.priority = 12;
console.log(flags.enabled); // 1
console.log(flags.mode); // 5
console.log(flags.priority); // 12
`
Alternative syntax with bitfield() helper:
`javascript
import { Structure, bitfield, c_uint32 } from 'node-ctypes';
class Flags extends Structure {
static _fields_ = [
["enabled", bitfield(c_uint32, 1)],
["mode", bitfield(c_uint32, 3)],
];
}
`
`javascript
import { c_int32, c_uint8, array } from 'node-ctypes';
// Fixed-size array
const IntArray = array(c_int32, 5);
const arr = IntArray.create([1, 2, 3, 4, 5]);
// Array access
console.log(arr[0]); // 1
console.log(arr[4]); // 5
// Iterate
for (const val of arr) {
console.log(val);
}
// Arrays in structs
import { Structure, array, c_uint8 } from 'node-ctypes';
class Packet extends Structure {
static _fields_ = [
["header", array(c_uint8, 8)],
["data", array(c_uint8, 256)]
];
}
const pkt = new Packet({
header: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
data: new Array(256).fill(0)
});
console.log(pkt.header.toString()); // [1, 2, 3, 4, 5, 6, 7, 8]
`
Real-world example from our test suite:
`javascript
import { Structure, Union, c_uint8, c_uint16, c_int32, array } from 'node-ctypes';
// RGB color components
class RGB extends Structure {
static _fields_ = [
["r", c_uint8],
["g", c_uint8],
["b", c_uint8],
["a", c_uint8]
];
}
// Union for color access (as RGB or as 32-bit value)
class Color extends Union {
static _fields_ = [
["rgb", RGB],
["value", c_int32]
];
}
// Pixel with position and color
class Pixel extends Structure {
static _fields_ = [
["x", c_uint16],
["y", c_uint16],
["color", Color]
];
}
// Image with array of pixels
class Image extends Structure {
static _fields_ = [
["width", c_uint16],
["height", c_uint16],
["pixels", array(Pixel, 2)]
];
}
// Create and manipulate
const img = new Image({
width: 640,
height: 480,
pixels: [
{ x: 10, y: 20, color: { rgb: { r: 255, g: 0, b: 0, a: 255 } } },
{ x: 30, y: 40, color: { value: 0xFF00FF00 } }
]
});
console.log(img.pixels[0].color.rgb.r); // 255
console.log(img.pixels[1].color.value); // -16711936 (0xFF00FF00 as signed)
// Union nested in struct - direct property access!
img.pixels[0].color.rgb.g = 128; // Works correctly!
console.log(img.pixels[0].color.rgb.g); // 128
`
`javascript
import { CDLL, callback, c_int32, c_void, c_void_p, c_size_t, readValue, writeValue, create_string_buffer } from 'node-ctypes';
const libc = new CDLL('msvcrt.dll'); // or libc.so.6 on Linux
// Create a comparison callback for qsort
const compare = callback(
(a, b) => {
// a and b are pointers to int32 values
const aVal = readValue(a, c_int32);
const bVal = readValue(b, c_int32);
return aVal - bVal;
},
c_int32, // return type
[c_void_p, c_void_p] // argument types: two pointers
);
// Sort an array using qsort
const qsort = libc.func('qsort', c_void, [
c_void_p, // array pointer
c_size_t, // number of elements
c_size_t, // element size
c_void_p // comparison function
]);
const arr = create_string_buffer(5 * 4);
const values = [5, 2, 8, 1, 9];
values.forEach((v, i) => writeValue(arr, c_int32, v, i * 4));
qsort(arr, 5, 4, compare.pointer);
// Array is now sorted: [1, 2, 5, 8, 9]
console.log(readValue(arr, c_int32, 0)); // 1
console.log(readValue(arr, c_int32, 4)); // 2
// IMPORTANT: Release callback when done
compare.release();
`
`javascript
import { CDLL, create_string_buffer, string_at, c_int, c_void_p, c_char_p } from 'node-ctypes';
const libc = new CDLL('msvcrt.dll'); // Windows
// const libc = new CDLL('libc.so.6'); // Linux
// Define only the fixed parameters - variadic args detected automatically!
const sprintf = libc.func('sprintf', c_int, [c_void_p, c_char_p]);
const buffer = Buffer.alloc(256);
// Pass extra arguments - automatically handled as variadic
sprintf(buffer, 'Hello %s!', 'World');
console.log(string_at(buffer)); // "Hello World!"
sprintf(buffer, 'Number: %d', 42);
console.log(string_at(buffer)); // "Number: 42"
sprintf(buffer, '%s: %d + %d = %d', 'Sum', 10, 20, 30);
console.log(string_at(buffer)); // "Sum: 10 + 20 = 30"
sprintf(buffer, 'Pi ā %.2f', 3.14159);
console.log(string_at(buffer)); // "Pi ā 3.14"
`
Automatic variadic detection - When you pass more arguments than specified, node-ctypes:
- ā
Detects the extra arguments
- ā
Infers their types (string ā char*, number ā int32/double, Buffer ā pointer)
- ā
Uses ffi_prep_cif_var for variadic call preparation
- ā
Calls the function with correct argument marshalling
This matches Python ctypes behavior exactly!
`javascript
import { WinDLL, Structure, c_uint16, c_uint32, c_void_p, c_wchar_p, c_int } from 'node-ctypes';
// WinDLL uses __stdcall convention (default for Windows API)
const kernel32 = new WinDLL('kernel32.dll');
// SYSTEMTIME structure (from tests/windows/test_winapi.js)
class SYSTEMTIME extends Structure {
static _fields_ = [
["wYear", c_uint16],
["wMonth", c_uint16],
["wDayOfWeek", c_uint16],
["wDay", c_uint16],
["wHour", c_uint16],
["wMinute", c_uint16],
["wSecond", c_uint16],
["wMilliseconds", c_uint16]
];
}
// Get local time
const GetLocalTime = kernel32.func('GetLocalTime', c_void, [c_void_p]);
const st = new SYSTEMTIME();
GetLocalTime(st); // Pass struct directly - automatic _buffer extraction!
console.log(${st.wYear}-${st.wMonth}-${st.wDay});${st.wHour}:${st.wMinute}:${st.wSecond}
console.log();
// MessageBox (wide string version)
const user32 = new WinDLL('user32.dll');
const MessageBoxW = user32.func('MessageBoxW', c_int, [
c_void_p, // hWnd
c_wchar_p, // lpText
c_wchar_p, // lpCaption
c_uint32 // uType
]);
// Create UTF-16 buffers for wide strings
const text = Buffer.from('Hello from node-ctypes!\0', 'utf16le');
const caption = Buffer.from('node-ctypes\0', 'utf16le');
MessageBoxW(null, text, caption, 0);
`
`javascript
import { readValue, writeValue, sizeof, create_string_buffer, string_at, c_int, c_double, c_void_p } from 'node-ctypes';
// Allocate memory
const buf = Buffer.alloc(16);
// Write values at specific offsets
writeValue(buf, c_int, 12345, 0);
writeValue(buf, c_double, 3.14159, 8);
// Read values back
console.log(readValue(buf, c_int, 0)); // 12345
console.log(readValue(buf, c_double, 8)); // 3.14159
// Get type sizes
console.log(sizeof(c_int)); // 4
console.log(sizeof(c_double)); // 8
console.log(sizeof(c_void_p)); // 8 (on 64-bit)
// String handling
const str = create_string_buffer('Hello, World!');
console.log(string_at(str)); // "Hello, World!"
`
Benchmarked on Windows with Node.js v24.11.0:
vs koffi (comprehensive 10-benchmark comparison, geometric mean: 3.27x slower):
- Simple int32 function: 1.74x slower
- String parameter: 1.95x slower
- Floating point: 1.83x slower
- No arguments: 2.11x slower
- Multiple arguments: 1.40x faster
- Variadic function: 1.28x slower
- Struct read/write: 14.91x slower
- Buffer allocation: 40.5% overhead
- Raw vs CDLL wrapper: 7.3% overhead
- Callback creation: 1.51x slower
Key Insights:
- koffi excels at simple operations and struct access
- node-ctypes competitive on complex argument handling
- Struct performance gap: koffi ~13x faster due to plain object vs Proxy+N-API
- Workaround: Use toObject() for repeated struct reads (see below)
- Callback overhead: koffi 1.5x faster at callback creation
When reading struct fields repeatedly (e.g., in a tight loop), use toObject() to convert to a plain JavaScript object:
`javascript
const point = Point.create({ x: 10, y: 20 });
// ā Slow: Each access goes through Proxy ā N-API ā buffer read
for (let i = 0; i < 1000000; i++) {
const x = point.x; // ~80ns per access
}
// ā
Fast: Convert once, then use plain object access
const obj = point.toObject(); // { x: 10, y: 20 }
for (let i = 0; i < 1000000; i++) {
const x = obj.x; // ~6ns per access (same as koffi!)
}
`
When to use direct access vs toObject():
- Direct access (point.x): Always synchronized with underlying buffer; required when C code modifies the buffer
- toObject(): Snapshot copy; use for read-only loops or when passing data to JS-only code
Transparent API overhead: Only 3.5% for auto ._buffer extraction!
See tests/benchmarks/ for full benchmark suite.
| Type Name | Aliases | Size |
|-----------|---------|------|
| void | - | 0 |int8
| | c_int8, char | 1 |uint8
| | c_uint8, uchar | 1 |int16
| | c_int16, short | 2 |uint16
| | c_uint16, ushort | 2 |int32
| | c_int32, int, c_int | 4 |uint32
| | c_uint32, uint, c_uint | 4 |int64
| | c_int64, long long | 8 |uint64
| | c_uint64 | 8 |float
| | c_float | 4 |double
| | c_double | 8 |pointer
| | c_void_p, void*, ptr | 8 (64-bit) |string
| | c_char_p, char* | pointer |bool
| | c_bool | 1 |size_t
| | c_size_t | pointer |
#### CDLL(libPath)
Load a shared library using default (cdecl) calling convention.
#### WinDLL(libPath)
Load a shared library using stdcall calling convention (Windows).
#### Library(libPath)
Low-level library wrapper.
---
This section provides a more complete description of the APIs exported from lib/index.js, with quick examples and usage notes.
Native classes and exports
- Version - Version information exposed by the native module.Library
- - Represents a loaded native library and exposes low-level functions for symbols and library management.FFIFunction
- - Low-level object representing an FFI function (has properties like address and internal methods).Callback
- - Builds JS callbacks callable from C (main thread).ThreadSafeCallback
- - Builds thread-safe JS callbacks (can be called from external threads).CType
- , StructType, ArrayType - Types and helpers exposed by the native layer.
Library loading and wrappers
- load(libPath) ā Library : loads a native library; libPath can be null for the current executable.CDLL(libPath)
- : common-use wrapper for C calls with cdecl convention; maintains a function cache and provides more convenient func().WinDLL(libPath)
- : like CDLL but with abi: 'stdcall' by default (useful for WinAPI).
Example:
`js
import { CDLL, c_int32 } from './lib/index.js';
const libc = new CDLL(null);
// Traditional syntax
const abs = libc.func('abs', c_int32, [c_int32]);
console.log(abs(-5));
// Python ctypes-like syntax
const abs_func = libc.abs;
abs_func.argtypes = [c_int32];
abs_func.restype = c_int32;
console.log(abs_func(-5));
`
Detailed CDLL API
- func(name, returnType, argTypes = [], options = {}) ā Function : gets a callable function. The returned function is optimized and:._buffer
- automatically extracts from struct objects passed as arguments;funcName
- exposes non-enumerable metadata: , address, _ffi; errcheck
- provides the property as getter/setter to intercept return errors.libc.functionName
- Python ctypes-like access: returns a wrapper with argtypes/restype/errcheck properties for Python-compatible syntax.symbol(name)
- ā BigInt : address of a symbol.close()
- : closes the library and clears the cache.path
- (getter) : library path.loaded
- (getter) : loading status.
Callback
- callback(fn, returnType, argTypes = []) ā { pointer, release(), _callback } : fast callback, main thread only.threadSafeCallback(fn, returnType, argTypes = [])
- ā { pointer, release(), _callback } : thread-safe callback for external threads.
Note: always call release() when a callback is no longer needed.
Allocation and strings
- Buffer.alloc(size) ā Buffer : allocates native memory.create_string_buffer(init)
- ā Buffer : creates null-terminated C string (init: size|string|Buffer).create_unicode_buffer(init)
- ā Buffer : creates null-terminated wide string (wchar_t).ptrToBuffer(address, size)
- ā Buffer : view on native address (use with caution).addressof(ptr)
- ā BigInt : get the address as BigInt.
Example string creation and passing to function:
`js`
import { create_string_buffer, CDLL, c_int32, c_void_p } from './lib/index.js';
const libc = new CDLL(null);
const puts = libc.func('puts', c_int32, [c_void_p]);
const s = create_string_buffer('hello');
puts(s);
Reading and writing values
- readValue(ptr, type, offset = 0) : supports fast-path for Buffer + basic types (int8, uint8, int16, int32, int64, float, double, bool).writeValue(ptr, type, value, offset = 0)
- : writes values with fast-path for Buffer.
Types and helpers
- sizeof(type) ā number : size in bytes of a type.POINTER(baseType)
- : creates a pointer type factory (see POINTER and pointer() API below).pointer(obj)
- : creates a pointer to an existing ctypes instance (Python-compatible).byref(buffer)
- : passes a buffer by reference (Python ctypes compatibility).cast(ptr, targetType)
- : interprets a pointer as another type (returns wrapper for struct).
Struct / Union / Array / Bitfield
- struct(fields, options) : defines struct with support for nested, bitfields, anonymous fields, packed option. Returns object with create(), get(), set(), toObject(), getNestedBuffer().union(fields)
- : defines union; provides create(), get(), set(), toObject() and returns plain objects with properties.array(elementType, count)
- : defines ArrayType; wrap(buffer) returns Proxy with indexing.bitfield(baseType, bits)
- : bitfield definition.
Struct example:
`js
class Point extends Structure {
static _fields_ = [
["x", c_int32],
["y", c_int32]
];
}
const p = new Point(1, 2);
console.log(p.x, p.y); // 1 2
`
Python-compatible conveniences
- create_string_buffer(init) : create string buffer from number/string/Buffer.create_unicode_buffer(init)
- : create wide string buffer.string_at(address, size)
- / wstring_at(address, size) : read strings from address.
Memory: utilities
- memmove(dst, src, count) : copy memory.memset(dst, value, count)
- : set memory.
Error handling and WinAPI helpers
- get_errno() / set_errno(value) : access to errno (platform-specific implementation)._initWinError()
- internals; public helpers: GetLastError(), SetLastError(code), FormatError(code), WinError(code).
Type aliases (Python-compatible)
The following type classes are exported (identical to Python ctypes):
c_int, c_uint, c_int8, c_uint8, c_int16, c_uint16, c_int32, c_uint32, c_int64, c_uint64, c_float, c_double, c_char, c_char_p, c_wchar, c_wchar_p, c_void_p, c_bool, c_size_t, c_long, c_ulong.
Note: Only type classes (e.g., c_int32) are supported, not string literals (e.g., "int32"). This matches Python ctypes behavior exactly.
Constants
- POINTER_SIZE - pointer size (from native.POINTER_SIZE).WCHAR_SIZE
- - wchar size (from native.WCHAR_SIZE).NULL
- - exported null value.
---
If you want, I can generate additional Windows or Linux-specific snippets, or integrate examples in the tests/ directory.
#### load(libPath) ā Library
Load a shared library.
#### callback(fn, returnType, argTypes) ā {pointer, release()}
Create a callback from a JavaScript function.
#### create_string_buffer(init) ā Bufferinit
Create a null-terminated C string buffer (like Python ctypes). can be a size, a string, or an existing Buffer.
#### create_unicode_buffer(init) ā Buffer
Create a wide (wchar_t) null-terminated buffer (UTF-16LE on Windows).
#### string_at(address, [size]) ā string
Read a C string from an address or buffer.
#### readValue(ptr, type, [offset]) ā value
Read a value from memory.
#### writeValue(ptr, type, value, [offset]) ā bytesWritten
Write a value to memory.
#### sizeof(type) ā number
Get the size of a type in bytes.
#### POINTER and pointer() API
node-ctypes provides a Python ctypes-compatible pointer API:
`javascript
import { POINTER, pointer, c_int32, Structure } from 'node-ctypes';
// Create a pointer type
const IntPtr = POINTER(c_int32);
// Create NULL pointer
const p1 = IntPtr.create();
console.log(p1.isNull); // true
// Create pointer from buffer
const buf = Buffer.alloc(12);
buf.writeInt32LE(10, 0);
buf.writeInt32LE(20, 4);
buf.writeInt32LE(30, 8);
const p2 = IntPtr.fromBuffer(buf);
// .contents property (Python-compatible)
console.log(p2.contents); // 10
p2.contents = 100;
console.log(buf.readInt32LE(0)); // 100
// Pointer indexing (Python-compatible)
console.log(p2[0]); // 100
console.log(p2[1]); // 20
console.log(p2[2]); // 30
p2[1] = 200;
console.log(buf.readInt32LE(4)); // 200
// pointer() function - create pointer to existing object
const x = new c_int32(42);
const px = pointer(x);
console.log(px.contents); // 42
px.contents = 100;
console.log(x.value); // 100
// Works with structures too
class Point extends Structure {
static _fields_ = [["x", c_int32], ["y", c_int32]];
}
const pt = new Point({ x: 10, y: 20 });
const ppt = pointer(pt);
`
POINTER type methods:
- POINTER(baseType) - creates a pointer type factoryPointerType.create()
- - creates a NULL pointer instancePointerType.fromBuffer(buf)
- - creates pointer to bufferPointerType.fromAddress(addr)
- - creates pointer from address
Pointer instance properties:
- .contents - get/set dereferenced value (Python-compatible).address
- - get raw address as BigInt.isNull
- - check if pointer is NULL[n]
- - array-style indexing with pointer arithmetic.deref()
- - alias for .contents getter.set(value)
- - update pointer target
Using POINTER in function definitions:
`javascript
// POINTER types can be used as argtypes and restype
const IntPtr = POINTER(c_int32);
// As argument type
const memset = msvcrt.func("memset", c_void_p, [IntPtr, c_int32, c_size_t]);
// As return type
const memchr = msvcrt.func("memchr", IntPtr, [c_void_p, c_int32, c_size_t]);
`
#### struct(fields) ā StructDefinition
Create a simple struct definition.
#### Structure (base class)static _fields_
Base class for Python-like struct definitions. Subclasses should define .
#### Union (base class)static _fields_
Base class for Python-like union definitions. Subclasses should define .
| Feature | Python ctypes | node-ctypes |
|---------|---------------|-------------|
| Load library | CDLL("lib.so") | new CDLL("lib.so") |lib.func.argtypes = [c_int]
| Define function | lib.func.restype = c_int | lib.func("func", c_int, [c_int])lib.func.argtypes = [c_int]
orlib.func.restype = c_int |class Point(Structure):
| Structs | _fields_ = [("x", c_int)]
| class Point extends Structure{ static _fields_ = [["x", c_int]] }
|class U(Union):
| Unions | _fields_ = [("i", c_int)]
| class U extends Union{ static _fields_ = [["i", c_int]] }
|c_int * 5
| Arrays | | array(c_int, 5) |("flags", c_uint, 3)
| Bit fields | | ["flags", c_uint32, 3]bitfield(c_uint32, 3)
or |CFUNCTYPE(c_int, c_int)
| Callbacks | | callback(fn, c_int, [c_int]) |c_char_p(b"hello")
| Strings | | create_string_buffer("hello")new c_char_p(b"hello")
or |POINTER(c_int)
| Pointers | p.contentsp[0] | POINTER(c_int)p.contentsp[0] |pointer(obj)
| pointer() | | pointer(obj) |sprintf(buf, b"%d", 42)
| Variadic | | sprintf(buf, fmt, 42) (auto) |sizeof(c_int)
| Sizeof | | sizeof(c_int) |
ā
Fully Compatible:
- All basic types (int8-64, uint8-64, float, double, bool, pointer, string)
- Structs with nested structures
- Unions (including nested in structs)
- Bit fields
- Arrays (fixed-size)
- Callbacks (with manual release)
- Variadic functions (automatic detection)
- Anonymous fields in structs/unions
- Class-based struct/union definitions (class MyStruct extends Structure)
- Platform-specific types (c_long, c_ulong, c_size_t)
- Memory operations (alloc, read, write)
- Windows API (__stdcall via WinDLL)
ā ļø Differences from Python ctypes:
- Callbacks must be manually released with .release()func(name, returnType, argTypes)
- Function definition supports both syntaxes: or func.argtypes = [...]; func.restype = ...
⨠100% Python-compatible:
- Struct property access: Direct access (p.x, p.y) works with class X extends Structure - identical to Python!c_int32
- Type system: Only type classes (, c_char_p) are accepted, exactly like Python ctypes"int32"
- No string literals: , "string" are NOT supported (Python doesn't use them either)
- ā ļø Callbacks must be released manually with .release() to prevent memory leaksfree()
- ā ļø No automatic memory management for returned pointers (manual required)class X extends Structure
- ā¹ļø Struct property access:
- Python-style classes (): Direct property access (p.x, p.y) - fully compatible!struct({...})
- Plain struct definitions (): Use .get() / .set() methods for property accesspacked: true
- ā¹ļø Struct alignment: Platform defaults are used, but option is available for packed structs
For complete, working examples, see the test suite:
- Basic types: tests/common/test_basic_types.js
- Structs & unions: tests/common/test_structs.js
- Nested structures: tests/common/test_nested_structs.js
- Arrays: tests/common/test_arrays.js
- Functions: tests/common/test_functions.js
- Callbacks: tests/common/test_callbacks.js
- Version info: tests/common/test_version.js
- Windows API: tests/windows/test_winapi.js
- Python compatibility: tests/common/*.py (parallel Python implementations)
Run tests:
`bash``
cd tests
npm install
npm run test # All tests
npm run bench:koffi # Benchmark vs koffi
For practical GUI application examples using the Windows API:
- Simple GUI Demo: examples/windows/simple.js - Message boxes and basic Windows API GUI elements
- Windows Controls Showcase Demo: examples/windows/windows_controls.js - Comprehensive demo with a wide set of common Win32 controls
MIT
Built with:
- libffi - Foreign Function Interface library
- node-addon-api - N-API C++ wrapper
- cmake-js - CMake-based build system for Node.js addons