feat: emcc C compiles go through the object cache

compileCWithEmcc now probes/saves .sx-cache/c-<key>.o with the same
content key as the native path (source + declared headers + transitive
deps + defines/flags/incdirs), keyed by the emcc --version line and the
wasm triple so emsdk upgrades and wasm32/64 variants never collide with
each other or with native objects. Cache hits hand the linker the cache
path directly. objectMagicOk accepts the wasm magic. Verified: warm
wasm build of a c-unit drops 1.85s -> 0.61s (emcc -c skipped).
This commit is contained in:
agra
2026-06-12 18:52:43 +03:00
parent 4b9324e585
commit 6114b51073
2 changed files with 73 additions and 6 deletions

View File

@@ -70,6 +70,7 @@ test "objectMagicOk: accepts Mach-O and ELF, rejects garbage and truncation" {
try std.testing.expect(c_import.objectMagicOk(&.{ 0xcf, 0xfa, 0xed, 0xfe, 0x00 })); // Mach-O 64
try std.testing.expect(c_import.objectMagicOk(&.{ 0xce, 0xfa, 0xed, 0xfe })); // Mach-O 32
try std.testing.expect(c_import.objectMagicOk(&.{ 0x7f, 'E', 'L', 'F', 0x02 }));
try std.testing.expect(c_import.objectMagicOk(&.{ 0x00, 'a', 's', 'm', 0x01 })); // wasm
try std.testing.expect(!c_import.objectMagicOk("not an object file"));
try std.testing.expect(!c_import.objectMagicOk(&.{ 0xcf, 0xfa, 0xed })); // truncated magic
try std.testing.expect(!c_import.objectMagicOk(&.{}));

View File

@@ -358,14 +358,15 @@ pub fn validateForeignRefs(allocator: std.mem.Allocator, root: *const Node, diag
checkForeignRefs(&valid, root.data.root.decls, diags);
}
/// A cached entry must at least LOOK like an object file (Mach-O or
/// ELF magic) — a truncated or garbage entry falls back to a fresh
/// compile instead of poisoning the link with an opaque error.
/// A cached entry must at least LOOK like an object file (Mach-O,
/// ELF, or wasm magic) — a truncated or garbage entry falls back to a
/// fresh compile instead of poisoning the link with an opaque error.
pub fn objectMagicOk(data: []const u8) bool {
if (data.len < 4) return false;
if (data[0] == 0x7f and data[1] == 'E' and data[2] == 'L' and data[3] == 'F') return true;
if (data[0] == 0xcf and data[1] == 0xfa and data[2] == 0xed and data[3] == 0xfe) return true; // Mach-O 64
if (data[0] == 0xce and data[1] == 0xfa and data[2] == 0xed and data[3] == 0xfe) return true; // Mach-O 32
if (data[0] == 0x00 and data[1] == 'a' and data[2] == 's' and data[3] == 'm') return true; // wasm (emcc -c)
return false;
}
@@ -615,6 +616,21 @@ pub fn loadCObjectsForJIT(
/// Compile C sources using emcc for Emscripten/WASM targets.
/// Shells out to `emcc -c` for each source file, returns temp object file paths.
// First line of `emcc --version` (the toolchain component of emcc
// cache keys); "" when it cannot be determined — keys then share one
// unversioned bucket, which only risks staleness across emsdk
// upgrades, never wrong content for a fixed toolchain.
fn emccVersionLine(allocator: std.mem.Allocator, io: std.Io) []const u8 {
const tmp = std.fmt.allocPrint(allocator, "/tmp/sx_emcc_ver_{d}", .{std.c.getpid()}) catch return "";
const cmd = std.fmt.allocPrint(allocator, "emcc --version 2>/dev/null | head -1 > {s}", .{tmp}) catch return "";
var child = std.process.spawn(io, .{ .argv = &.{ "sh", "-c", cmd } }) catch return "";
const result = child.wait(io) catch return "";
if (result != .exited or result.exited != 0) return "";
const line = std.Io.Dir.readFileAlloc(.cwd(), io, tmp, allocator, .limited(4096)) catch return "";
std.Io.Dir.deleteFile(.cwd(), io, tmp) catch {};
return std.mem.trim(u8, line, " \t\r\n");
}
pub fn compileCWithEmcc(
allocator: std.mem.Allocator,
io: std.Io,
@@ -625,10 +641,57 @@ pub fn compileCWithEmcc(
var paths = std.ArrayList([]const u8).empty;
var obj_idx: usize = 0;
const triple: []const u8 = if (target_config.isWasm64()) "wasm64-emscripten" else "wasm32-emscripten";
var emcc_version: ?[]const u8 = null; // resolved lazily, once, only if a unit exists
for (infos) |info| {
if (info.sources.len == 0) continue;
var inc_dirs = std.ArrayList([]const u8).empty;
for (info.includes) |inc| {
try inc_dirs.append(allocator, dirName(inc));
}
// Same cache participation as the native path: declared headers
// by content; an unreadable one disables caching, never the build.
var header_bytes = std.ArrayList([]const u8).empty;
var cache_ok = true;
for (info.includes) |inc| {
const hb = std.Io.Dir.readFileAlloc(.cwd(), io, inc, allocator, .limited(64 * 1024 * 1024)) catch {
cache_ok = false;
break;
};
try header_bytes.append(allocator, hb);
}
for (info.sources) |src| {
var cache_path: ?[:0]const u8 = null;
if (cache_ok) {
if (std.Io.Dir.readFileAlloc(.cwd(), io, src, allocator, .limited(64 * 1024 * 1024))) |src_bytes| {
if (emcc_version == null) emcc_version = emccVersionLine(allocator, io);
const dep_bytes = collectIncludeDepBytes(allocator, io, src, src_bytes, inc_dirs.items) catch &.{};
const key = cSourceCacheKey(
src_bytes,
header_bytes.items,
dep_bytes,
info.defines,
info.flags,
inc_dirs.items,
emcc_version.?,
triple,
null,
);
cache_path = try std.fmt.allocPrintSentinel(allocator, ".sx-cache/c-{x:0>16}.o", .{key}, 0);
if (loadCachedObject(cache_path.?)) |cached| {
// The linker only reads it — hand the cache path over
// directly, no copy into tmp_dir.
c.LLVMDisposeMemoryBuffer(cached);
try paths.append(allocator, cache_path.?);
continue;
}
} else |_| {}
}
const out_path = try std.fmt.allocPrint(allocator, "{s}/sx_emcc_{d}.o", .{ tmp_dir, obj_idx });
obj_idx += 1;
@@ -638,9 +701,8 @@ pub fn compileCWithEmcc(
if (target_config.isWasm64()) {
try argv.append(allocator, "-sMEMORY64");
}
// Add include paths
for (info.includes) |inc| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-I{s}", .{dirName(inc)}));
for (inc_dirs.items) |dir| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-I{s}", .{dir}));
}
for (info.defines) |def| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-D{s}", .{def}));
@@ -659,6 +721,10 @@ pub fn compileCWithEmcc(
std.debug.print("error: emcc failed for '{s}'\n", .{src});
return error.CompileError;
}
if (cache_path) |cp| {
std.Io.Dir.copyFile(.cwd(), out_path, .cwd(), cp, io, .{ .make_path = true }) catch {};
}
try paths.append(allocator, out_path);
}
}