relaxnative 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,506 @@
1
+ // src/memory/memoryTypes.ts
2
+ var MemoryError = class extends Error {
3
+ name = "MemoryError";
4
+ };
5
+ var UseAfterFreeError = class extends MemoryError {
6
+ name = "UseAfterFreeError";
7
+ };
8
+ var InvalidFreeError = class extends MemoryError {
9
+ name = "InvalidFreeError";
10
+ };
11
+ var NullPointerError = class extends MemoryError {
12
+ name = "NullPointerError";
13
+ };
14
+
15
+ // src/memory/NativeBuffer.ts
16
+ import koffi from "koffi";
17
+ var kBrand = /* @__PURE__ */ Symbol.for("relaxnative.NativeBuffer");
18
+ var finalizer = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((held) => {
19
+ try {
20
+ koffi.free(held.handle);
21
+ } catch {
22
+ }
23
+ }) : null;
24
+ var NativeBuffer = class {
25
+ [kBrand] = true;
26
+ _handle;
27
+ _view;
28
+ _freed = false;
29
+ ownership;
30
+ constructor(allocation, opts) {
31
+ if (!allocation?.handle || !allocation?.view) {
32
+ throw new NullPointerError("NativeBuffer: missing allocation");
33
+ }
34
+ this._handle = allocation.handle;
35
+ this._view = allocation.view;
36
+ this.ownership = opts?.ownership ?? "js";
37
+ if (opts?.autoFree && this.ownership === "js") {
38
+ finalizer?.register(
39
+ this,
40
+ { handle: this._handle, addr: this.address },
41
+ this
42
+ );
43
+ }
44
+ }
45
+ get address() {
46
+ this.assertAlive();
47
+ const raw = koffi.address(this._handle);
48
+ const addr = typeof raw === "bigint" ? Number(raw) : raw;
49
+ if (!Number.isFinite(addr) || addr === 0) {
50
+ throw new NullPointerError(`NativeBuffer has null/invalid address: ${addr}`);
51
+ }
52
+ return addr;
53
+ }
54
+ get size() {
55
+ this.assertAlive();
56
+ return this._view.byteLength;
57
+ }
58
+ get freed() {
59
+ return this._freed;
60
+ }
61
+ assertAlive() {
62
+ if (this._freed) {
63
+ throw new UseAfterFreeError("Use-after-free: NativeBuffer was freed");
64
+ }
65
+ }
66
+ toUint8Array() {
67
+ this.assertAlive();
68
+ return this._view;
69
+ }
70
+ /**
71
+ * Create a bounds-checked DataView into this buffer.
72
+ *
73
+ * Tip: prefer the typed view helpers (u32(), f64(), etc.) when possible.
74
+ */
75
+ dataView(offset = 0, length) {
76
+ this.assertAlive();
77
+ if (!Number.isInteger(offset) || offset < 0) {
78
+ throw new RangeError(`NativeBuffer.dataView: offset must be >= 0, got ${offset}`);
79
+ }
80
+ const size = this._view.byteLength;
81
+ const len = length == null ? size - offset : length;
82
+ if (!Number.isInteger(len) || len < 0) {
83
+ throw new RangeError(`NativeBuffer.dataView: length must be >= 0, got ${len}`);
84
+ }
85
+ if (offset + len > size) {
86
+ throw new RangeError(
87
+ `NativeBuffer.dataView: out of bounds (offset=${offset}, length=${len}, size=${size})`
88
+ );
89
+ }
90
+ return new DataView(this._view.buffer, this._view.byteOffset + offset, len);
91
+ }
92
+ _typed(ctor, offset, length) {
93
+ this.assertAlive();
94
+ if (!Number.isInteger(offset) || offset < 0) {
95
+ throw new RangeError(`NativeBuffer view: offset must be >= 0, got ${offset}`);
96
+ }
97
+ const bpe = ctor.BYTES_PER_ELEMENT;
98
+ if (offset % bpe !== 0) {
99
+ throw new RangeError(`NativeBuffer view: offset must be aligned to ${bpe} bytes`);
100
+ }
101
+ const bytesAvail = this._view.byteLength - offset;
102
+ if (bytesAvail < 0) {
103
+ throw new RangeError("NativeBuffer view: offset out of bounds");
104
+ }
105
+ const maxLen = Math.floor(bytesAvail / bpe);
106
+ const len = length == null ? maxLen : length;
107
+ if (!Number.isInteger(len) || len < 0) {
108
+ throw new RangeError(`NativeBuffer view: length must be >= 0, got ${len}`);
109
+ }
110
+ if (len > maxLen) {
111
+ throw new RangeError(
112
+ `NativeBuffer view: out of bounds (offset=${offset}, length=${len}, elementSize=${bpe}, maxLength=${maxLen})`
113
+ );
114
+ }
115
+ return new ctor(this._view.buffer, this._view.byteOffset + offset, len);
116
+ }
117
+ u8(offset = 0, length) {
118
+ return this._typed(Uint8Array, offset, length);
119
+ }
120
+ i8(offset = 0, length) {
121
+ return this._typed(Int8Array, offset, length);
122
+ }
123
+ u16(offset = 0, length) {
124
+ return this._typed(Uint16Array, offset, length);
125
+ }
126
+ i16(offset = 0, length) {
127
+ return this._typed(Int16Array, offset, length);
128
+ }
129
+ u32(offset = 0, length) {
130
+ return this._typed(Uint32Array, offset, length);
131
+ }
132
+ i32(offset = 0, length) {
133
+ return this._typed(Int32Array, offset, length);
134
+ }
135
+ f32(offset = 0, length) {
136
+ return this._typed(Float32Array, offset, length);
137
+ }
138
+ f64(offset = 0, length) {
139
+ return this._typed(Float64Array, offset, length);
140
+ }
141
+ write(src, offset = 0) {
142
+ this.assertAlive();
143
+ if (!(src instanceof Uint8Array)) {
144
+ throw new TypeError("NativeBuffer.write(src): src must be a Uint8Array");
145
+ }
146
+ if (!Number.isInteger(offset) || offset < 0) {
147
+ throw new RangeError(`NativeBuffer.write: offset must be >= 0, got ${offset}`);
148
+ }
149
+ if (offset + src.byteLength > this._view.byteLength) {
150
+ throw new RangeError(
151
+ `NativeBuffer.write: write out of bounds (offset=${offset}, len=${src.byteLength}, size=${this._view.byteLength})`
152
+ );
153
+ }
154
+ this._view.set(src, offset);
155
+ }
156
+ free() {
157
+ if (this._freed) {
158
+ throw new InvalidFreeError("Double-free: NativeBuffer already freed");
159
+ }
160
+ if (this.ownership !== "js") {
161
+ throw new InvalidFreeError(
162
+ "Invalid free: NativeBuffer is native-owned"
163
+ );
164
+ }
165
+ finalizer?.unregister(this);
166
+ koffi.free(this._handle);
167
+ this._freed = true;
168
+ }
169
+ toJSON() {
170
+ return { address: this.address, size: this.size, ownership: this.ownership };
171
+ }
172
+ static isNativeBuffer(v) {
173
+ return !!v && v[kBrand] === true;
174
+ }
175
+ /**
176
+ * Return the koffi allocation handle.
177
+ *
178
+ * Important: passing raw numeric addresses to koffi is not reliably supported
179
+ * across platforms/Node versions and can segfault. The handle returned by
180
+ * `koffi.alloc()` is the safe thing to pass back into FFI calls.
181
+ */
182
+ toKoffiPointer() {
183
+ this.assertAlive();
184
+ return this._handle;
185
+ }
186
+ };
187
+
188
+ // src/memory/NativePointer.ts
189
+ import koffi2 from "koffi";
190
+ var kBrand2 = /* @__PURE__ */ Symbol.for("relaxnative.NativePointer");
191
+ var NativePointer = class {
192
+ [kBrand2] = true;
193
+ /** Opaque native address (number). */
194
+ address;
195
+ /** Size in bytes when known (0 means unknown). */
196
+ size;
197
+ /** Ownership of the pointee memory. */
198
+ ownership;
199
+ _freed = false;
200
+ constructor(opts) {
201
+ const address = opts.address;
202
+ if (!Number.isFinite(address) || address === 0) {
203
+ throw new NullPointerError(`Null/invalid native pointer address: ${address}`);
204
+ }
205
+ this.address = address;
206
+ this.size = opts.size ?? 0;
207
+ this.ownership = opts.ownership ?? "native";
208
+ }
209
+ get freed() {
210
+ return this._freed;
211
+ }
212
+ /**
213
+ * Marks this pointer as freed (used internally by NativeBuffer).
214
+ * This does *not* free underlying memory.
215
+ */
216
+ _markFreed() {
217
+ this._freed = true;
218
+ }
219
+ assertAlive() {
220
+ if (this._freed) {
221
+ throw new UseAfterFreeError(
222
+ `Use-after-free: pointer 0x${this.address.toString(16)} was freed`
223
+ );
224
+ }
225
+ }
226
+ /**
227
+ * Convert to the koffi pointer value.
228
+ *
229
+ * Note: numeric-address pointers are inherently less safe than passing a
230
+ * koffi External handle (like NativeBuffer does). Use NativePointer for
231
+ * borrowed/opaque pointers returned by native code.
232
+ */
233
+ toKoffiPointer() {
234
+ this.assertAlive();
235
+ return this.address;
236
+ }
237
+ static isNativePointer(v) {
238
+ return !!v && v[kBrand2] === true;
239
+ }
240
+ /**
241
+ * Convenience for koffi type declarations.
242
+ * Example: lib.func('foo', 'int', [NativePointer.koffiType()])
243
+ */
244
+ static koffiType() {
245
+ return koffi2.pointer("void");
246
+ }
247
+ };
248
+
249
+ // src/memory/nativeMemory.ts
250
+ import koffi3 from "koffi";
251
+ function allocRaw(size) {
252
+ if (!Number.isInteger(size) || size <= 0) {
253
+ throw new TypeError(`native.alloc(size): size must be a positive integer, got ${size}`);
254
+ }
255
+ const handle = koffi3.alloc("char", size);
256
+ const ab = koffi3.view(handle, size);
257
+ const view = new Uint8Array(ab);
258
+ return { handle, view };
259
+ }
260
+ function freeRaw(ptr) {
261
+ koffi3.free(ptr);
262
+ }
263
+
264
+ // src/ffi/bindFunctions.ts
265
+ import koffi5 from "koffi";
266
+
267
+ // src/ffi/typeMap.ts
268
+ import koffi4 from "koffi";
269
+ function mapType(type) {
270
+ if (type == null) {
271
+ throw new Error("Unsupported native type: <undefined>");
272
+ }
273
+ const cstringType = koffi4.types.cstring ?? koffi4.cstring ?? // Fallback: some koffi builds don't expose cstring; treat it as char*.
274
+ // This is good enough for reading NUL-terminated strings.
275
+ (koffi4.pointer ? koffi4.pointer("char") : void 0);
276
+ if (/^pointer\s*<.+>$/i.test(type)) {
277
+ const inner = type.replace(/^pointer\s*<\s*/i, "").replace(/\s*>\s*$/i, "").trim();
278
+ if (/^pointer\s*<\s*pointer\s*<.+>\s*>\s*$/i.test(inner)) {
279
+ return koffi4.pointer(koffi4.pointer("void"));
280
+ }
281
+ switch (inner) {
282
+ case "uint8_t":
283
+ return koffi4.pointer(koffi4.types.uint8 ?? koffi4.types.uchar);
284
+ case "int8_t":
285
+ return koffi4.pointer(koffi4.types.int8 ?? koffi4.types.char);
286
+ case "uint16_t":
287
+ return koffi4.pointer(koffi4.types.uint16 ?? koffi4.types.ushort);
288
+ case "int16_t":
289
+ return koffi4.pointer(koffi4.types.int16 ?? koffi4.types.short);
290
+ case "int32_t":
291
+ return koffi4.pointer(koffi4.types.int32 ?? koffi4.types.int);
292
+ case "uint32_t":
293
+ return koffi4.pointer(
294
+ koffi4.types.uint32 ?? koffi4.types.uint
295
+ );
296
+ case "int64_t":
297
+ return koffi4.pointer(koffi4.types.int64 ?? koffi4.types.int64);
298
+ case "uint64_t":
299
+ return koffi4.pointer(koffi4.types.uint64 ?? koffi4.types.ulonglong);
300
+ case "int":
301
+ return koffi4.pointer(koffi4.types.int);
302
+ case "double":
303
+ return koffi4.pointer(koffi4.types.double);
304
+ case "float":
305
+ return koffi4.pointer(koffi4.types.float);
306
+ default:
307
+ return koffi4.pointer("void");
308
+ }
309
+ }
310
+ switch (type) {
311
+ case "int":
312
+ return koffi4.types.int;
313
+ case "uint":
314
+ return koffi4.types.uint;
315
+ case "uint32_t":
316
+ return koffi4.types.uint32 ?? koffi4.types.uint;
317
+ case "int32_t":
318
+ return koffi4.types.int32 ?? koffi4.types.int;
319
+ case "uint8_t":
320
+ return koffi4.types.uint8 ?? koffi4.types.uchar;
321
+ case "int8_t":
322
+ return koffi4.types.int8 ?? koffi4.types.char;
323
+ case "uint16_t":
324
+ return koffi4.types.uint16 ?? koffi4.types.ushort;
325
+ case "int16_t":
326
+ return koffi4.types.int16 ?? koffi4.types.short;
327
+ case "uint64_t":
328
+ return koffi4.types.uint64 ?? koffi4.types.ulonglong;
329
+ case "int64_t":
330
+ return koffi4.types.int64 ?? koffi4.types.int64;
331
+ case "long":
332
+ return koffi4.types.int64;
333
+ case "size_t":
334
+ return koffi4.types.size_t ?? koffi4.types.uint64;
335
+ case "float":
336
+ return koffi4.types.float;
337
+ case "double":
338
+ return koffi4.types.double;
339
+ case "char*":
340
+ return cstringType;
341
+ case "cstring":
342
+ return cstringType;
343
+ case "void":
344
+ return koffi4.types.void;
345
+ case "pointer":
346
+ return koffi4.pointer(koffi4.types.int);
347
+ case "buffer":
348
+ return koffi4.pointer("void");
349
+ default:
350
+ throw new Error(`Unsupported native type: ${type}`);
351
+ }
352
+ }
353
+
354
+ // src/ffi/bindFunctions.ts
355
+ function isSerializedNativeBuffer(v) {
356
+ return !!v && typeof v === "object" && typeof v.address === "number" && typeof v.size === "number" && v.size >= 0;
357
+ }
358
+ function bindFunctions(lib, bindings) {
359
+ const exports = {};
360
+ const functions = Array.isArray(bindings.functions) ? bindings.functions : Object.values(bindings.functions);
361
+ for (const fn of functions) {
362
+ if (!fn?.name || !fn?.returns || !Array.isArray(fn?.args)) {
363
+ throw new Error(
364
+ `Invalid FFI binding entry: ${JSON.stringify(fn)}. Expected {name, returns, args[]}.`
365
+ );
366
+ }
367
+ const returns = mapType(fn.returns);
368
+ const args = fn.args.map(mapType);
369
+ if (!returns) {
370
+ throw new Error(
371
+ `FFI type mapping returned undefined for return type ${JSON.stringify(fn.returns)} (fn=${fn.name})`
372
+ );
373
+ }
374
+ for (let i = 0; i < args.length; i++) {
375
+ if (!args[i]) {
376
+ throw new Error(
377
+ `FFI type mapping returned undefined for arg[${i}] type ${JSON.stringify(fn.args[i])} (fn=${fn.name})`
378
+ );
379
+ }
380
+ }
381
+ if (process.env.RELAXNATIVE_DEBUG_FFI === "1") {
382
+ console.log("[relaxnative ffi] bind", fn.name, { returns: fn.returns, args: fn.args });
383
+ }
384
+ const raw = lib.func(fn.name, returns, args);
385
+ exports[fn.name] = (...args2) => {
386
+ const tmpPointerTables = [];
387
+ const mapped = args2.map((a, i) => {
388
+ const spec = fn.args[i];
389
+ if (typeof spec === "string" && /^pointer\s*<\s*pointer\s*<.+>\s*>\s*$/i.test(spec)) {
390
+ if (!Array.isArray(a)) {
391
+ return a;
392
+ }
393
+ const rowPtrs = a.map((row) => {
394
+ if (NativeBuffer.isNativeBuffer(row)) return row.toKoffiPointer();
395
+ if (NativePointer.isNativePointer(row)) return row.toKoffiPointer();
396
+ if (isSerializedNativeBuffer(row)) {
397
+ const tmp = allocRaw(row.size);
398
+ tmpPointerTables.push(tmp);
399
+ return tmp.handle;
400
+ }
401
+ if (ArrayBuffer.isView(row) || Array.isArray(row)) {
402
+ throw new TypeError(
403
+ `Unsupported row value for ${fn.name} arg[${i}] (pointer-to-pointer): row must be a NativeBuffer or NativePointer. TypedArray/number[] rows don't expose a stable native address for building a T** table. Use native.alloc() per row and pass the row buffers instead.`
404
+ );
405
+ }
406
+ throw new TypeError(
407
+ `Unsupported row value for ${fn.name} arg[${i}] (pointer-to-pointer): expected NativeBuffer|NativePointer`
408
+ );
409
+ });
410
+ const ptrSize = process.arch.includes("64") ? 8 : 4;
411
+ const table = allocRaw(rowPtrs.length * ptrSize);
412
+ const dv = new DataView(table.view.buffer, table.view.byteOffset, table.view.byteLength);
413
+ for (let r = 0; r < rowPtrs.length; r++) {
414
+ const off = r * ptrSize;
415
+ const rawPtr = rowPtrs[r];
416
+ const addrRaw = koffi5.address(rawPtr);
417
+ const addr = typeof addrRaw === "bigint" ? Number(addrRaw) : addrRaw;
418
+ if (!Number.isFinite(addr) || addr === 0) {
419
+ throw new TypeError(
420
+ `Failed to take address of row[${r}] for ${fn.name} arg[${i}] (pointer-to-pointer)`
421
+ );
422
+ }
423
+ if (ptrSize === 8) dv.setBigUint64(off, BigInt(addr), true);
424
+ else dv.setUint32(off, addr >>> 0, true);
425
+ }
426
+ tmpPointerTables.push(table);
427
+ return table.handle;
428
+ }
429
+ if (Array.isArray(a) && typeof spec === "string") {
430
+ const isPtr = /^pointer\s*<.+>$/i.test(spec);
431
+ if (isPtr) {
432
+ const inner = spec.replace(/^pointer\s*<\s*/i, "").replace(/\s*>\s*$/i, "").trim();
433
+ switch (inner) {
434
+ case "int":
435
+ case "int32_t":
436
+ return Int32Array.from(a);
437
+ case "uint32_t":
438
+ return Uint32Array.from(a);
439
+ case "uint8_t":
440
+ return Uint8Array.from(a);
441
+ case "int8_t":
442
+ return Int8Array.from(a);
443
+ case "uint16_t":
444
+ return Uint16Array.from(a);
445
+ case "int16_t":
446
+ return Int16Array.from(a);
447
+ case "float":
448
+ return Float32Array.from(a);
449
+ case "double":
450
+ return Float64Array.from(a);
451
+ default:
452
+ break;
453
+ }
454
+ }
455
+ }
456
+ if (NativeBuffer.isNativeBuffer(a)) return a.toKoffiPointer();
457
+ if (NativePointer.isNativePointer(a)) return a.toKoffiPointer();
458
+ if (ArrayBuffer.isView(a)) return a;
459
+ return a;
460
+ });
461
+ const out = raw(...mapped);
462
+ for (const t of tmpPointerTables) {
463
+ try {
464
+ koffi5.free(t.handle);
465
+ } catch {
466
+ }
467
+ }
468
+ if (fn.returns === "pointer" || /^pointer\s*<.+>$/i.test(fn.returns)) {
469
+ if (out == null) return out;
470
+ const rawAddr = out;
471
+ const addr = typeof rawAddr === "bigint" ? Number(rawAddr) : rawAddr;
472
+ return new NativePointer({ address: addr, ownership: "borrowed" });
473
+ }
474
+ return out;
475
+ };
476
+ }
477
+ return exports;
478
+ }
479
+
480
+ // src/ffi/createLibrary.ts
481
+ import koffi6 from "koffi";
482
+ function loadLibrary(libPath) {
483
+ try {
484
+ return koffi6.load(libPath);
485
+ } catch (err) {
486
+ throw new Error(`Failed to load native library: ${libPath}`);
487
+ }
488
+ }
489
+
490
+ // src/ffi/index.ts
491
+ function loadFfi(libPath, bindings) {
492
+ const lib = loadLibrary(libPath);
493
+ return bindFunctions(lib, bindings);
494
+ }
495
+
496
+ export {
497
+ MemoryError,
498
+ UseAfterFreeError,
499
+ InvalidFreeError,
500
+ NullPointerError,
501
+ NativeBuffer,
502
+ NativePointer,
503
+ allocRaw,
504
+ freeRaw,
505
+ loadFfi
506
+ };