Files
bun-src/test/napi/napi-value-ffi.test.ts
2026-08-27 21:09:14 +00:00

174 lines
5.7 KiB
TypeScript

import { spawnSync } from "bun";
import { cc, dlopen } from "bun:ffi";
import { beforeAll, describe, expect, it } from "bun:test";
import { existsSync } from "fs";
import { bunEnv, bunExe, canBuildNodeAddons, isASAN, isWindows, tempDir } from "harness";
import { join, resolve } from "path";
import source from "./napi-app/ffi_addon_1.c" with { type: "file" };
// The napi-app fixture needs a toolchain that can compile the reported
// Node headers.
const isFFIUnavailable = !canBuildNodeAddons();
const symbols = {
set_instance_data: {
args: ["napi_env", "int"],
returns: "void",
},
get_instance_data: {
args: ["napi_env"],
returns: "int",
},
get_type: {
args: ["napi_env", "napi_value"],
returns: "cstring",
},
};
let cc1, cc2;
const nodeApiHeadersInclude = join(__dirname, "napi-app/node_modules/node-api-headers/include");
function needsInstall(): boolean {
return !existsSync(nodeApiHeadersInclude);
}
beforeAll(() => {
if (isFFIUnavailable) return;
if (needsInstall()) {
// build gyp
const install = spawnSync({
cmd: [bunExe(), "install", "--verbose"],
cwd: join(__dirname, "napi-app"),
stderr: "inherit",
env: bunEnv,
stdout: "inherit",
stdin: "inherit",
});
if (!install.success) {
throw new Error("build failed");
}
}
// TinyCC's setjmp/longjmp error handling conflicts with ASan.
// Skip cc() calls on ASan, and catch errors on Windows.
if (!isASAN) {
try {
cc1 = cc({
source,
symbols,
flags: `-I${nodeApiHeadersInclude}`,
}).symbols;
cc2 = cc({
source,
symbols,
flags: `-I${nodeApiHeadersInclude}`,
}).symbols;
} catch (e) {
// ignore compilation failure on Windows
if (!isWindows) throw e;
}
}
});
describe.skipIf(isFFIUnavailable)("cc() bundled N-API headers", () => {
it.todoIf(isWindows || isASAN)("resolves <node_api.h> without any -I flag", () => {
const { symbols } = cc({
source: join(__dirname, "napi-app/bundled_napi_headers.c"),
symbols: { passthrough: { args: ["napi_env", "napi_value"], returns: "napi_value" } },
});
const marker = { marker: 42 };
expect(symbols.passthrough(undefined, marker)).toBe(marker);
});
it.todoIf(isWindows || isASAN)("provides the Node 26 type surface and NAPI_MODULE_INIT()", () => {
const { symbols } = cc({
source: join(__dirname, "napi-app/bundled_napi_headers_node26.c"),
symbols: {
node_api_module_get_api_version_v1: { args: [], returns: "i32" },
use_node26_types: { args: ["napi_env"], returns: "i32" },
},
});
expect(symbols.node_api_module_get_api_version_v1()).toBe(10);
expect(symbols.use_node26_types(undefined)).toBe(10);
});
it.todoIf(isWindows || isASAN)("compiles with NAPI_EXPERIMENTAL defined", () => {
const { symbols } = cc({
source: join(__dirname, "napi-app/bundled_napi_headers_experimental.c"),
symbols: { passthrough: { args: ["napi_env", "napi_value"], returns: "napi_value" } },
});
const marker = { marker: 42 };
expect(symbols.passthrough(undefined, marker)).toBe(marker);
});
});
// Bun's in-tree N-API headers (src/runtime/napi) are what napi.cpp compiles
// against and what cc() bundles for `#include <node_api.h>`. Compile a file
// that references the Node 26 type surface directly against them so the build
// asserts they stay in sync with upstream.
const systemCC = process.env.CC || Bun.which("cc") || Bun.which("gcc") || Bun.which("clang");
describe.skipIf(isWindows || !systemCC)("in-tree N-API headers", () => {
it("provide the Node 26 type surface and modern NAPI_MODULE_INIT()", async () => {
const bunHeaders = resolve(__dirname, "../../src/runtime/napi");
expect(existsSync(join(bunHeaders, "node_api.h"))).toBe(true);
using dir = tempDir("napi-headers-node26", {});
const out = join(String(dir), "addon.node");
await using compile = Bun.spawn({
cmd: [
systemCC!,
"-shared",
"-fPIC",
...(process.platform === "darwin" ? ["-undefined", "dynamic_lookup"] : []),
`-I${bunHeaders}`,
join(__dirname, "napi-app/bundled_napi_headers_node26.c"),
"-o",
out,
],
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
compile.stdout.text(),
compile.stderr.text(),
compile.exited,
]);
expect(stderr).not.toContain("error:");
expect({ stdout, exitCode }).toEqual({ stdout: "", exitCode: 0 });
// NAPI_MODULE_INIT() must emit node_api_module_get_api_version_v1 returning
// the header's default NAPI_VERSION; dlopen it and call it.
const lib = dlopen(out, {
node_api_module_get_api_version_v1: { args: [], returns: "i32" },
use_node26_types: { args: ["ptr"], returns: "i32" },
});
try {
expect(lib.symbols.node_api_module_get_api_version_v1()).toBe(10);
expect(lib.symbols.use_node26_types(null)).toBe(10);
} finally {
lib.close();
}
});
});
describe.skipIf(isFFIUnavailable)("cc napi integration", () => {
// fails on windows as TCC can't link the napi_ functions
// TinyCC's setjmp/longjmp error handling conflicts with ASan.
it.todoIf(isWindows || isASAN)("has a different napi_env for each cc invocation", () => {
cc1.set_instance_data(undefined, 5);
cc2.set_instance_data(undefined, 6);
expect(cc1.get_instance_data()).toBe(5);
expect(cc2.get_instance_data()).toBe(6);
});
// broken
it.todo("passes values correctly", () => {
expect(cc1.get_type(undefined, 123).toString()).toBe("number");
expect(cc1.get_type(undefined, "hello").toString()).toBe("string");
expect(cc1.get_type(undefined, 190n).toString()).toBe("bigint");
});
});