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, isASAN, 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. // TinyCC's setjmp/longjmp error handling conflicts with ASan. it.todoIf(isWindows || isASAN)("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); }); // TinyCC's setjmp/longjmp error handling conflicts with ASan. describe.skipIf(isASAN)("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); }); }); // 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/); }); }); // 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 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); }); }); // // 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); }); }); // // ============================================================================= function makeValidCase>( name: string, source: string, symbols: Fns, ): Library { const filename = `${name}.c`; var library: Library; 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; }