Skip to main content
ArkFFI manages some memory at the TypeScript layer, but most C library memory is owned by the library itself. Understanding ownership for each scenario prevents leaks and dangling pointers.

Overview

ScenarioAllocatorDeallocatorSafe Usage
Primitive args (int, double)Pass by value, no allocation
String args (FFIType.CString)NAPI layerNAPI layer (freed after call)No action needed
String return value (callString)C libraryC libraryRead only, do not free
const char* struct fieldC libraryC libraryfromPtr reads pointer, do not free
Struct arg (StructSchema)User (create)UserFree ArrayBuffer when done
Struct return (StructSchema)ArkFFI (internal ArrayBuffer)User calls releasePtrCall releasePtr after reading
getSymbolPtr / ffi.ptrAddress only, no allocation

Primitive Args

int, double, char, etc. are passed by value via registers or stack — no allocation involved.
lib.symbols.add(2.0, 3.0); // value only, no allocation

String Args

When a parameter type is FFIType.CString, the NAPI bridge calls napi_get_value_string_utf8 to get the C string, allocates a char* on the heap, calls the C function, and immediately delete[] it. No user action needed.
lib.symbols.compute(0, 4.0, 'square');
// ↑ NAPI: allocate char* → call → delete[]

String Return Values

callString returns a const char* owned by the C library. ArkFFI only reads its content via napi_create_string_utf8 — it does not allocate or free. The pointer lifecycle is managed by the C library. Similarly, const char* fields from fromPtr return a pointer value pointing into C library memory. Do not pass it to releasePtr.
let ptr = result.name; // const char*, points to C library memory
let name = new CString(ptr).toString(); // read only
// Do NOT releasePtr(ptr) — not managed by ArkFFI

Struct Args

ArrayBuffer created by StructSchema.create() is allocated by the user. After passing it to a C function, the user owns its lifecycle.
let buf = Complex.create({ real: 1, imag: 2 });
lib.symbols.complex_add(buf, otherBuf);
// buf owned by user — assign null when done to allow GC

Struct Returns (releasePtr)

When dlopen / CFunction has a StructSchema as returns, ArkFFI:
  1. Calls NAPI to obtain an ArrayBuffer
  2. Gets its data pointer via ffi.ptr
  3. Stores the ArrayBuffer in an internal Map to prevent GC
  4. Returns the pointer to the user
After reading with fromPtr, call releasePtr to remove the Map reference and allow GC.
import { releasePtr } from 'arkffi';

let ptr = lib.symbols.complex_add(a, b);   // ArkFFI allocates buffer internally
let result = Complex.fromPtr(ptr);          // read data
releasePtr(ptr);                            // release reference (allow GC)

What if I forget to call releasePtr?

The internal Map holds the ArrayBuffer reference indefinitely. The pointer remains valid. The entry is only removed by releasePtr or program exit. This is generally harmless — entries grow linearly with struct return calls.

What if I pass the wrong pointer to releasePtr?

releasePtr(ptr) does a Map lookup by key. If ptr was not allocated by an ArkFFI struct return (e.g., a C library const char*), the key is not found and the call is silently ignored — no crash.

Pointer Types (ptr, callback)

FFIType.ptr and FFIType.callback pass address values (number) — no allocation. The address is managed by the C library or by JSCallback’s internal slot system.
let applyPtr = ffi.getSymbolPtr(handle, 'apply_callback');
// address only, no allocation

JSCallback.ptr points to an internal slot or trampoline managed by JSCallback.
close() releases the slot.

Core Principle

ArkFFI only manages memory it allocates. C library memory is managed by the C library.
Memory SourceManager
napi_get_value_string_utf8 copyArkFFI (auto free)
Struct return internal ArrayBufferArkFFI (releasePtr to free)
C library const char*C library
User StructSchema.create() resultUser
dlopen internal handleArkFFI (close() calls dlclose)