koffi 3.0.0-alpha.9 → 3.0.0-rc.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +26 -4
  2. package/README.md +0 -2
  3. package/cnoke.cjs +44 -44
  4. package/doc/benchmarks.md +17 -65
  5. package/doc/index.md +4 -6
  6. package/index.d.ts +47 -12
  7. package/lib/native/base/base.cc +7 -4
  8. package/package.json +17 -1
  9. package/src/koffi/CMakeLists.txt +4 -10
  10. package/src/koffi/index.cjs +82 -87
  11. package/src/koffi/index.js +70 -75
  12. package/src/koffi/indirect.cjs +82 -87
  13. package/src/koffi/indirect.js +14 -5
  14. package/src/koffi/src/abi/arm64.cc +51 -145
  15. package/src/koffi/src/abi/loong64_asm.S +50 -50
  16. package/src/koffi/src/abi/riscv64.cc +1009 -567
  17. package/src/koffi/src/abi/riscv64_asm.S +50 -50
  18. package/src/koffi/src/abi/x64sysv.cc +98 -102
  19. package/src/koffi/src/abi/x64sysv_asm.S +0 -4
  20. package/src/koffi/src/abi/x64win.cc +56 -77
  21. package/src/koffi/src/abi/x86.cc +54 -94
  22. package/src/koffi/src/abi/x86_asm.S +2 -2
  23. package/src/koffi/src/call.cc +755 -93
  24. package/src/koffi/src/call.hh +26 -1
  25. package/src/koffi/src/ffi.cc +357 -603
  26. package/src/koffi/src/ffi.hh +37 -39
  27. package/src/koffi/src/trampolines.cjs +2 -2
  28. package/src/koffi/src/util.cc +106 -96
  29. package/src/koffi/src/util.hh +75 -76
  30. package/src/koffi/src/uv.def +0 -3
  31. package/src/koffi/src/uv.hh +0 -8
  32. package/build/koffi/darwin_arm64/koffi.node +0 -0
  33. package/build/koffi/darwin_x64/koffi.node +0 -0
  34. package/build/koffi/freebsd_arm64/koffi.node +0 -0
  35. package/build/koffi/freebsd_ia32/koffi.node +0 -0
  36. package/build/koffi/freebsd_x64/koffi.node +0 -0
  37. package/build/koffi/linux_arm64/koffi.node +0 -0
  38. package/build/koffi/linux_ia32/koffi.node +0 -0
  39. package/build/koffi/linux_x64/koffi.node +0 -0
  40. package/build/koffi/musl_arm64/koffi.node +0 -0
  41. package/build/koffi/musl_x64/koffi.node +0 -0
  42. package/build/koffi/openbsd_ia32/koffi.node +0 -0
  43. package/build/koffi/openbsd_x64/koffi.node +0 -0
  44. package/build/koffi/win32_ia32/koffi.exp +0 -0
  45. package/build/koffi/win32_ia32/koffi.lib +0 -0
  46. package/build/koffi/win32_ia32/koffi.node +0 -0
  47. package/build/koffi/win32_x64/koffi.exp +0 -0
  48. package/build/koffi/win32_x64/koffi.lib +0 -0
  49. package/build/koffi/win32_x64/koffi.node +0 -0
  50. package/doc/packaging.md +0 -88
  51. package/src/koffi/src/abi/arm32.cc +0 -1022
  52. package/src/koffi/src/abi/arm32_asm.S +0 -166
package/CHANGELOG.md CHANGED
@@ -7,19 +7,35 @@
7
7
 
8
8
  ### Koffi 3.0
9
9
 
10
- #### Koffi 3.0.0 (WIP)
10
+ #### Koffi 3.0.0
11
11
 
12
- **Main changes:**
12
+ *Released on 2026-05-16*
13
+
14
+ **Highlights:**
15
+
16
+ - Rewrite call preparation and execution for **vastly improved performance**
17
+ - Distribute prebuilt binaries in **separate subpackages**
18
+
19
+ **Breaking changes:**
13
20
 
14
21
  - Replace use of externals with type objects:
15
22
  * Use `koffi.type()` to resolve type specifiers (strings or objects) to type objects
16
- * Access type information directly on type variables without `koffi.introspect()`
23
+ * Access type information directly on type objects without `koffi.introspect()`
17
24
  - Replace use of externals with BigInt pointers
18
- - Rewrite call preparation and execution for vastly improved performance
25
+ - Support ESM and CJS module types
19
26
 
20
27
  **Other changes:**
21
28
 
22
29
  - Add `koffi.enumeration()` to create [enum types](input#enum-types)
30
+ - Add fast decode functions for integers, floats and strings
31
+ - Use proper types for various objects and handles:
32
+ * Use *LibraryHandle* objects for loaded libraries
33
+ * Use *TypeObject* objects for Koffi types
34
+ * Use *PollHandle* for socket poll handles
35
+ - Add `Symbol.dispose` on library objects and poll handles
36
+ - Prefer types to interfaces in TypeScript declaration file
37
+ - Fix various bugs and small leaks (such as library handles)
38
+ - The ARM32 backend has been temporarily removed
23
39
 
24
40
  **Newly deprecated functions:**
25
41
 
@@ -37,6 +53,12 @@ Consult the [migration guide](migration) for more information.
37
53
 
38
54
  ### Koffi 2.16
39
55
 
56
+ #### Koffi 2.16.2
57
+
58
+ *Released on 2026-05-06*
59
+
60
+ - Fix string truncation bugs when passing some kinds of long V8 strings (see [Koromix/koffi#266](https://github.com/Koromix/koffi/issues/266))
61
+
40
62
  #### Koffi 2.16.1
41
63
 
42
64
  *Released on 2026-04-17*
package/README.md CHANGED
@@ -13,13 +13,11 @@ ISA / OS | Windows | Linux/glibc | Linux/musl | macOS | Fre
13
13
  ------------------ | ----------- | ----------- | ----------- | ----------- | ----------- | --------
14
14
  x86 (IA32) [^1] | ✅ Yes | ✅ Yes | 🟨 Probably | ⬜️ *N/A* | ✅ Yes | ✅ Yes
15
15
  x86_64 (AMD64) | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes
16
- ARM32 LE [^2] | ⬜️ *N/A* | ✅ Yes | 🟨 Probably | ⬜️ *N/A* | 🟨 Probably | 🟨 Probably
17
16
  ARM64 (AArch64) LE | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | 🟨 Probably
18
17
  RISC-V 64 [^3] | ⬜️ *N/A* | ✅ Yes | 🟨 Probably | ⬜️ *N/A* | 🟨 Probably | 🟨 Probably
19
18
  LoongArch64 | ⬜️ *N/A* | ✅ Yes | 🟨 Probably | ⬜️ *N/A* | 🟨 Probably | 🟨 Probably
20
19
 
21
20
  [^1]: The following call conventions are supported: cdecl, stdcall, MS fastcall, thiscall.
22
- [^2]: The prebuilt binary uses the hard float ABI and expects a VFP coprocessor. Build from source to use Koffi with a different ABI (softfp, soft).
23
21
  [^3]: The prebuilt binary uses the LP64D (double-precision float) ABI. The LP64 ABI is supported in theory if you build Koffi from source but this is untested. The LP64F ABI is not supported.
24
22
 
25
23
  Go to the web site for more information: https://koffi.dev/
package/cnoke.cjs CHANGED
@@ -23,16 +23,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // ../cnoke/cnoke.js
26
- var import_fs4 = __toESM(require("fs"), 1);
26
+ var import_node_fs4 = __toESM(require("node:fs"), 1);
27
27
 
28
28
  // ../cnoke/src/builder.js
29
- var import_fs3 = __toESM(require("fs"), 1);
30
- var import_os = __toESM(require("os"), 1);
31
- var import_path = __toESM(require("path"), 1);
32
- var import_child_process = require("child_process");
29
+ var import_node_fs3 = __toESM(require("node:fs"), 1);
30
+ var import_node_os = __toESM(require("node:os"), 1);
31
+ var import_node_path = __toESM(require("node:path"), 1);
32
+ var import_node_child_process = require("node:child_process");
33
33
 
34
34
  // ../cnoke/src/abi.js
35
- var import_fs = __toESM(require("fs"), 1);
35
+ var import_node_fs = __toESM(require("node:fs"), 1);
36
36
  function determineAbi() {
37
37
  let abi = process.arch;
38
38
  if (abi == "riscv32" || abi == "riscv64") {
@@ -76,13 +76,13 @@ function determineAbi() {
76
76
  function readFileHeader(filename, read) {
77
77
  let fd = null;
78
78
  try {
79
- let fd2 = import_fs.default.openSync(filename);
79
+ let fd2 = import_node_fs.default.openSync(filename);
80
80
  let buf = Buffer.allocUnsafe(read);
81
- let len = import_fs.default.readSync(fd2, buf);
81
+ let len = import_node_fs.default.readSync(fd2, buf);
82
82
  return buf.subarray(0, len);
83
83
  } finally {
84
84
  if (fd != null)
85
- import_fs.default.closeSync(fd);
85
+ import_node_fs.default.closeSync(fd);
86
86
  }
87
87
  }
88
88
  function decodeElfHeader(buf) {
@@ -122,7 +122,7 @@ function decodeElfHeader(buf) {
122
122
  }
123
123
 
124
124
  // ../cnoke/src/util.js
125
- var import_fs2 = __toESM(require("fs"), 1);
125
+ var import_node_fs2 = __toESM(require("node:fs"), 1);
126
126
  function pathIsAbsolute(path2) {
127
127
  if (process.platform == "win32" && path2.match(/^[a-zA-Z]:/))
128
128
  path2 = path2.substr(2);
@@ -138,28 +138,28 @@ function isPathSeparator(c) {
138
138
  function syncFiles(src_dir, dest_dir) {
139
139
  let keep = /* @__PURE__ */ new Set();
140
140
  {
141
- let entries = import_fs2.default.readdirSync(src_dir, { withFileTypes: true });
141
+ let entries = import_node_fs2.default.readdirSync(src_dir, { withFileTypes: true });
142
142
  for (let entry of entries) {
143
143
  if (!entry.isFile())
144
144
  continue;
145
145
  keep.add(entry.name);
146
- import_fs2.default.copyFileSync(src_dir + `/${entry.name}`, dest_dir + `/${entry.name}`);
146
+ import_node_fs2.default.copyFileSync(src_dir + `/${entry.name}`, dest_dir + `/${entry.name}`);
147
147
  }
148
148
  }
149
149
  {
150
- let entries = import_fs2.default.readdirSync(dest_dir, { withFileTypes: true });
150
+ let entries = import_node_fs2.default.readdirSync(dest_dir, { withFileTypes: true });
151
151
  for (let entry of entries) {
152
152
  if (!entry.isFile())
153
153
  continue;
154
154
  if (keep.has(entry.name))
155
155
  continue;
156
- import_fs2.default.unlinkSync(dest_dir + `/${entry.name}`);
156
+ import_node_fs2.default.unlinkSync(dest_dir + `/${entry.name}`);
157
157
  }
158
158
  }
159
159
  }
160
160
  function unlinkRecursive(path2) {
161
161
  try {
162
- import_fs2.default.rmSync(path2, { recursive: true, maxRetries: process.platform == "win32" ? 3 : 0 });
162
+ import_node_fs2.default.rmSync(path2, { recursive: true, maxRetries: process.platform == "win32" ? 3 : 0 });
163
163
  } catch (err) {
164
164
  if (err.code !== "ENOENT")
165
165
  throw err;
@@ -413,10 +413,10 @@ function Builder(config = {}) {
413
413
  checkCompatibility();
414
414
  console.log(`>> Node: ${runtime_version}`);
415
415
  console.log(`>> Toolchain: ${toolchain ?? "native"}`);
416
- import_fs3.default.mkdirSync(build_dir, { recursive: true, mode: 493 });
417
- import_fs3.default.mkdirSync(work_dir, { recursive: true, mode: 493 });
418
- import_fs3.default.mkdirSync(output_dir, { recursive: true, mode: 493 });
419
- retry &= import_fs3.default.existsSync(work_dir + "/CMakeCache.txt");
416
+ import_node_fs3.default.mkdirSync(build_dir, { recursive: true, mode: 493 });
417
+ import_node_fs3.default.mkdirSync(work_dir, { recursive: true, mode: 493 });
418
+ import_node_fs3.default.mkdirSync(output_dir, { recursive: true, mode: 493 });
419
+ retry &= import_node_fs3.default.existsSync(work_dir + "/CMakeCache.txt");
420
420
  args.push(`-DNODE_JS_EXECPATH=${process.execPath}`);
421
421
  if (options2.api == null) {
422
422
  let downloaded = false;
@@ -427,7 +427,7 @@ function Builder(config = {}) {
427
427
  let api_dir = expandPath(options2.api, project_dir);
428
428
  args.push(`-DNODE_JS_INCLUDE_DIRS=${api_dir}/include`);
429
429
  }
430
- import_fs3.default.writeFileSync(work_dir + "/FindCNoke.cmake", FIND_CNOKE_CMAKE);
430
+ import_node_fs3.default.writeFileSync(work_dir + "/FindCNoke.cmake", FIND_CNOKE_CMAKE);
431
431
  args.push(`-DCMAKE_MODULE_PATH=${work_dir}`);
432
432
  let win32 = (toolchain ?? host).startsWith("win32_");
433
433
  let mingw = process.platform == "win32" && process.env.MSYSTEM != null;
@@ -444,16 +444,16 @@ function Builder(config = {}) {
444
444
  args.push(`-DNODE_JS_LINK_DEF=${api_dir}/def/node_api.def`);
445
445
  }
446
446
  }
447
- import_fs3.default.writeFileSync(work_dir + "/win_delay_hook.c", WIN_DELAY_HOOK_C);
447
+ import_node_fs3.default.writeFileSync(work_dir + "/win_delay_hook.c", WIN_DELAY_HOOK_C);
448
448
  args.push(`-DNODE_JS_SOURCES=${work_dir}/win_delay_hook.c`);
449
449
  }
450
450
  if (process.platform != "win32" || mingw) {
451
- if ((0, import_child_process.spawnSync)("ninja", ["--version"]).status === 0) {
451
+ if ((0, import_node_child_process.spawnSync)("ninja", ["--version"]).status === 0) {
452
452
  args.push("-G", "Ninja");
453
453
  } else if (process.platform == "win32") {
454
454
  args.push("-G", "MinGW Makefiles");
455
455
  }
456
- if (config.ccache && (0, import_child_process.spawnSync)("ccache", ["--version"]).status === 0) {
456
+ if (config.ccache && (0, import_node_child_process.spawnSync)("ccache", ["--version"]).status === 0) {
457
457
  args.push("-DCMAKE_C_COMPILER_LAUNCHER=ccache");
458
458
  args.push("-DCMAKE_CXX_COMPILER_LAUNCHER=ccache");
459
459
  }
@@ -475,7 +475,7 @@ function Builder(config = {}) {
475
475
  args.push(`-D${define}`);
476
476
  args.push("--no-warn-unused-cli");
477
477
  console.log(">> Running configuration");
478
- let proc = (0, import_child_process.spawnSync)(cmake_bin, args, { cwd: work_dir, stdio: "inherit" });
478
+ let proc = (0, import_node_child_process.spawnSync)(cmake_bin, args, { cwd: work_dir, stdio: "inherit" });
479
479
  if (proc.status !== 0) {
480
480
  unlinkRecursive(work_dir);
481
481
  if (retry)
@@ -494,10 +494,10 @@ function Builder(config = {}) {
494
494
  }
495
495
  }
496
496
  checkCMake();
497
- if (!import_fs3.default.existsSync(work_dir + "/CMakeCache.txt"))
497
+ if (!import_node_fs3.default.existsSync(work_dir + "/CMakeCache.txt"))
498
498
  await self.configure();
499
499
  if (process.env.MAKEFLAGS == null)
500
- process.env.MAKEFLAGS = "-j" + (import_os.default.cpus().length || 1);
500
+ process.env.MAKEFLAGS = "-j" + (import_node_os.default.cpus().length || 1);
501
501
  let args = [
502
502
  "--build",
503
503
  work_dir,
@@ -509,14 +509,14 @@ function Builder(config = {}) {
509
509
  for (let target of targets)
510
510
  args.push("--target", target);
511
511
  console.log(">> Running build");
512
- let proc = (0, import_child_process.spawnSync)(cmake_bin, args, { stdio: "inherit" });
512
+ let proc = (0, import_node_child_process.spawnSync)(cmake_bin, args, { stdio: "inherit" });
513
513
  if (proc.status !== 0)
514
514
  throw new Error("Failed to run build step");
515
515
  console.log(">> Copy target files");
516
516
  syncFiles(output_dir, build_dir);
517
517
  };
518
518
  async function checkPrebuild() {
519
- let proc = (0, import_child_process.spawnSync)(process.execPath, ["-e", "require(process.argv[1])", package_dir]);
519
+ let proc = (0, import_node_child_process.spawnSync)(process.execPath, ["-e", "require(process.argv[1])", package_dir]);
520
520
  return proc.status === 0;
521
521
  }
522
522
  this.clean = function() {
@@ -526,9 +526,9 @@ function Builder(config = {}) {
526
526
  if (process.platform == "win32")
527
527
  dirname = dirname.replace(/\\/g, "/");
528
528
  do {
529
- if (import_fs3.default.existsSync(dirname + "/" + basename))
529
+ if (import_node_fs3.default.existsSync(dirname + "/" + basename))
530
530
  return dirname;
531
- dirname = import_path.default.dirname(dirname);
531
+ dirname = import_node_path.default.dirname(dirname);
532
532
  } while (!dirname.endsWith("/"));
533
533
  return null;
534
534
  }
@@ -537,7 +537,7 @@ function Builder(config = {}) {
537
537
  let cache_dir2 = process.env["LOCALAPPDATA"] || process.env["APPDATA"];
538
538
  if (cache_dir2 == null)
539
539
  throw new Error("Missing LOCALAPPDATA and APPDATA environment variable");
540
- cache_dir2 = import_path.default.join(cache_dir2, "cnoke");
540
+ cache_dir2 = import_node_path.default.join(cache_dir2, "cnoke");
541
541
  return cache_dir2;
542
542
  } else {
543
543
  let cache_dir2 = process.env["XDG_CACHE_HOME"];
@@ -545,29 +545,29 @@ function Builder(config = {}) {
545
545
  let home = process.env["HOME"];
546
546
  if (home == null)
547
547
  throw new Error("Missing HOME environment variable");
548
- cache_dir2 = import_path.default.join(home, ".cache");
548
+ cache_dir2 = import_node_path.default.join(home, ".cache");
549
549
  }
550
- cache_dir2 = import_path.default.join(cache_dir2, "cnoke");
550
+ cache_dir2 = import_node_path.default.join(cache_dir2, "cnoke");
551
551
  return cache_dir2;
552
552
  }
553
553
  }
554
554
  function checkCMake() {
555
555
  if (cmake_bin != null)
556
556
  return;
557
- if (!import_fs3.default.existsSync(project_dir + "/CMakeLists.txt"))
557
+ if (!import_node_fs3.default.existsSync(project_dir + "/CMakeLists.txt"))
558
558
  throw new Error("This directory does not appear to have a CMakeLists.txt file");
559
559
  {
560
- let proc = (0, import_child_process.spawnSync)("cmake", ["--version"]);
560
+ let proc = (0, import_node_child_process.spawnSync)("cmake", ["--version"]);
561
561
  if (proc.status === 0) {
562
562
  cmake_bin = "cmake";
563
563
  } else {
564
564
  if (process.platform == "win32") {
565
- let proc2 = (0, import_child_process.spawnSync)("reg", ["query", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Kitware\\CMake", "/v", "InstallDir"]);
565
+ let proc2 = (0, import_node_child_process.spawnSync)("reg", ["query", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Kitware\\CMake", "/v", "InstallDir"]);
566
566
  if (proc2.status === 0) {
567
567
  let matches = proc2.stdout.toString("utf-8").match(/InstallDir[ \t]+REG_[A-Z_]+[ \t]+(.*)+/);
568
568
  if (matches != null) {
569
- let bin = import_path.default.join(matches[1].trim(), "bin\\cmake.exe");
570
- if (import_fs3.default.existsSync(bin))
569
+ let bin = import_node_path.default.join(matches[1].trim(), "bin\\cmake.exe");
570
+ if (import_node_fs3.default.existsSync(bin))
571
571
  cmake_bin = bin;
572
572
  }
573
573
  }
@@ -599,7 +599,7 @@ function Builder(config = {}) {
599
599
  let cnoke = null;
600
600
  if (package_dir != null) {
601
601
  try {
602
- let json = import_fs3.default.readFileSync(package_dir + "/package.json", { encoding: "utf-8" });
602
+ let json = import_node_fs3.default.readFileSync(package_dir + "/package.json", { encoding: "utf-8" });
603
603
  pkg = JSON.parse(json);
604
604
  directory = package_dir;
605
605
  } catch (err) {
@@ -610,7 +610,7 @@ function Builder(config = {}) {
610
610
  if (cnoke == null)
611
611
  cnoke = pkg?.cnoke ?? {};
612
612
  options = {
613
- name: pkg?.name ?? import_path.default.basename(project_dir),
613
+ name: pkg?.name ?? import_node_path.default.basename(project_dir),
614
614
  version: pkg?.version ?? null,
615
615
  directory,
616
616
  ...cnoke
@@ -644,8 +644,8 @@ function Builder(config = {}) {
644
644
  function expandPath(str, root) {
645
645
  let expanded = expandString(str);
646
646
  if (!pathIsAbsolute(expanded))
647
- expanded = import_path.default.join(root, expanded);
648
- expanded = import_path.default.normalize(expanded);
647
+ expanded = import_node_path.default.join(root, expanded);
648
+ expanded = import_node_path.default.normalize(expanded);
649
649
  return expanded;
650
650
  }
651
651
  }
@@ -690,11 +690,11 @@ async function main() {
690
690
  } else if (arg == "-D" || arg == "--directory") {
691
691
  if (value == null)
692
692
  throw new Error(`Missing value for ${arg}`);
693
- config.project_dir = import_fs4.default.realpathSync(value);
693
+ config.project_dir = import_node_fs4.default.realpathSync(value);
694
694
  } else if (arg == "-P" || arg == "--package") {
695
695
  if (value == null)
696
696
  throw new Error(`Missing value for ${arg}`);
697
- config.package_dir = import_fs4.default.realpathSync(value);
697
+ config.package_dir = import_node_fs4.default.realpathSync(value);
698
698
  } else if (arg == "-O" || arg == "--output") {
699
699
  if (value == null)
700
700
  throw new Error(`Missing value for ${arg}`);
package/doc/benchmarks.md CHANGED
@@ -4,12 +4,10 @@ Here is a quick overview of the execution time of Koffi calls on three benchmark
4
4
 
5
5
  - The first benchmark is based on `rand()` calls
6
6
  - The second benchmark is based on `atoi()` calls
7
- - The third benchmark is based on [Raylib](https://www.raylib.com/)
7
+ - The third benchmark is based on `memset()` calls
8
8
 
9
- <p style="text-align: center;">
10
- <a href="{{ ASSET static/perf_linux.png }}" target="_blank"><img src="{{ ASSET static/perf_linux.png }}" alt="Linux x86_64 performance" style="width: 350px;"/></a>
11
- <a href="{{ ASSET static/perf_windows.png }}" target="_blank"><img src="{{ ASSET static/perf_windows.png }}" alt="Windows x86_64 performance" style="width: 350px;"/></a>
12
- </p>
9
+ <div class="benchmark chart" data-platform="linux_x64"></div>
10
+ <div class="benchmark chart" data-platform="win32_x64"></div>
13
11
 
14
12
  These results are detailed and explained below, and compared to node-ffi/node-ffi-napi.
15
13
 
@@ -19,17 +17,9 @@ The results presented below were measured on my x86_64 Linux machine (Intel® Co
19
17
 
20
18
  ## rand results
21
19
 
22
- This test is based around repeated calls to a simple standard C function `rand`, and has three implementations:
20
+ This test is based around repeated calls to a simple standard C function `rand`, which takes no parameter and returns a 32-bit integer.
23
21
 
24
- - the first one is the reference, it calls rand through an N-API module, and is close to the theoretical limit of a perfect (no overhead) Node.js > C FFI implementation (pre-compiled static glue code)
25
- - the second one calls rand through Koffi
26
- - the third one uses the official Node.js FFI implementation, node-ffi-napi
27
-
28
- rand | Iteration time | Relative performance | Overhead
29
- ------------- | -------------- | -------------------- | --------
30
- napi | 256 ns | x1.00 | +0%
31
- koffi | 375 ns | x0.68 | +46%
32
- node-ffi-napi | 29783 ns | x0.009 | +11544%
22
+ <div class="benchmark table" data-platform="linux_x64" data-benchmark="rand"></div>
33
23
 
34
24
  Because rand is a pretty small function, the FFI overhead is clearly visible.
35
25
 
@@ -37,27 +27,13 @@ Because rand is a pretty small function, the FFI overhead is clearly visible.
37
27
 
38
28
  This test is similar to the rand one, but it is based on `atoi`, which takes a string parameter. Javascript (V8) to C string conversion is relatively slow and heavy.
39
29
 
40
- atoi | Iteration time | Relative performance | Overhead
41
- ------------- | -------------- | -------------------- | --------
42
- napi | 371 ns | x1.00 | +0%
43
- koffi | 557 ns | x0.67 | +50%
44
- node-ffi-napi | 104340 ns | x0.004 | +27988%
45
-
46
- Because atoi is a pretty small function, the FFI overhead is clearly visible.
47
-
48
- ## Raylib results
30
+ <div class="benchmark table" data-platform="linux_x64" data-benchmark="atoi"></div>
49
31
 
50
- This benchmark uses the CPU-based image drawing functions in Raylib. The calls are much heavier than in previous benchmarks, thus the FFI overhead is reduced. In this implementation, Koffi is compared to:
32
+ ## memset results
51
33
 
52
- - Baseline: Full C++ version of the code (no JS)
53
- - [node-raylib](https://github.com/RobLoach/node-raylib): This is a native wrapper implemented with N-API
34
+ This test is based around repeated calls to the standard C function `memset`. All implementations pass a Node.js Buffer for the pointer argument.
54
35
 
55
- raylib | Iteration time | Relative performance | Overhead
56
- ------------- | -------------- | -------------------- | --------
57
- C++ | 10.8 µs | x1.14 | -12%
58
- node-raylib | 12.3 µs | x1.00 | +0%
59
- koffi | 13.2 µs | x0.92 | +8%
60
- node-ffi-napi | 80.3 µs | x0.15 | +555%
36
+ <div class="benchmark table" data-platform="linux_x64" data-benchmark="memset"></div>
61
37
 
62
38
  # Windows x86_64
63
39
 
@@ -65,47 +41,21 @@ The results presented below were measured on my x86_64 Windows machine (Intel®
65
41
 
66
42
  ## rand results
67
43
 
68
- This test is based around repeated calls to a simple standard C function `rand`, and has three implementations:
69
-
70
- - the first one is the reference, it calls rand through an N-API module, and is close to the theoretical limit of a perfect (no overhead) Node.js > C FFI implementation (pre-compiled static glue code)
71
- - the second one calls rand through Koffi
72
- - the third one uses the official Node.js FFI implementation, node-ffi-napi
73
-
74
- rand | Iteration time | Relative performance | Overhead
75
- ------------- | -------------- | -------------------- | --------
76
- napi | 859 ns | x1.00 | (ref)
77
- koffi | 1352 ns | x0.64 | +57%
78
- node-ffi-napi | 35640 ns | x0.02 | +4048%
44
+ This test is based around repeated calls to a simple standard C function `rand`, which takes no parameter and returns a 32-bit integer.
79
45
 
80
- Because rand is a pretty small function, the FFI overhead is clearly visible.
46
+ <div class="benchmark table" data-platform="win32_x64" data-benchmark="rand"></div>
81
47
 
82
48
  ## atoi results
83
49
 
84
50
  This test is similar to the rand one, but it is based on `atoi`, which takes a string parameter. Javascript (V8) to C string conversion is relatively slow and heavy.
85
51
 
86
- The results below were measured on my x86_64 Windows machine (Intel® Core™ i5-4460):
87
-
88
- atoi | Iteration time | Relative performance | Overhead
89
- ------------- | -------------- | -------------------- | --------
90
- napi | 1336 ns | x1.00 | (ref)
91
- koffi | 2440 ns | x0.55 | +83%
92
- node-ffi-napi | 136890 ns | x0.010 | +10144%
93
-
94
- Because atoi is a pretty small function, the FFI overhead is clearly visible.
52
+ <div class="benchmark table" data-platform="win32_x64" data-benchmark="atoi"></div>
95
53
 
96
- ## Raylib results
54
+ ## memset results
97
55
 
98
- This benchmark uses the CPU-based image drawing functions in Raylib. The calls are much heavier than in the atoi benchmark, thus the FFI overhead is reduced. In this implementation, Koffi is compared to:
56
+ This test is based around repeated calls to the standard C function `memset`. All implementations pass a Node.js Buffer for the pointer argument.
99
57
 
100
- - [node-raylib](https://github.com/RobLoach/node-raylib) (baseline): This is a native wrapper implemented with N-API
101
- - raylib_cc: C++ implementation of the benchmark, without any Javascript
102
-
103
- raylib | Iteration time | Relative performance | Overhead
104
- ------------- | -------------- | -------------------- | --------
105
- C++ | 18.2 µs | x1.50 | -33%
106
- node-raylib | 27.3 µs | x1.00 | (ref)
107
- koffi | 29.8 µs | x0.92 | +9%
108
- node-ffi-napi | 96.3 µs | x0.28 | +253%
58
+ <div class="benchmark table" data-platform="win32_x64" data-benchmark="memset"></div>
109
59
 
110
60
  # Running benchmarks
111
61
 
@@ -124,3 +74,5 @@ Once everything is built and ready, run:
124
74
  ```sh
125
75
  node benchmark.js
126
76
  ```
77
+
78
+ <script src="{{ ASSET static/benchmarks.js }}"></script>
package/doc/index.md CHANGED
@@ -21,18 +21,16 @@ The following combinations of OS and architectures __are officially supported an
21
21
 
22
22
  ISA / OS | Windows | Linux/glibc | Linux/musl | macOS | FreeBSD | OpenBSD
23
23
  ------------------ | ------- | ----------- | ---------- | ----- | ------- | -------
24
- x86 (IA32) [^2] | ✅ | ✅ | 🟨 | ⬜️ | ✅ | ✅
24
+ x86 (IA32) [^1] | ✅ | ✅ | 🟨 | ⬜️ | ✅ | ✅
25
25
  x86_64 (AMD64) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅
26
- ARM32 LE [^3] | ⬜️ | ✅ | 🟨 | ⬜️ | 🟨 | 🟨
27
26
  ARM64 (AArch64) LE | ✅ | ✅ | ✅ | ✅ | ✅ | 🟨
28
- RISC-V 64 [^4] | ⬜️ | ✅ | 🟨 | ⬜️ | 🟨 | 🟨
27
+ RISC-V 64 [^2] | ⬜️ | ✅ | 🟨 | ⬜️ | 🟨 | 🟨
29
28
  LoongArch64 | ⬜️ | ✅ | 🟨 | ⬜️ | 🟨 | 🟨
30
29
 
31
30
  <div class="legend">✅ Yes | 🟨 Probably | ⬜️ Not applicable</div>
32
31
 
33
- [^2]: The following call conventions are supported for forward calls: cdecl, stdcall, MS fastcall, thiscall. Only cdecl and stdcall can be used for C to JS callbacks.
34
- [^3]: The prebuilt binary uses the hard float ABI and expects a VFP coprocessor. Build from source to use Koffi with a different ABI (softfp, soft).
35
- [^4]: The prebuilt binary uses the LP64D (double-precision float) ABI. The LP64 ABI is supported in theory if you build Koffi from source (untested), the LP64F ABI is not supported.
32
+ [^1]: The following call conventions are supported for forward calls: cdecl, stdcall, MS fastcall, thiscall. Only cdecl and stdcall can be used for C to JS callbacks.
33
+ [^2]: The prebuilt binary uses the LP64D (double-precision float) ABI. The LP64 ABI is supported in theory if you build Koffi from source (untested), the LP64F ABI is not supported.
36
34
 
37
35
  For all fully supported platforms (green check marks), a prebuilt binary is included in the NPM package which means you can install Koffi without a C++ compiler.
38
36
 
package/index.d.ts CHANGED
@@ -43,8 +43,6 @@ export type KoffiFunc<T extends (...args: any) => any> = T & {
43
43
  info: PrototypeInfo;
44
44
  };
45
45
 
46
- export type CallbackHandle = {};
47
-
48
46
  type LoadOptions = {
49
47
  lazy?: boolean,
50
48
  global?: boolean,
@@ -68,6 +66,8 @@ export type LibraryHandle = {
68
66
  symbol(name: string, type: TypeSpec): any;
69
67
 
70
68
  unload(): void;
69
+
70
+ [Symbol.dispose](): void;
71
71
  };
72
72
 
73
73
  export function load(path: string | null, options?: LoadOptions): LibraryHandle;
@@ -116,23 +116,54 @@ export function proto(convention: string, result: TypeSpec, arguments: TypeSpec[
116
116
  export function proto(name: string | null | undefined, result: TypeSpec, arguments: TypeSpec[]): TypeObject;
117
117
  export function proto(convention: string, name: string | null | undefined, result: TypeSpec, arguments: TypeSpec[]): TypeObject;
118
118
 
119
- export function register(callback: Function, type: TypeSpec): CallbackHandle;
120
- export function register(thisValue: any, callback: Function, type: TypeSpec): CallbackHandle;
121
- export function unregister(callback: CallbackHandle): void;
119
+ export function register(callback: Function, type: TypeSpec): bigint;
120
+ /** @deprecated */ export function register(thisValue: any, callback: Function, type: TypeSpec): bigint;
121
+ export function unregister(callback: bigint): void;
122
122
 
123
123
  export function as(value: any, type: TypeSpec): IKoffiPointerCast;
124
- export function decode(value: any, type: TypeSpec): any;
125
- export function decode(value: any, type: TypeSpec, len: number): any;
126
- export function decode(value: any, offset: number, type: TypeSpec): any;
127
- export function decode(value: any, offset: number, type: TypeSpec, len: number): any;
128
124
  export function address(value: any): bigint;
129
125
  export function call(value: any, type: TypeSpec, ...args: any[]): any;
126
+ export function view(ref: any, len: number): ArrayBuffer;
127
+
128
+ export const decode: {
129
+ (value: any, type: TypeSpec): any;
130
+ (value: any, type: TypeSpec, len: number): any;
131
+ (value: any, offset: number, type: TypeSpec): any;
132
+ (value: any, offset: number, type: TypeSpec, len: number): any;
133
+
134
+ char(ptr: any): number;
135
+ short(ptr: any): number;
136
+ int(ptr: any): number;
137
+ long(ptr: any): number | bigint;
138
+ longlong(ptr: any): number | bigint;
139
+ uchar(ptr: any): number;
140
+ ushort(ptr: any): number;
141
+ uint(ptr: any): number;
142
+ ulong(ptr: any): number | bigint;
143
+ ulonglong(ptr: any): number | bigint;
144
+
145
+ int8(ptr: any): number;
146
+ int16(ptr: any): number;
147
+ int32(ptr: any): number;
148
+ int64(ptr: any): number | bigint;
149
+ uint8(ptr: any): number;
150
+ uint16(ptr: any): number;
151
+ uint32(ptr: any): number;
152
+ uint64(ptr: any): number | bigint;
153
+
154
+ float(ptr: any): number;
155
+ double(ptr: any): number;
156
+
157
+ string(ptr: any, length?: number | bigint | null): string;
158
+ string16(ptr: any, length?: number | bigint | null): string;
159
+ string32(ptr: any, length?: number | bigint | null): string;
160
+ };
161
+
130
162
  export function encode(ref: any, type: TypeSpec, value: any): void;
131
163
  export function encode(ref: any, type: TypeSpec, value: any, len: number): void;
132
164
  export function encode(ref: any, offset: number, type: TypeSpec): void;
133
165
  export function encode(ref: any, offset: number, type: TypeSpec, value: any): void;
134
166
  export function encode(ref: any, offset: number, type: TypeSpec, value: any, len: number): void;
135
- export function view(ref: any, len: number): ArrayBuffer;
136
167
 
137
168
  export function type(type: TypeSpec): TypeObject;
138
169
  export function sizeof(type: TypeSpec): number;
@@ -261,13 +292,13 @@ export const types: Record<PrimitiveTypes, TypeObject>;
261
292
  export namespace node {
262
293
  export const env: { __brand: 'IKoffiNodeEnv' };
263
294
 
264
- export type PollOptions = {
295
+ type PollOptions = {
265
296
  readable?: boolean;
266
297
  writable?: boolean;
267
298
  disconnect?: boolean;
268
299
  };
269
300
 
270
- export type PollEvents = {
301
+ type PollEvents = {
271
302
  readable: boolean;
272
303
  writable: boolean;
273
304
  disconnect: boolean;
@@ -277,9 +308,13 @@ export namespace node {
277
308
  start(opts: PollOptions, callback: (ev: PollEvents) => void): void;
278
309
  start(callback: (ev: PollEvents) => void): void;
279
310
  stop(): void;
311
+
280
312
  close(): void;
313
+
281
314
  unref(): void;
282
315
  ref(): void;
316
+
317
+ [Symbol.dispose](): void;
283
318
  }
284
319
 
285
320
  export function poll(fd: number, opts: PollOptions, callback: (ev: PollEvents) => void): PollHandle;
@@ -2075,12 +2075,15 @@ const char *GetEnv(const char *name)
2075
2075
 
2076
2076
  bool GetDebugFlag(const char *name)
2077
2077
  {
2078
- const char *debug = GetEnv(name);
2078
+ Span<const char> str = TrimStr(GetEnv(name));
2079
2079
 
2080
- if (debug) {
2080
+ if (str.len) {
2081
2081
  bool ret = false;
2082
- if (!ParseBool(debug, &ret, K_DEFAULT_PARSE_FLAGS & ~(int)ParseFlag::Log)) {
2083
- LogError("Environment variable '%1' is not a boolean", name);
2082
+ if (!ParseBool(str, &ret, K_DEFAULT_PARSE_FLAGS & ~(int)ParseFlag::Log)) {
2083
+ LogError("Environment variable '%1=%2' is not a boolean", name, str);
2084
+ }
2085
+ if (ret) {
2086
+ LogWarning("Debug flag '%1' is in effect", name);
2084
2087
  }
2085
2088
  return ret;
2086
2089
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koffi",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.0-rc.1",
4
4
  "description": "Fast and simple C FFI (foreign function interface) for Node.js",
5
5
  "keywords": [
6
6
  "foreign",
@@ -32,6 +32,22 @@
32
32
  "napi": 8
33
33
  },
34
34
  "funding": "https://liberapay.com/Koromix",
35
+ "optionalDependencies": {
36
+ "@koromix/koffi-linux-arm64": "3.0.0-rc.1",
37
+ "@koromix/koffi-linux-ia32": "3.0.0-rc.1",
38
+ "@koromix/koffi-linux-x64": "3.0.0-rc.1",
39
+ "@koromix/koffi-linux-riscv64d": "3.0.0-rc.1",
40
+ "@koromix/koffi-freebsd-ia32": "3.0.0-rc.1",
41
+ "@koromix/koffi-freebsd-x64": "3.0.0-rc.1",
42
+ "@koromix/koffi-freebsd-arm64": "3.0.0-rc.1",
43
+ "@koromix/koffi-openbsd-ia32": "3.0.0-rc.1",
44
+ "@koromix/koffi-openbsd-x64": "3.0.0-rc.1",
45
+ "@koromix/koffi-win32-ia32": "3.0.0-rc.1",
46
+ "@koromix/koffi-win32-x64": "3.0.0-rc.1",
47
+ "@koromix/koffi-darwin-x64": "3.0.0-rc.1",
48
+ "@koromix/koffi-darwin-arm64": "3.0.0-rc.1",
49
+ "@koromix/koffi-linux-loong64": "3.0.0-rc.1"
50
+ },
35
51
  "type": "module",
36
52
  "main": "./index.cjs",
37
53
  "module": "./index.js",