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:
@@ -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(&.{}));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user