Python ctypes for Node.js — A foreign function interface library that mirrors the Python ctypes API, built on libffi and N-API.
If you know Python ctypes, you already know node-ctypes.
npm install node-ctypesPrebuilt binaries for Windows, Linux, and macOS (x64/ARM64).
Build from source
Requires Node.js >= 16, CMake >= 3.15, and a C++ compiler.
npm install
npm run buildPython:
from ctypes import CDLL, c_int
libc = CDLL("libc.so.6")
abs_func = libc.abs
abs_func.argtypes = [c_int]
abs_func.restype = c_int
print(abs_func(-42)) # 42Node.js:
import { CDLL, c_int } from "node-ctypes";
const libc = new CDLL("libc.so.6"); // Linux
// const libc = new CDLL('msvcrt.dll'); // Windows
// const libc = new CDLL('libc.dylib'); // macOS
const abs_func = libc.abs;
abs_func.argtypes = [c_int];
abs_func.restype = c_int;
console.log(abs_func(-42)); // 42A traditional syntax is also available: libc.func("abs", c_int, [c_int]).
Python:
from ctypes import Structure, c_int, c_uint32
class Point(Structure):
_fields_ = [("x", c_int), ("y", c_int)]
p = Point(10, 20)
print(p.x, p.y) # 10 20Node.js:
import { Structure, c_int, c_uint32 } from "node-ctypes";
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 20Nested structs, unions, bit fields, and anonymous fields all work the same way.
import { Union, c_int, c_float } from "node-ctypes";
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 integerimport { array, c_int32 } from "node-ctypes";
const IntArray = array(c_int32, 5);
const arr = IntArray.create([1, 2, 3, 4, 5]);
console.log(arr[0]); // 1import { POINTER, pointer, c_int32 } from "node-ctypes";
const IntPtr = POINTER(c_int32);
const buf = Buffer.alloc(4);
buf.writeInt32LE(42, 0);
const p = IntPtr.fromBuffer(buf);
console.log(p.contents); // 42 (like *p in C)
console.log(p[0]); // 42 (like p[0] in C)
// pointer() function
const x = new c_int32(42);
const px = pointer(x);
console.log(px.contents); // 42
// fromAddress() — typed access to native memory
const pValues = POINTER(MyStruct).fromAddress(nativeAddr);
console.log(pValues[0].field); // pointer arithmetic (like pValues[0] in C)
console.log(pValues[5].field); // pValues + 5 * sizeof(MyStruct)
// cast() to POINTER — Python: cast(c_void_p(addr), POINTER(MyStruct))
const pData = cast(rawAddr, POINTER(MyStruct));
console.log(pData[0].field); // same result as fromAddress()import { callback, c_int32, c_void_p, readValue } from "node-ctypes";
const compare = callback((a, b) => readValue(a, c_int32) - readValue(b, c_int32), c_int32, [c_void_p, c_void_p]);
// Use with qsort, etc.
compare.release(); // Release when doneCFUNCTYPE(restype, ...argtypes) is also supported for Python-compatible function pointer types.
import { CDLL, c_int, c_void_p, c_char_p, string_at } from "node-ctypes";
const libc = new CDLL("libc.so.6"); // Linux
// const libc = new CDLL('msvcrt.dll'); // Windows
// const libc = new CDLL('libc.dylib'); // macOS
const sprintf = libc.func("sprintf", c_int, [c_void_p, c_char_p]);
const buf = Buffer.alloc(256);
sprintf(buf, "Hello %s! %d", "World", 42); // Extra args auto-detected
console.log(string_at(buf)); // "Hello World! 42"import { WinDLL, Structure, c_uint16, c_void, c_void_p } from "node-ctypes";
const kernel32 = new WinDLL("kernel32.dll"); // Uses __stdcall
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],
];
}
const GetLocalTime = kernel32.func("GetLocalTime", c_void, [c_void_p]);
const st = new SYSTEMTIME();
GetLocalTime(st);
console.log(`${st.wYear}-${st.wMonth}-${st.wDay}`);| Feature | Python ctypes | node-ctypes |
|---|---|---|
| Load library | CDLL("lib.so") |
new CDLL("lib.so") |
| Function setup | f.argtypes = [c_int] |
f.argtypes = [c_int] |
| Structs | class P(Structure): |
class P extends Structure |
| Unions | class U(Union): |
class U extends Union |
| Arrays | c_int * 5 |
array(c_int, 5) |
| Bit fields | ("f", c_uint, 3) |
["f", c_uint32, 3] |
| Callbacks | CFUNCTYPE(c_int, c_int) |
CFUNCTYPE(c_int, c_int) |
| Pointers | POINTER(c_int) / pointer(obj) |
POINTER(c_int) / pointer(obj) |
| Pointer arithmetic | p[i], C-style ptr + n |
p[i], p.add(n), p.slice(a, b) |
| Sizeof | sizeof(c_int) |
sizeof(c_int) |
| Alignment | alignment(c_int) |
alignment(c_int) |
| Strings | c_char_p(b"hello") |
create_string_buffer("hello") |
| Variadic | sprintf(buf, b"%d", 42) |
sprintf(buf, "%d", 42) |
| Errno | get_errno() |
get_errno() |
| byref | byref(obj) |
byref(obj) |
| cast | cast(ptr, type) |
cast(ptr, type) (supports POINTER() target) |
| Find library | ctypes.util.find_library("c") |
find_library("c") |
| Lazy load | ctypes.cdll.msvcrt.printf(...) |
cdll.msvcrt.printf(...) |
from_buffer |
S.from_buffer(buf) |
S.from_buffer(buf) |
from_buffer_copy |
S.from_buffer_copy(buf) |
S.from_buffer_copy(buf) |
from_address |
S.from_address(addr) |
S.from_address(addr) |
in_dll |
c_int.in_dll(lib, "errno") |
c_int.in_dll(lib, "errno") |
_as_parameter_ |
Unwrap on call | Unwrap on call |
from_param |
Custom conversion | Custom conversion |
use_last_error |
CDLL(..., use_last_error=True) |
new CDLL(..., { use_last_error: true }) |
use_errno |
CDLL(..., use_errno=True) |
new CDLL(..., { use_errno: true }) |
OleDLL / HRESULT |
OleDLL("ole32") auto-raises |
new OleDLL("ole32.dll") auto-throws |
paramflags |
proto((name, lib), paramflags) |
proto.bind(lib, name, paramflags) |
| Big-endian struct | class X(BigEndianStructure) |
class X extends BigEndianStructure |
| Little-endian struct | class X(LittleEndianStructure) |
class X extends LittleEndianStructure |
| Big-endian union | class X(BigEndianUnion) |
class X extends BigEndianUnion |
| Little-endian union | class X(LittleEndianUnion) |
class X extends LittleEndianUnion |
| Type | Aliases | Size |
|---|---|---|
c_int8 |
c_char |
1 |
c_uint8 |
c_uchar |
1 |
c_int16 |
c_short |
2 |
c_uint16 |
c_ushort |
2 |
c_int32 |
c_int |
4 |
c_uint32 |
c_uint |
4 |
c_int64 |
c_long (64-bit) |
8 |
c_uint64 |
c_ulong (64-bit) |
8 |
c_float |
4 | |
c_double |
8 | |
c_void_p |
pointer | 8 (64-bit) |
c_char_p |
string | pointer |
c_wchar_p |
wide string | pointer |
c_bool |
1 | |
c_size_t |
platform | |
c_ssize_t |
platform |
node-ctypes targets near-complete compatibility with CPython's ctypes.
Highlights below.
import { find_library, cdll, windll, CDLL, OleDLL, HRESULT } from "node-ctypes";
// Cross-platform name resolution
const libc = new CDLL(find_library("c"));
// Lazy namespaces (Python: ctypes.cdll / ctypes.windll). Unlike Python,
// node-ctypes does NOT default restype to c_int — set it explicitly or use
// the one-shot .func() form.
const GetTickCount = windll.kernel32.func("GetTickCount", c_uint32, []);
GetTickCount();
// Attribute-style (requires restype/argtypes to be set):
cdll.msvcrt.printf.argtypes = [c_char_p];
cdll.msvcrt.printf.restype = c_int;
cdll.msvcrt.printf("Hi\n");
// Auto-throw on negative HRESULT (Python: OleDLL)
const ole32 = new OleDLL("ole32.dll");
const CoInit = ole32.func("CoInitializeEx", HRESULT, [c_void_p, c_uint32]);
CoInit(null, 0); // throws on failure, no manual check neededconst user32 = new WinDLL("user32.dll", { use_last_error: true });
const FindWindow = user32.func("FindWindowW", c_void_p, [c_wchar_p, c_wchar_p]);
FindWindow("NoSuchClass", null);
console.log(user32.get_last_error()); // ERROR_CANNOT_FIND_WND_CLASSThe snapshot is taken inside the native bridge immediately after ffi_call,
so other Node.js code can't clobber it before you read it back.
const proto = WINFUNCTYPE(BOOL, HWND, POINTER(DWORD), POINTER(DWORD));
const fn = proto.bind(user32, "GetWindowThreadProcessId", [
{ dir: "in", name: "hWnd" },
{ dir: "out", name: "pid" },
{ dir: "out", name: "tid" },
]);
const [ok, pid, tid] = fn(hwnd); // positional
const [ok2, pid2, tid2] = fn({ hWnd }); // or named// Share memory with an existing Buffer (zero-copy view)
const r = RECT.from_buffer(packet, 16);
// Wrap an externally-allocated pointer
const r2 = RECT.from_address(malloc_result);
// Bind to an exported global variable
const tz = c_int32.in_dll(libc, "_timezone");
console.log(tz.value);class Handle {
constructor(h) {
this._as_parameter_ = h;
}
}
user32.CloseHandle(new Handle(hwnd)); // auto-unwraps
class StrLen extends c_int32 {
static from_param(s) {
return typeof s === "string" ? s.length : s;
}
}
const abs = libc.func("abs", c_int32, [StrLen]);
abs("hello"); // 5class NetHeader extends BigEndianStructure {
static _fields_ = [
["magic", c_uint32],
["length", c_uint32],
];
}
const h = new NetHeader();
h.magic = 0x12345678; // stored as 12 34 56 78 on the wire- Callbacks must be manually released with
.release()to prevent memory leaks - No automatic memory management for returned pointers
- Both
class extends Structure(Python-like) andstruct({...})(functional) syntaxes available - Only type classes (
c_int32) are accepted, not string literals ("int32") — same as Python
Forgetting .release() leaks the libffi trampoline and, if C code retains the pointer,
can cause use-after-free. To catch this in development, set
NODE_CTYPES_DEBUG_CALLBACKS=1:
NODE_CTYPES_DEBUG_CALLBACKS=1 node my-app.jsEvery callback garbage-collected without .release() prints a warning with the creation
stack trace. Zero overhead when the variable is unset (default).
Argument/return types are narrowed when argTypes is passed as a literal tuple
(as const) and returnType is a CType constant:
import { CDLL, c_int, c_char_p, c_int64 } from "node-ctypes";
const libc = new CDLL(null);
const strlen = libc.func("strlen", c_int64, [c_char_p] as const);
const n: bigint = strlen("hello"); // typed as bigint, not anyFor typed struct fields use defineStruct instead of the class-extension syntax:
import { defineStruct, c_int32 } from "node-ctypes";
class Point extends defineStruct({ x: c_int32, y: c_int32 }) {
distance(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
const p = new Point({ x: 3, y: 4 }); // p.x, p.y typed as numbercreate_string_buffer(init)/create_unicode_buffer(init)— create C string buffersstring_at(address, size)/wstring_at(address, size)— read strings from memoryreadValue(ptr, type, offset)/writeValue(ptr, type, value, offset)— direct memory accesssizeof(type)— type size in bytesalignment(type)— type alignment in bytesaddressof(ptr)— get address as BigIntmemmove(dst, src, count)/memset(dst, value, count)— memory operationsGetLastError()/FormatError(code)— Windows error helpers
- Windows Controls Demo — Win32 common controls showcase
- Windows Registry Demo — setValue, getValue, openKey, deleteValue, deleteKey
- Windows Tray Demo — System tray menu
- Windows COM Automation Demo — IShellLinkW / IPersistFile COM interfaces
- Windows LDAP Demo — LDAP directory queries
- Smart Card (PC/SC) Demo — WinSCard on Windows, pcsclite on macOS/Linux
cd tests
npm install
npm run testSee tests/common/ for working examples including parallel Python implementations.
Generate the API docs locally:
npm run docsThis produces a browsable site in docs/ using TypeDoc. To preview it:
npm run docs:serveMIT
Built with libffi, node-addon-api, and cmake-js.