diff --git a/src/c_import.test.zig b/src/c_import.test.zig index 39c76b3..9c9566e 100644 --- a/src/c_import.test.zig +++ b/src/c_import.test.zig @@ -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(&.{})); diff --git a/src/c_import.zig b/src/c_import.zig index 53538af..7e4d409 100644 --- a/src/c_import.zig +++ b/src/c_import.zig @@ -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); } }