Files
bun.sh/test/js/bun/ffi/cc.test.ts
robobun 039c89442f chore: bump TinyCC to latest upstream (Jan 2026) (#26210)
## 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>
2026-01-18 13:31:21 -08:00

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;
}