node-ctypes
v0.1.5
Published
Python ctypes-like FFI for Node.js using libffi
Readme
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.
Why node-ctypes?
- ✨ 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.
Features
- 🐍 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
Installation
From npm (recommended)
npm install node-ctypesPrebuilt binaries are available for:
- Windows x64, ARM64
- Linux x64, ARM64
- macOS ARM64
From source
Prerequisites
- Node.js >= 16
- CMake >= 3.15
- C++ compiler (GCC, Clang, or MSVC)
Ubuntu/Debian
sudo apt install build-essential cmakemacOS
brew install cmakeWindows
- Install Visual Studio Build Tools
- Install CMake
Build
npm install
npm run buildQuick Start - Python ctypes Users
If you're familiar with Python ctypes, you'll feel right at home:
Python ctypes:
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 20node-ctypes (identical patterns!):
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 20Usage
Basic FFI - Calling C Functions
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.0Structs - Full Python ctypes Compatibility
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); // 16711680Unions - Shared Memory Regions
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 floatBit Fields - Compact Data Structures
import { Structure, bitfield, c_uint32 } from 'node-ctypes';
// Bit fields for flags and compact data
class Flags extends Structure {
static _fields_ = [
["enabled", bitfield(c_uint32, 1)], // 1 bit
["mode", bitfield(c_uint32, 3)], // 3 bits
["priority", bitfield(c_uint32, 4)], // 4 bits
["reserved", bitfield(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); // 12Arrays - Fixed-size and Dynamic
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]Complex Nested Structures
Real-world example from our test suite:
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); // 128Callbacks - JavaScript Functions in C
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();Variadic Functions - printf, sprintf
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_varfor variadic call preparation - ✅ Calls the function with correct argument marshalling
This matches Python ctypes behavior exactly!
Windows API - Full Support
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}`);
console.log(`${st.wHour}:${st.wMinute}:${st.wSecond}`);
// 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);Memory Operations - Low-level Control
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!"Performance Benchmarks
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 15x faster due to direct object manipulation
- Callback overhead: koffi 1.5x faster at callback creation
Transparent API overhead: Only 3.5% for auto ._buffer extraction!
See tests/benchmarks/ for full benchmark suite.
Supported Types
| 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 |
API Reference
Classes
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.
Detailed API Reference (from lib/index.js)
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 likeaddressand 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;libPathcan benullfor the current executable.CDLL(libPath): common-use wrapper for C calls with cdecl convention; maintains a function cache and provides more convenientfunc().WinDLL(libPath): likeCDLLbut withabi: 'stdcall'by default (useful for WinAPI).
Example:
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:- automatically extracts
._bufferfrom struct objects passed as arguments; - exposes non-enumerable metadata:
funcName,address,_ffi; - provides the
errcheckproperty as getter/setter to intercept return errors.
- automatically extracts
- Python ctypes-like access:
libc.functionNamereturns a wrapper withargtypes/restype/errcheckproperties 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:
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 with helperscreate(),fromBuffer(),deref(),set().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 withcreate(),get(),set(),toObject(),getNestedBuffer().union(fields): defines union; providescreate(),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:
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 2Python-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
The following aliases are exposed (mapped from native.types):
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.
Constants
POINTER_SIZE- pointer size (fromnative.POINTER_SIZE).WCHAR_SIZE- wchar size (fromnative.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.
Functions
load(libPath) → Library
Load a shared library.
callback(fn, returnType, argTypes) → {pointer, release()}
Create a callback from a JavaScript function.
create_string_buffer(init) → Buffer
Create a null-terminated C string buffer (like Python ctypes). init 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.
struct(fields) → StructDefinition
Create a simple struct definition.
Structure (base class)
Base class for Python-like struct definitions. Subclasses should define static _fields_.
Union (base class)
Base class for Python-like union definitions. Subclasses should define static _fields_.
Python ctypes Compatibility Reference
API Comparison
| Feature | Python ctypes | node-ctypes |
|---------|---------------|-------------|
| Load library | CDLL("lib.so") | new CDLL("lib.so") |
| Define function | lib.func.argtypes = [c_int]lib.func.restype = c_int | lib.func("func", c_int, [c_int])orlib.func.argtypes = [c_int]lib.func.restype = c_int |
| Structs | class Point(Structure): _fields_ = [("x", c_int)] | class Point extends Structure { static _fields_ = [["x", c_int]] } |
| Unions | class U(Union): _fields_ = [("i", c_int)] | class U extends Union { static _fields_ = [["i", c_int]] } |
| Arrays | c_int * 5 | array(c_int, 5) |
| Bit fields | ("flags", c_uint, 3) | bitfield(c_uint32, 3) |
| Callbacks | CFUNCTYPE(c_int, c_int) | callback(fn, c_int, [c_int]) |
| Strings | c_char_p(b"hello") | create_string_buffer("hello")orc_char_p(b"hello") |
| Pointers | POINTER(c_int) | c_void_p |
| Variadic | sprintf(buf, b"%d", 42) | sprintf(buf, fmt, 42) (auto) |
| Sizeof | sizeof(c_int) | sizeof(c_int) |
What's Supported
✅ 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:
- Structs use
.toObject()for property access (eager loading for performance) - Callbacks must be manually released with
.release() - Function definition supports both syntaxes:
func(name, returnType, argTypes)orfunc.argtypes = [...]; func.restype = ... - No
POINTER()type - usec_void_p
Limitations & Known Issues
- ⚠️ Callbacks must be released manually with
.release()to prevent memory leaks - ⚠️ No automatic memory management for returned pointers (manual
free()required) - ⚠️ Struct alignment follows platform defaults (not customizable per-field)
- ℹ️ Nested union access uses getter caching (slight behavior difference from Python)
- ℹ️ Struct property access via
.toObject()instead of direct field access (performance optimization)
Examples in Test Suite
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:
cd tests
npm install
npm run test # All tests
npm run bench:koffi # Benchmark vs koffiExamples
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
License
MIT
Credits
Built with:
- libffi - Foreign Function Interface library
- node-addon-api - N-API C++ wrapper
- cmake-js - CMake-based build system for Node.js addons
