feat(C2): unit-first JIT symbol resolution — program-owned dylibs beat process images
runJITFromObject now takes priority dylibs (the #import c unit's linked objects first, then #library deps in declaration order) and attaches a per-path search generator for each AHEAD of the process-wide fallback, so a vendored symbol can never lose to a same-named export of an image the host process happens to carry (libz via LLVM, libsqlite3 via CoreServices). loadLibrary reports the name dlopen succeeded on; the c-import handle records its dylib path; temp link inputs are per-pid so concurrent runs can't clobber each other. Flips the C0.3 shadowing pin to from_unit: true.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
// Pins the C2 gap (PLAN-C C0.3): a `#source` unit defining a symbol
|
||||
// that ALSO lives in an OS image already loaded into the compiler
|
||||
// process (libz, via libLLVM) loses under `sx run` — the JIT resolves
|
||||
// globally and the earlier-loaded OS copy wins. The unit's
|
||||
// zlibCompileFlags answers 0xDEADBEEF (3735928559); the OS one answers
|
||||
// real flag bits. After C2 (unit-first resolution) this prints true.
|
||||
// Unit-first JIT resolution (PLAN-C C2): a `#source` unit defining a
|
||||
// symbol that ALSO lives in an OS image already loaded into the
|
||||
// compiler process (libz, via libLLVM) still wins — the unit's dylib
|
||||
// is a priority symbol-search target ahead of the process-wide
|
||||
// fallback. The unit's zlibCompileFlags answers 0xDEADBEEF; the OS
|
||||
// one answers real flag bits, so `true` proves the unit won.
|
||||
// Regression (PLAN-C C0.3 xfail, flipped by C2.1).
|
||||
#import "modules/std.sx";
|
||||
|
||||
zshadow :: #import c {
|
||||
|
||||
@@ -1 +1 @@
|
||||
from_unit: false
|
||||
from_unit: true
|
||||
|
||||
@@ -80,6 +80,9 @@ pub fn cSourceCacheKey(
|
||||
/// Handle returned from loadCObjectsForJIT — caller must call unload() after JIT.
|
||||
pub const CImportHandle = struct {
|
||||
dylib_handle: ?*anyopaque = null,
|
||||
/// Where the unit's linked dylib lives for THIS run; the JIT adds it
|
||||
/// as a priority symbol-search target ahead of the process images.
|
||||
dylib_path: ?[:0]const u8 = null,
|
||||
temp_paths: []const []const u8 = &.{},
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
@@ -415,10 +418,12 @@ pub fn loadCObjectsForJIT(
|
||||
|
||||
var temp_paths = std.ArrayList([]const u8).empty;
|
||||
|
||||
// Write each .o buffer to a temp file
|
||||
// Write each .o buffer to a temp file (per-pid names: concurrent
|
||||
// `sx run` processes must not clobber each other's link inputs)
|
||||
const pid = std.c.getpid();
|
||||
var obj_paths = std.ArrayList([]const u8).empty;
|
||||
for (obj_bufs, 0..) |buf, i| {
|
||||
const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}.o", .{i});
|
||||
const path = try std.fmt.allocPrint(allocator, "/tmp/sx_c_{d}_{d}.o", .{ pid, i });
|
||||
const start = c.LLVMGetBufferStart(buf);
|
||||
const size = c.LLVMGetBufferSize(buf);
|
||||
const data = @as([*]const u8, @ptrCast(start))[0..size];
|
||||
@@ -432,8 +437,8 @@ pub fn loadCObjectsForJIT(
|
||||
}
|
||||
|
||||
// Link into a shared library
|
||||
const dylib_path = "/tmp/sx_c_import.dylib";
|
||||
try temp_paths.append(allocator, try allocator.dupe(u8, dylib_path));
|
||||
const dylib_path = try std.fmt.allocPrintSentinel(allocator, "/tmp/sx_c_import_{d}.dylib", .{pid}, 0);
|
||||
try temp_paths.append(allocator, dylib_path);
|
||||
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
try argv.append(allocator, "cc");
|
||||
@@ -477,6 +482,7 @@ pub fn loadCObjectsForJIT(
|
||||
|
||||
return .{
|
||||
.dylib_handle = handle,
|
||||
.dylib_path = dylib_path,
|
||||
.temp_paths = try temp_paths.toOwnedSlice(allocator),
|
||||
.allocator = allocator,
|
||||
};
|
||||
|
||||
27
src/main.zig
27
src/main.zig
@@ -270,15 +270,21 @@ pub fn main(init: std.process.Init) !void {
|
||||
defer c_handle.unload(io);
|
||||
timer.record("c-import");
|
||||
|
||||
// dlopen #library dependencies so JIT can resolve foreign symbols
|
||||
// dlopen #library dependencies so JIT can resolve foreign symbols.
|
||||
// Program-owned dylibs (the #import c unit first, then #library
|
||||
// deps in declaration order) also become PRIORITY search targets
|
||||
// for the JIT, consulted before the process-wide fallback.
|
||||
const libs = extractLibraries(allocator, root) catch std.process.exit(1);
|
||||
var lib_handles = std.ArrayList(*anyopaque).empty;
|
||||
var priority_dylibs = std.ArrayList([:0]const u8).empty;
|
||||
if (c_handle.dylib_path) |cp| priority_dylibs.append(allocator, cp) catch {};
|
||||
defer {
|
||||
for (lib_handles.items) |h| _ = std.c.dlclose(h);
|
||||
}
|
||||
for (libs) |lib_name| {
|
||||
if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |handle| {
|
||||
lib_handles.append(allocator, handle) catch {};
|
||||
if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |loaded| {
|
||||
lib_handles.append(allocator, loaded.handle) catch {};
|
||||
priority_dylibs.append(allocator, loaded.path) catch {};
|
||||
} else {
|
||||
const e = std.c.dlerror();
|
||||
if (e) |msg| std.debug.print("warning: could not load library '{s}': {s}\n", .{ lib_name, std.mem.span(msg) });
|
||||
@@ -304,7 +310,7 @@ pub fn main(init: std.process.Init) !void {
|
||||
const marker = "--- build done ---\n";
|
||||
_ = std.c.write(1, marker.ptr, marker.len);
|
||||
}
|
||||
const exit_code = sx.target.runJITFromObject(obj_buf) catch {
|
||||
const exit_code = sx.target.runJITFromObject(obj_buf, priority_dylibs.items) catch {
|
||||
// JIT failed — fall back to AOT
|
||||
timer.record("jit-fail");
|
||||
runAOT(allocator, io, path, target_config, &timer, enable_cache, stdlib_paths) catch std.process.exit(1);
|
||||
@@ -924,8 +930,15 @@ fn extractFrameworks(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]
|
||||
return try fws.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
const LoadedLibrary = struct {
|
||||
handle: *anyopaque,
|
||||
/// The name dlopen succeeded on (full path or bare name) — reused
|
||||
/// verbatim as the JIT's priority search target for this library.
|
||||
path: [:0]const u8,
|
||||
};
|
||||
|
||||
/// Try to dlopen a library by name, searching user paths, host paths, and common naming conventions.
|
||||
fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?*anyopaque {
|
||||
fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?LoadedLibrary {
|
||||
const is_macos = comptime @import("builtin").os.tag == .macos;
|
||||
const suffixes: []const []const u8 = if (is_macos) &.{ ".dylib", ".so" } else &.{ ".so", ".dylib" };
|
||||
|
||||
@@ -944,7 +957,7 @@ fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_path
|
||||
for (paths) |dir| {
|
||||
for (suffixes) |sfx| {
|
||||
const full = std.fmt.allocPrintSentinel(allocator, "{s}/lib{s}{s}", .{ dir, lib_name, sfx }, 0) catch continue;
|
||||
if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return h;
|
||||
if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return .{ .handle = h, .path = full };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -952,7 +965,7 @@ fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_path
|
||||
// Fallback: bare name (let dlopen search its default paths)
|
||||
for (suffixes) |sfx| {
|
||||
const bare = std.fmt.allocPrintSentinel(allocator, "lib{s}{s}", .{ lib_name, sfx }, 0) catch continue;
|
||||
if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return h;
|
||||
if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return .{ .handle = h, .path = bare };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -198,7 +198,12 @@ pub const TargetConfig = struct {
|
||||
|
||||
/// Execute a precompiled object file in-process using LLVM's ORC JIT.
|
||||
/// Takes ownership of obj_buf. Returns the exit code from main().
|
||||
pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
|
||||
/// `priority_dylibs` are consulted for symbols BEFORE the process-wide
|
||||
/// search, in order: dylibs that belong to the program (the `#import c`
|
||||
/// unit's linked objects, then `#library` deps in declaration order)
|
||||
/// must win over a same-named export of an image the host process
|
||||
/// happens to carry (libz via LLVM, libsqlite3 via CoreServices, ...).
|
||||
pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef, priority_dylibs: []const [:0]const u8) !u8 {
|
||||
// Create LLJIT with default builder (no custom TM needed — .o is precompiled)
|
||||
var jit: c.LLVMOrcLLJITRef = null;
|
||||
var err = c.LLVMOrcCreateLLJIT(&jit, null);
|
||||
@@ -210,9 +215,25 @@ pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
|
||||
}
|
||||
defer _ = c.LLVMOrcDisposeLLJIT(jit);
|
||||
|
||||
// Add process symbols so JIT can find libc (printf, etc.)
|
||||
const jd = c.LLVMOrcLLJITGetMainJITDylib(jit);
|
||||
const prefix = c.LLVMOrcLLJITGetGlobalPrefix(jit);
|
||||
|
||||
// Program-owned dylibs first (generators run in attachment order).
|
||||
// A failed generator is skipped: resolution then degrades to the
|
||||
// process-wide search below, exactly the pre-priority behavior.
|
||||
for (priority_dylibs) |path| {
|
||||
var pgen: c.LLVMOrcDefinitionGeneratorRef = null;
|
||||
err = c.LLVMOrcCreateDynamicLibrarySearchGeneratorForPath(&pgen, path.ptr, prefix, null, null);
|
||||
if (err != null) {
|
||||
const msg = c.LLVMGetErrorMessage(err);
|
||||
defer c.LLVMDisposeErrorMessage(msg);
|
||||
std.debug.print("warning: JIT could not search '{s}': {s}\n", .{ path, std.mem.span(msg) });
|
||||
continue;
|
||||
}
|
||||
c.LLVMOrcJITDylibAddGenerator(jd, pgen);
|
||||
}
|
||||
|
||||
// Process-wide fallback so the JIT finds libc (printf, etc.)
|
||||
var gen: c.LLVMOrcDefinitionGeneratorRef = null;
|
||||
err = c.LLVMOrcCreateDynamicLibrarySearchGeneratorForProcess(&gen, prefix, null, null);
|
||||
if (err != null) {
|
||||
|
||||
Reference in New Issue
Block a user