Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
751be55926 fix(ffi): support cc() in bundled executables (bun build --compile)
This fixes issue #24752 where cc() failed when used in executables
created with `bun build --compile`. The problem was that C source
files bundled into the standalone executable were stored in the
virtual /$bunfs/ filesystem, which TinyCC couldn't read using addFile().

The fix detects when a file path is from the bundled filesystem and
uses TinyCC's compileString() to compile the C source directly from
memory instead of trying to read it from disk.

Fixes #24752

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 09:32:24 +00:00
2 changed files with 173 additions and 2 deletions

View File

@@ -111,18 +111,46 @@ pub const FFI = struct {
switch (this.*) {
.file => {
current_file_for_errors.* = this.file;
state.addFile(this.file) catch return error.CompilationError;
try addFileOrString(state, this.file);
current_file_for_errors.* = "";
},
.files => {
for (this.files.items) |file| {
current_file_for_errors.* = file;
state.addFile(file) catch return error.CompilationError;
try addFileOrString(state, file);
current_file_for_errors.* = "";
}
},
}
}
fn addFileOrString(state: *TCC.State, file_path: [:0]const u8) !void {
// Check if this is a bundled file from the standalone executable
if (bun.StandaloneModuleGraph.isBunStandaloneFilePath(file_path)) {
if (bun.StandaloneModuleGraph.get()) |graph| {
if (graph.find(file_path)) |file| {
// For bundled files, compile the source string directly
// We need to ensure it's null-terminated for TCC
const contents = file.contents;
if (contents.len > 0) {
// Check if already null-terminated
if (contents[contents.len - 1] == 0) {
state.compileString(contents[0 .. contents.len - 1 :0]) catch return error.CompilationError;
} else {
// Need to allocate a null-terminated copy
const nt_contents = bun.handleOom(bun.default_allocator.dupeZ(u8, contents));
defer bun.default_allocator.free(nt_contents);
state.compileString(nt_contents) catch return error.CompilationError;
}
return;
}
}
}
}
// For regular files, use addFile as before
state.addFile(file_path) catch return error.CompilationError;
}
};
const stdarg = struct {

View File

@@ -0,0 +1,143 @@
// https://github.com/oven-sh/bun/issues/24752
// Test that cc() works with bun build --compile
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
import { join } from "path";
test("cc() works with bun build --compile", async () => {
using dir = tempDir("test-cc-compile", {
"hello.ts": `
import { cc } from "bun:ffi";
import source from "./hello.c" with { type: "file" };
const {
symbols: { hello },
} = cc({
source,
symbols: {
hello: {
args: [],
returns: "int",
},
},
});
console.log("What is the answer to the universe?", hello());
`,
"hello.c": `
int hello() {
return 42;
}
`,
});
const outfile = join(String(dir), "hello");
// Build the standalone executable
await using proc1 = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target", "bun", "--outfile", outfile, "--entrypoint", "hello.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
if (exitCode1 !== 0) {
console.log("Build stdout:", stdout1);
console.log("Build stderr:", stderr1);
}
expect(exitCode1).toBe(0);
// Run the compiled executable
await using proc2 = Bun.spawn({
cmd: [outfile],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
expect(normalizeBunSnapshot(stdout2, dir)).toMatchInlineSnapshot(`"What is the answer to the universe? 42"`);
expect(exitCode2).toBe(0);
});
test("cc() works with multiple source files in bun build --compile", async () => {
using dir = tempDir("test-cc-compile-multi", {
"main.ts": `
import { cc } from "bun:ffi";
import add_source from "./add.c" with { type: "file" };
import mul_source from "./mul.c" with { type: "file" };
const {
symbols: { add, multiply },
} = cc({
source: [add_source, mul_source],
symbols: {
add: {
args: ["int", "int"],
returns: "int",
},
multiply: {
args: ["int", "int"],
returns: "int",
},
},
});
console.log("5 + 3 =", add(5, 3));
console.log("5 * 3 =", multiply(5, 3));
`,
"add.c": `
int add(int a, int b) {
return a + b;
}
`,
"mul.c": `
int multiply(int a, int b) {
return a * b;
}
`,
});
const outfile = join(String(dir), "main");
// Build the standalone executable
await using proc1 = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "--target", "bun", "--outfile", outfile, "--entrypoint", "main.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
if (exitCode1 !== 0) {
console.log("Build stdout:", stdout1);
console.log("Build stderr:", stderr1);
}
expect(exitCode1).toBe(0);
// Run the compiled executable
await using proc2 = Bun.spawn({
cmd: [outfile],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
const lines = normalizeBunSnapshot(stdout2, dir).split("\n").filter(Boolean);
expect(lines).toEqual(["5 + 3 = 8", "5 * 3 = 15"]);
expect(exitCode2).toBe(0);
});