Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion scripts/build/buildOptionsRs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export function generateBuildOptionsRs(cfg: Config): string {
"pub const ENABLE_LOGS: bool = cfg!(bun_debug);",
"pub const ENABLE_ASAN: bool = cfg!(bun_asan);",
"pub const ENABLE_TINYCC: bool = !cfg!(any(",
` all(windows, target_arch = "aarch64"),`,
` target_os = "android",`,
` target_os = "freebsd",`,
"));",
Expand Down
7 changes: 3 additions & 4 deletions scripts/build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,10 +867,9 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con
// failure is loud ("cannot find -l:libatomic.a") and the fix is obvious.
const staticLibatomic = partial.staticLibatomic ?? true;

// TinyCC: off on Windows ARM64 (not supported), Android (no upstream
// bionic support; FFI cc() falls back to dlopen-only), and FreeBSD
// (oven-sh/tinycc has no FreeBSD target).
const tinycc = partial.tinycc ?? !((windows && arm64) || abi === "android" || freebsd);
// TinyCC: off on Android (no upstream bionic support; FFI cc() falls back
// to dlopen-only) and FreeBSD (oven-sh/tinycc has no FreeBSD target).
const tinycc = partial.tinycc ?? !(abi === "android" || freebsd);
Comment thread
robobun marked this conversation as resolved.

const valgrind = partial.valgrind ?? false;
const fuzzilli = partial.fuzzilli ?? false;
Expand Down
7 changes: 3 additions & 4 deletions scripts/build/deps/tinycc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* TinyCC — small embeddable C compiler. Powers bun:ffi's JIT-compile path,
* where user-provided C gets compiled and linked at runtime.
*
* Disabled on windows-arm64 (tinycc doesn't have an arm64-coff backend).
* Disabled on Android and FreeBSD — see cfg.tinycc in config.ts.
*
* Built via DirectBuild — no cmake sub-process. The old overlay
* CMakeLists.txt had two recurring ASAN workarounds for the c2str host
Expand All @@ -12,14 +12,13 @@

import type { Dependency, DirectBuild } from "../source.ts";

const TINYCC_COMMIT = "12882eee073cfe5c7621bcfadf679e1372d4537b";
const TINYCC_COMMIT = "05f0fafaa3be31e31d7b4b5c17dc60f62c991171";

export const tinycc: Dependency = {
name: "tinycc",
versionMacro: "TINYCC",

// The cfg.tinycc flag already encodes the windows-arm64 exclusion
// (see config.ts: `tinycc ?? !(windows && arm64)`).
// cfg.tinycc encodes the platform exclusions (config.ts).
enabled: cfg => cfg.tinycc,

source: () => ({
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export interface Dependency {

/**
* Whether this dep participates in the build at all. Defaults to always-on.
* E.g. libuv is windows-only, tinycc is disabled on windows-arm64.
* E.g. libuv is windows-only, tinycc is disabled on Android/FreeBSD.
*/
enabled?: (cfg: Config) => boolean;

Expand Down
52 changes: 52 additions & 0 deletions src/runtime/ffi/libtcc1.c
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,55 @@ unsigned long long __fixunsxfdi (long double a1)
else
return 0;
}

/* TinyCC lib/va_list.c (x86_64 SysV only): __va_arg is no longer inlined and
Bun supplies libtcc1 from this file. No extern abort(): Bun never injects
that symbol, so referencing it would fail every cc() at relocate. */
#if defined(__x86_64__) && !defined(_WIN32)

enum __va_arg_type {
__va_gen_reg, __va_float_reg, __va_stack
};

void *__va_arg(__builtin_va_list ap,
int arg_type,
int size, int align)
{
size = (size + 7) & ~7;
align = (align + 7) & ~7;
switch ((enum __va_arg_type)arg_type) {
case __va_gen_reg:
if (ap->gp_offset + size <= 48) {
ap->gp_offset += size;
return ap->reg_save_area + ap->gp_offset - size;
}
goto use_overflow_area;

case __va_float_reg:
if (ap->fp_offset < 128 + 48) {
ap->fp_offset += 16;
if (size == 8)
return ap->reg_save_area + ap->fp_offset - 16;
if (ap->fp_offset < 128 + 48) {
double *p = (double *)(ap->reg_save_area + ap->fp_offset);
p[-1] = p[0];
ap->fp_offset += 16;
return ap->reg_save_area + ap->fp_offset - 32;
}
}
goto use_overflow_area;

case __va_stack:
use_overflow_area:
ap->overflow_arg_area += size;
ap->overflow_arg_area = (char*)((long long)(ap->overflow_arg_area + align - 1) & -align);
return ap->overflow_arg_area - size;

default:
/* unreachable: the compiler only emits the three classes above.
Trap with a null write like TinyCC's old inline __va_arg did. */
*(volatile char *)0 = 0;
return 0;
}
}
#endif /* __x86_64__ && !_WIN32 */
12 changes: 6 additions & 6 deletions src/tcc_sys/tcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ pub type TCCErrorFunc = Option<unsafe extern "C" fn(opaque: *mut c_void, msg: *c
pub type ErrorFunc<Ctx> = unsafe extern "C" fn(ctx: *mut Ctx, msg: *const c_char);

// `libtcc.a` is only built where `cfg.tinycc` is true (`scripts/build/config.ts`):
// not Windows/aarch64 (TinyCC has no aarch64-pe-coff backend), not Android, not
// FreeBSD (the vendored fork doesn't support those targets). On those platforms
// these `extern "C"` decls would be undefined at link:
// not Android, not FreeBSD (the vendored fork doesn't support those targets).
// On those platforms these `extern "C"` decls would be undefined at link:
// `bun_runtime::ffi::ffi_body::{Source::add,
// CompileC::compile}` are reachable from `extern "C"` JS bindings and the
// monomorphized refs land in `libbun_rust.a` regardless of any
Expand All @@ -24,15 +23,16 @@ pub type ErrorFunc<Ctx> = unsafe extern "C" fn(ctx: *mut Ctx, msg: *const c_char
// in this build"), and the `unreachable!()` makes any future gate regression
// loud rather than silently UB.
//
// Keep this predicate in sync with `cfg.tinycc` in `scripts/build/config.ts`.
// Keep this predicate in sync with `cfg.tinycc` in `scripts/build/config.ts`
// and `ENABLE_TINYCC` in `scripts/build/buildOptionsRs.ts`.
macro_rules! tcc_externs {
($($(#[$attr:meta])* fn $name:ident($($arg:ident: $ty:ty),* $(,)?) $(-> $ret:ty)?;)*) => {
#[cfg(not(any(target_os = "android", target_os = "freebsd", all(windows, target_arch = "aarch64"))))]
#[cfg(not(any(target_os = "android", target_os = "freebsd")))]
unsafe extern "C" {
$($(#[$attr])* fn $name($($arg: $ty),*) $(-> $ret)?;)*
}
$(
#[cfg(any(target_os = "android", target_os = "freebsd", all(windows, target_arch = "aarch64")))]
#[cfg(any(target_os = "android", target_os = "freebsd"))]
#[allow(unused_variables, clippy::missing_safety_doc)]
unsafe extern "C" fn $name($($arg: $ty),*) $(-> $ret)? {
unreachable!(concat!(
Expand Down
199 changes: 190 additions & 9 deletions test/js/bun/ffi/cc.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { cc, CString, JSCallback, 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, isArm64, isASAN, isWindows, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tempDir, tempDirWithFiles } from "harness";
import path from "path";

// TinyCC (and all of bun:ffi) is disabled on Windows ARM64
const isFFIUnavailable = isWindows && isArm64;

// 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 || isFFIUnavailable)("can run a .c file", () => {
it.todoIf(isWindows || isASAN)("can run a .c file", () => {
const result = Bun.spawnSync({
cmd: [bunExe(), path.join(__dirname, "cc-fixture.js")],
cwd: __dirname,
Expand All @@ -22,8 +19,7 @@ it.todoIf(isWindows || isASAN || isFFIUnavailable)("can run a .c file", () => {
});

// TinyCC's setjmp/longjmp error handling conflicts with ASan.
// TinyCC is disabled on Windows ARM64.
describe.skipIf(isASAN || isFFIUnavailable)("given an add(a, b) function", () => {
describe.skipIf(isASAN)("given an add(a, b) function", () => {
const source = /* c */ `
int add(int a, int b) {
return a + b;
Expand Down Expand Up @@ -391,7 +387,7 @@ describe.skipIf(isWindows || isASAN)("threadsafe JSCallback invoked from a forei
// Pins GC liveness: compiled trampolines survive the library wrapper being
// collected, and a JSCallback's closure stays alive until close().
// TinyCC's setjmp/longjmp error handling conflicts with ASan.
describe.skipIf(isASAN || isFFIUnavailable)("GC liveness of compiled symbols and callbacks", () => {
describe.skipIf(isASAN)("GC liveness of compiled symbols and callbacks", () => {
it("keeps symbol functions and callback closures alive across forced GC", async () => {
using dir = tempDir("bun-ffi-cc-gc-liveness", {
"lib.c": /* c */ `
Expand Down Expand Up @@ -457,7 +453,192 @@ describe.skipIf(isASAN || isFFIUnavailable)("GC liveness of compiled symbols and
});
});

describe.skipIf(isFFIUnavailable)("double <-> JSValue conversions", () => {
// va_arg on x86_64 SysV lowers to a call to __va_arg, which TinyCC expects
// libtcc1 to provide; Bun replaces libtcc1 with src/runtime/ffi/libtcc1.c.
// TinyCC's setjmp/longjmp error handling conflicts with ASan.
describe.skipIf(isASAN)("variadic functions inside cc()-compiled C", () => {
it("va_arg over ints, doubles, and the stack overflow area", async () => {
using dir = tempDir("bun-ffi-cc-varargs", {
"varargs.c": /* c */ `
#include <stdarg.h>

static long long sum_ints(int count, ...) {
va_list ap;
va_start(ap, count);
long long total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, int);
va_end(ap);
return total;
}

static double sum_doubles(int count, ...) {
va_list ap;
va_start(ap, count);
double total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, double);
va_end(ap);
return total;
}

/* alternating int/double reads from one va_list: gp_offset and
fp_offset must advance independently */
static double sum_pairs(int count, ...) {
va_list ap;
va_start(ap, count);
double total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(ap, int);
total += va_arg(ap, double);
}
va_end(ap);
return total;
}

/* a 16-byte all-double struct occupies two SSE register save slots */
struct dd { double a, b; };
static double sum_dd(int count, ...) {
va_list ap;
va_start(ap, count);
double total = 0;
for (int i = 0; i < count; i++) {
struct dd v = va_arg(ap, struct dd);
total += v.a + v.b;
}
va_end(ap);
return total;
}

/* 10 ints: exhausts the 6 integer registers and spills to the stack. */
long long ten_ints(void) { return sum_ints(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); }
/* 10 doubles: exhausts the 8 SSE registers and spills to the stack. */
double ten_doubles(void) { return sum_doubles(10, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5); }
double interleaved(void) { return sum_pairs(9, 1,0.5, 2,0.5, 3,0.5, 4,0.5, 5,0.5, 6,0.5, 7,0.5, 8,0.5, 9,0.5); }
double double_pairs(void) {
struct dd x = { 1.5, 2.5 }, y = { 3.0, 4.0 };
return sum_dd(2, x, y);
}
`,
"fixture.js": /* js */ `
import { cc } from "bun:ffi";
import path from "path";

const { symbols } = cc({
source: path.join(import.meta.dir, "varargs.c"),
symbols: {
ten_ints: { args: [], returns: "i64" },
ten_doubles: { args: [], returns: "f64" },
interleaved: { args: [], returns: "f64" },
double_pairs: { args: [], returns: "f64" },
},
});
console.log(
JSON.stringify({
ten_ints: Number(symbols.ten_ints()),
ten_doubles: symbols.ten_doubles(),
interleaved: symbols.interleaved(),
double_pairs: symbols.double_pairs(),
}),
);
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// stderr is included in the received object so failures show it, but is not
// asserted empty: debug builds emit benign startup warnings.
const results = stdout.startsWith("{") ? JSON.parse(stdout) : stdout;
expect({ results, stderr, exitCode }).toMatchObject({
results: {
ten_ints: 55,
ten_doubles: 50,
interleaved: 49.5,
double_pairs: 11,
},
exitCode: 0,
});
});
});

// long double is 16 bytes on x86_64 and always va_arg'd through the stack; on
// aarch64 it is binary128 and its arithmetic needs soft-float helpers
// (__addtf3, ...) that Bun's TCC states do not provide, so x64 only.
describe.skipIf(isASAN || process.arch !== "x64")("long double varargs inside cc()-compiled C", () => {
it("va_arg over long double", async () => {
using dir = tempDir("bun-ffi-cc-varargs-ld", {
"ld.c": /* c */ `
#include <stdarg.h>

static double sum_long_doubles(int count, ...) {
va_list ap;
va_start(ap, count);
long double total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, long double);
va_end(ap);
return (double)total;
}

double long_doubles(void) { return sum_long_doubles(3, 1.5L, 2.25L, 3.25L); }
`,
"fixture.js": /* js */ `
import { cc } from "bun:ffi";
import path from "path";

const { symbols } = cc({
source: path.join(import.meta.dir, "ld.c"),
symbols: { long_doubles: { args: [], returns: "f64" } },
});
console.log(JSON.stringify({ long_doubles: symbols.long_doubles() }));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const results = stdout.startsWith("{") ? JSON.parse(stdout) : stdout;
expect({ results, stderr, exitCode }).toMatchObject({
results: { long_doubles: 7 },
exitCode: 0,
});
});
});

// TinyCC emits Local-Exec TLS, which has no PT_TLS segment to target under
// in-memory relocation and would alias the host's own thread block; it must be
// rejected up front instead of silently corrupting Bun's thread-locals.
describe.skipIf(isASAN)("thread-local storage inside cc()-compiled C", () => {
it.each([
["_Thread_local", " = 0"],
["__thread", " = 0"],
// No initializer: lands in .tbss, so the guard's tbss/SHF_TLS arm is covered too.
["_Thread_local", ""],
["__thread", ""],
])("%s int x%s; is a compile error", (keyword, init) => {
using dir = tempDir("bun-ffi-cc-tls", {
"tls.c": `${keyword} int bun_test_tls_counter${init};\nint bump(void) { return ++bun_test_tls_counter; }\n`,
});
expect(() => {
cc({
source: path.join(String(dir), "tls.c"),
symbols: { bump: { args: [], returns: "int" } },
});
}).toThrow(/thread-local storage is not supported/);
});
});

describe("double <-> JSValue conversions", () => {
// JSC NaN-boxes doubles, so a NaN whose payload collides with the tag space
// ("impure NaN", see JSC's PureNaN.h) must never be encoded as-is: it would
// decode as a native-chosen JSValue (true, undefined, an Int32, or a cell
Expand Down
7 changes: 2 additions & 5 deletions test/js/bun/ffi/ffi-error-messages.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { dlopen, linkSymbols } from "bun:ffi";
import { describe, expect, test } from "bun:test";
import { isArm64, isMusl, isWindows } from "harness";
import { isMusl } from "harness";

// TinyCC (and all of bun:ffi) is disabled on Windows ARM64
const isFFIUnavailable = isWindows && isArm64;

describe.skipIf(isFFIUnavailable)("FFI error messages", () => {
describe("FFI error messages", () => {
test("dlopen shows library name when library cannot be opened", () => {
// Try to open a non-existent library
try {
Expand Down
Loading
Loading