mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## What does this PR do? Updates the oven-sh/tinycc fork to the latest upstream TinyCC, incorporating 30+ upstream commits while preserving all Bun-specific patches. ### Upstream changes incorporated - Build system improvements (c2str.exe handling, cross-compilation) - macOS 15 compatibility fixes - libtcc debugging support - pic/pie support for i386 - arm64 alignment and symbol offset fixes - RISC-V 64 improvements (pointer difference, assembly, Zicsr extension) - Relocation updates - Preprocessor improvements (integer literal overflow handling) - x86-64 cvts*2si fix - Various bug fixes ### Bun-specific patches preserved - Fix crash on macOS x64 (libxcselect.dylib memory handling) - Implement `-framework FrameworkName` on macOS (for framework header parsing) - Add missing #ifdef guards for TCC_IS_NATIVE - Make `__attribute__(deprecated)` a no-op - Fix `__has_include` with framework paths - Support attributes after identifiers in enums - Fix dlsym behavior on macOS (RTLD_SELF first, then RTLD_DEFAULT) - Various tccmacho.c improvements ### Related PRs - TinyCC fork CI is passing: https://github.com/oven-sh/tinycc/actions/runs/21105489093 ## How did you verify your code works? - [x] TinyCC fork CI passes on all platforms (Linux x86_64/arm64/armv7/riscv64, macOS x86_64/arm64, Windows i386/x86_64) - [ ] Bun CI passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
5.6 KiB
TypeScript
225 lines
5.6 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, 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);
|
|
});
|
|
}); // </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 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;
|
|
}
|