mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
223 lines
5.5 KiB
TypeScript
223 lines
5.5 KiB
TypeScript
import { cc, CString, ptr, type FFIFunction, type Library } from "bun:ffi";
|
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
import { promises as fs } from "fs";
|
|
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
|
|
import path from "path";
|
|
|
|
// TODO: we need to install build-essential and Apple SDK in CI.
|
|
// It can't find includes. It can on machines with that enabled.
|
|
it.todoIf(isWindows)("can run a .c file", () => {
|
|
const result = Bun.spawnSync({
|
|
cmd: [bunExe(), path.join(__dirname, "cc-fixture.js")],
|
|
cwd: __dirname,
|
|
env: bunEnv,
|
|
stdio: ["inherit", "inherit", "inherit"],
|
|
});
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
});
|
|
|
|
describe("given an add(a, b) function", () => {
|
|
const source = /* c */ `
|
|
int add(int a, int b) {
|
|
return a + b;
|
|
}
|
|
`;
|
|
let dir: string;
|
|
|
|
beforeAll(() => {
|
|
dir = tempDirWithFiles("bun-ffi-cc-test", {
|
|
"add.c": source,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("when compiled", () => {
|
|
let res: Library<{ add: { args: ["int", "int"]; returns: "int" } }>;
|
|
|
|
beforeAll(() => {
|
|
res = cc({
|
|
source: path.join(dir, "add.c"),
|
|
symbols: {
|
|
add: {
|
|
returns: "int",
|
|
args: ["int", "int"],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
res.close();
|
|
});
|
|
|
|
it("provides an add symbol", () => {
|
|
expect(res.symbols.add(1, 2)).toBe(3);
|
|
});
|
|
|
|
// FIXME: produces junk
|
|
it.skip("when passed arguments with incorrect types, throws an error", () => {
|
|
// @ts-expect-error
|
|
expect(() => res.symbols.add("1", "2")).toThrow();
|
|
});
|
|
|
|
// looks like `b` defaults to `0`, is this U.B. or expected?
|
|
it.skip("when passed too few arguments, throws an error", () => {
|
|
// @ts-expect-error
|
|
expect(() => res.symbols.add(1)).toThrow();
|
|
});
|
|
|
|
it("when passed too many arguments, still works", () => {
|
|
// @ts-expect-error
|
|
expect(res.symbols.add(1, 2, 3)).toBe(3);
|
|
});
|
|
|
|
it("Only contains 1 symbol", () => {
|
|
expect(Object.keys(res.symbols)).toHaveLength(1);
|
|
});
|
|
}); // </when compiled>
|
|
|
|
it("when compiled with a symbol that doesn't exist, throws an error", () => {
|
|
expect(() => {
|
|
cc({
|
|
source: path.join(dir, "add.c"),
|
|
symbols: { subtract: { args: ["int", "int"], returns: "int" } },
|
|
});
|
|
}).toThrow(/"subtract" is missing/);
|
|
});
|
|
}); // </given add(a, b) function>
|
|
|
|
describe("given a source file with syntax errors", () => {
|
|
const source = /* c */ `
|
|
int add(int a, int b) {
|
|
return a b;
|
|
}
|
|
`;
|
|
let dir: string;
|
|
|
|
beforeAll(() => {
|
|
dir = tempDirWithFiles("bun-ffi-cc-test", {
|
|
"add.c": source,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
// FIXME: fails asan poisoning check
|
|
// TinyCC uses `setjmp` on an internal error handler, then jumps there when it
|
|
// encounters a syntax error. Newer versions of tcc added a public API to
|
|
// set a runtime error handler, but but we need to upgrade in order to get it.
|
|
// https://github.com/TinyCC/tinycc/blob/f8bd136d198bdafe71342517fa325da2e243dc68/libtcc.h#L106C9-L106C24
|
|
it.skip("when compiled, throws an error", () => {
|
|
expect(() => {
|
|
cc({
|
|
source: path.join(dir, "add.c"),
|
|
symbols: {
|
|
add: {
|
|
returns: "int",
|
|
args: ["int", "int"],
|
|
},
|
|
},
|
|
});
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
describe.skip("given a ping(cstr) function", () => {
|
|
const library = makeValidCase(
|
|
"ping",
|
|
/* c */ `
|
|
char* ping(char* str) {
|
|
return str;
|
|
}
|
|
`,
|
|
{
|
|
ping: {
|
|
args: ["cstring"],
|
|
returns: "cstring",
|
|
},
|
|
},
|
|
);
|
|
|
|
it("given a valid CString, returns the same pointer", () => {
|
|
const buf = Buffer.from("hello\0");
|
|
const arr = new Uint8Array(buf);
|
|
const cstr = new CString(ptr(arr));
|
|
|
|
expect(library.symbols.ping(cstr)).toBe(cstr);
|
|
});
|
|
}); // </given a ping(cstr) function>
|
|
|
|
// FIXME: bus error
|
|
describe.skip("given a strlen(cstring) function", () => {
|
|
const library = makeValidCase(
|
|
"strlen",
|
|
/* c */ `
|
|
size_t strlen(char* str) {
|
|
char* s = str;
|
|
while (*s) s++;
|
|
return s - str;
|
|
}
|
|
`,
|
|
{
|
|
strlen: {
|
|
args: ["cstring"],
|
|
returns: "usize",
|
|
},
|
|
},
|
|
);
|
|
|
|
it("given a valid CString containing 'hello', returns the correct length", () => {
|
|
const buf = Buffer.from("hello\0");
|
|
const arr = new Uint8Array(buf);
|
|
const cstr = new CString(ptr(arr));
|
|
|
|
expect(library.symbols.strlen(cstr)).toBe(5);
|
|
});
|
|
|
|
it("given a JSString, throws", () => {
|
|
// @ts-expect-error
|
|
expect(() => library.symbols.strlen("hello")).toThrow(TypeError);
|
|
});
|
|
}); // </given a strlen(cstring) function>
|
|
|
|
// =============================================================================
|
|
|
|
function makeValidCase<Fns extends Record<string, FFIFunction>>(
|
|
name: string,
|
|
source: string,
|
|
symbols: Fns,
|
|
): Library<Fns> {
|
|
const filename = `${name}.c`;
|
|
|
|
var library: Library<Fns>;
|
|
|
|
beforeAll(() => {
|
|
try {
|
|
var dir = tempDirWithFiles(`bun-ffi-cc-${name}`, {
|
|
[filename]: source,
|
|
});
|
|
|
|
library = cc({
|
|
source: path.join(dir, filename),
|
|
symbols,
|
|
});
|
|
} finally {
|
|
// @ts-ignore -- `var` gets hoisted
|
|
if (dir) fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
library.close();
|
|
});
|
|
|
|
// @ts-ignore
|
|
return library;
|
|
}
|