ffi 2.16c green: TL fallback via C-helper runtime + always-omit env in #jni_call
`#jni_call` collapses to a single surface — env is *always* implicit:
either picked up from the lexically-enclosing `#jni_env(env) { ... }`
block's Ref (cheap, register-resident, no TL touch) or from the
runtime's thread-local slot via `sx_jni_env_tl_get()` (one fn call
per dispatch). The explicit-env shape is gone — chess and the
existing tests migrate cleanly by wrapping their helper-fn bodies
in `#jni_env(env) { ... }`.
The TL slot lives outside the user's IR module so the LLVM ORC JIT
can load object files cleanly without `orc_rt` for TLS support:
library/vendors/sx_jni_runtime/sx_jni_env_tl.c:
static _Thread_local void *sx_jni_env_tl_slot;
void *sx_jni_env_tl_get(void) { return sx_jni_env_tl_slot; }
void sx_jni_env_tl_set(void *env) { sx_jni_env_tl_slot = env; }
Linkage:
- sx-the-compiler links the .c file via build.zig so the JIT
process-symbol generator resolves `sx_jni_env_tl_get`/`_set`.
- AOT targets get the same .c file auto-linked via the lowering
pass: when lower touches the TL externs, it sets
`needs_jni_env_tl_runtime`, and `Compilation.lowerToIR` appends a
synthetic `CImportInfo` to `lowering_extra_c_sources` that
`collectCImportSources` merges with user-written ones.
Lowering-side changes:
- `getJniEnvTlFids` lazily declares the two externs (parallel
to `getSelRegisterNameFid`) and flips `needs_jni_env_tl_runtime`.
- `#jni_env(env) { body }` emits save→set→body→restore via three
`call` ops to the externs; the inner body sees env via the
lexical-direct stack.
- `lowerJniCall` resolves env from `jni_env_stack` (top) or the TL
fallback. The explicit-env branch is gone.
- `jni_env_stack_base` tracks per-fn lexical scope so lazy-lowering
a callee doesn't accidentally see the caller's Ref (Refs are only
valid inside one fn's instruction stream).
Test migration (mechanical):
- ffi-jni-call-{01..09}: each helper fn wraps `#jni_call(...)`
bodies in `#jni_env(env) { ... }`. Returning values pass through
the block as an expression — `#jni_env` now also lowers in
expression position.
Verified:
- zig build test + tests/run_examples.sh: 130/130 green.
- tests/cross_compile.sh: 3/3 green.
- Chess APK rebuilt + reinstalled on Pixel. Board renders with
status-bar clearance + info panel intact; no crashes in logcat.
Safe-insets dispatch through `#jni_env` + lexical-direct now
fully exercised end-to-end on real hardware.
This commit is contained in:
51
src/core.zig
51
src/core.zig
@@ -28,6 +28,10 @@ pub const Compilation = struct {
|
||||
import_graph: std.StringHashMap(std.StringHashMap(void)),
|
||||
sema_result: ?sema.SemaResult = null,
|
||||
ir_emitter: ?ir.LLVMEmitter = null,
|
||||
/// C sources requested by the lowering pass (not in the user's AST).
|
||||
/// E.g. the JNI env TL runtime when `#jni_env` is used. Merged with
|
||||
/// AST sources in `collectCImportSources`.
|
||||
lowering_extra_c_sources: std.ArrayList(c_import.CImportInfo) = .empty,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, target_config: TargetConfig, stdlib_paths: []const []const u8) Compilation {
|
||||
return .{
|
||||
@@ -145,10 +149,36 @@ pub const Compilation = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Collect C import source info from the resolved AST.
|
||||
/// Collect C import source info — both from user-written `#import c { ... }`
|
||||
/// blocks in the AST AND from lowering-time auto-injections (currently:
|
||||
/// the JNI env TL runtime when `#jni_env` / `#jni_call`-with-omitted-env
|
||||
/// is used). The lower-side auto-injections live in
|
||||
/// `lowering_extra_c_sources` and are populated by `lowerToIR` based on
|
||||
/// `Lowering.needs_jni_env_tl_runtime` etc.
|
||||
pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo {
|
||||
const root = self.resolved_root orelse self.root orelse return &.{};
|
||||
return c_import.collectCImportSources(self.allocator, root);
|
||||
const ast_sources = try c_import.collectCImportSources(self.allocator, root);
|
||||
if (self.lowering_extra_c_sources.items.len == 0) return ast_sources;
|
||||
var merged = std.ArrayList(c_import.CImportInfo).empty;
|
||||
try merged.appendSlice(self.allocator, ast_sources);
|
||||
try merged.appendSlice(self.allocator, self.lowering_extra_c_sources.items);
|
||||
return merged.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
/// Resolve a stdlib-relative path through the configured `stdlib_paths`.
|
||||
/// Returns the first candidate whose absolute path resolves to an
|
||||
/// existing file. Used by lower-side auto-injected C sources.
|
||||
fn resolveStdlibPath(self: *Compilation, rel: []const u8) !?[]const u8 {
|
||||
for (self.stdlib_paths) |root_path| {
|
||||
const candidate = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ root_path, rel });
|
||||
if (std.Io.Dir.readFileAlloc(.cwd(), self.io, candidate, self.allocator, .limited(1024 * 1024))) |buf| {
|
||||
self.allocator.free(buf);
|
||||
return candidate;
|
||||
} else |_| {
|
||||
self.allocator.free(candidate);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Lower the parsed AST to the sx IR module (shadow pipeline).
|
||||
@@ -168,6 +198,23 @@ pub const Compilation = struct {
|
||||
lowering.import_graph = &self.import_graph;
|
||||
lowering.lowerRoot(root);
|
||||
if (self.diagnostics.hasErrors()) return error.CompileError;
|
||||
|
||||
// Auto-link the JNI env TL runtime when lowering used it. The .c file
|
||||
// ships with the sx library; we resolve it through stdlib_paths so
|
||||
// consumers don't need to vendor a copy.
|
||||
if (lowering.needs_jni_env_tl_runtime) {
|
||||
if (try self.resolveStdlibPath("vendors/sx_jni_runtime/sx_jni_env_tl.c")) |abs_path| {
|
||||
var sources = std.ArrayList([]const u8).empty;
|
||||
try sources.append(self.allocator, abs_path);
|
||||
try self.lowering_extra_c_sources.append(self.allocator, .{
|
||||
.sources = try sources.toOwnedSlice(self.allocator),
|
||||
.includes = &.{},
|
||||
.defines = &.{},
|
||||
.flags = &.{},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
|
||||
@@ -534,6 +534,10 @@ pub const LLVMEmitter = struct {
|
||||
|
||||
c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage);
|
||||
|
||||
if (global.is_thread_local) {
|
||||
c.LLVMSetThreadLocal(llvm_global, 1);
|
||||
}
|
||||
|
||||
// Evaluate comptime initializer if present
|
||||
if (global.comptime_func) |func_id| {
|
||||
var interp_inst = Interpreter.init(self.ir_mod, self.alloc);
|
||||
|
||||
@@ -503,6 +503,10 @@ pub const Global = struct {
|
||||
init_val: ?ConstantValue = null,
|
||||
is_extern: bool = false,
|
||||
is_const: bool = false,
|
||||
/// Thread-local storage. `global_get` / `global_set` emit normal LLVM
|
||||
/// load/store instructions; LLVM handles the per-thread access through
|
||||
/// the `thread_local` attribute on the global.
|
||||
is_thread_local: bool = false,
|
||||
/// For comptime globals: the function to interpret to get the init value.
|
||||
comptime_func: ?FuncId = null,
|
||||
};
|
||||
|
||||
122
src/ir/lower.zig
122
src/ir/lower.zig
@@ -98,6 +98,10 @@ pub const Lowering = struct {
|
||||
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
||||
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
|
||||
jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call`
|
||||
jni_env_stack_base: usize = 0, // index above which the currently-lowering fn's `#jni_env` scopes live; outer-fn Refs aren't valid in this fn's instruction stream
|
||||
jni_env_tl_get_fid: ?FuncId = null, // extern `sx_jni_env_tl_get` (from library/vendors/sx_jni_runtime/sx_jni_env_tl.c)
|
||||
jni_env_tl_set_fid: ?FuncId = null, // extern `sx_jni_env_tl_set`
|
||||
needs_jni_env_tl_runtime: bool = false, // set when lowering touches the JNI env TL; signals Compilation to auto-link the runtime .c
|
||||
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator), // sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass)
|
||||
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||
@@ -736,6 +740,14 @@ pub const Lowering = struct {
|
||||
const saved_block_terminated = self.block_terminated;
|
||||
const saved_force_block_value = self.force_block_value;
|
||||
const saved_source_file = self.current_source_file;
|
||||
// The `#jni_env` Ref stack is lexical within ONE function's instruction
|
||||
// stream — Refs from the caller don't dereference correctly in this
|
||||
// callee's body. Move the visible base to the current top so
|
||||
// omitted-env `#jni_call` in this fn doesn't accidentally pick up the
|
||||
// caller's Refs. Defer covers all the early-return paths below.
|
||||
const saved_jni_env_base = self.jni_env_stack_base;
|
||||
self.jni_env_stack_base = self.jni_env_stack.items.len;
|
||||
defer self.jni_env_stack_base = saved_jni_env_base;
|
||||
self.func_defer_base = self.defer_stack.items.len;
|
||||
self.block_terminated = false;
|
||||
self.force_block_value = false;
|
||||
@@ -1062,15 +1074,27 @@ pub const Lowering = struct {
|
||||
.insert_expr => |ins| self.lowerInsertExpr(ins.expr),
|
||||
.block => self.lowerBlock(node),
|
||||
.jni_env_block => |eb| {
|
||||
// Lexical-direct env resolution (2.16b): evaluate env once,
|
||||
// push onto the env stack, lower body, pop. `#jni_call`
|
||||
// sites inside `eb.body` with an omitted env arg pick up
|
||||
// the top-of-stack value directly — no thread-local read,
|
||||
// env stays register-resident across the body.
|
||||
// Compile-time stack push for lexical-direct env resolution
|
||||
// (2.16b — `#jni_call` in the same fn picks up env from
|
||||
// jni_env_stack directly, no TL read).
|
||||
//
|
||||
// Runtime TL save/set/restore (2.16c) for cross-function
|
||||
// helpers: callees in OTHER fns invoked from inside the
|
||||
// body read the slot via `sx_jni_env_tl_get`. Storage
|
||||
// lives in a separately-linked C helper (see
|
||||
// library/vendors/sx_jni_runtime/sx_jni_env_tl.c) so the
|
||||
// JIT doesn't need orc_rt for TLS.
|
||||
const env_ref = self.lowerExpr(eb.env);
|
||||
const fids = self.getJniEnvTlFids();
|
||||
const ptr_ty = self.module.types.ptrTo(.void);
|
||||
const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
||||
const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void);
|
||||
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
|
||||
defer _ = self.jni_env_stack.pop();
|
||||
self.lowerBlock(eb.body);
|
||||
_ = self.jni_env_stack.pop();
|
||||
const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
|
||||
},
|
||||
// Block-local type declarations
|
||||
.struct_decl => |sd| self.registerStructDecl(&sd),
|
||||
@@ -1795,6 +1819,23 @@ pub const Lowering = struct {
|
||||
.spread_expr => self.emitError("spread_expr", node.span),
|
||||
.chained_comparison => |cc| self.lowerChainedComparison(&cc),
|
||||
|
||||
// `#jni_env(env) { body }` in expression position — the block's
|
||||
// value becomes the env-scope's value. Save→set→body-value→restore.
|
||||
.jni_env_block => |eb| blk: {
|
||||
const env_ref = self.lowerExpr(eb.env);
|
||||
const fids = self.getJniEnvTlFids();
|
||||
const ptr_ty = self.module.types.ptrTo(.void);
|
||||
const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
||||
const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void);
|
||||
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
|
||||
const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void);
|
||||
_ = self.jni_env_stack.pop();
|
||||
const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
|
||||
break :blk value;
|
||||
},
|
||||
|
||||
// Statements that can appear in expression position
|
||||
.block => |blk| blk: {
|
||||
// Create a child scope for block-level variable shadowing
|
||||
@@ -3875,35 +3916,31 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerJniCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref {
|
||||
// env disambiguation: the method-name slot is always a string literal,
|
||||
// so its position tells us whether env was omitted.
|
||||
// omitted → args = target, "name", "sig", method-args... (≥3)
|
||||
// explicit → args = env, target, "name", "sig", method-args... (≥4)
|
||||
const env_omitted = fic.args.len >= 3 and fic.args[1].data == .string_literal;
|
||||
const min_arity: usize = if (env_omitted) 3 else 4;
|
||||
if (fic.args.len < min_arity) {
|
||||
// env is always implicit: lexical-direct from the enclosing `#jni_env(env)`
|
||||
// block (2.16b, cheap), else the thread-local slot the block populated
|
||||
// at runtime (2.16c, one TL load per call). Surface form is uniform:
|
||||
// #jni_call(T)(target, "name", "sig", method-args...) (≥3 args)
|
||||
if (fic.args.len < 3) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.add(.err, "#jni_call requires env (optional in #jni_env scope), target, method name, and signature", null);
|
||||
d.add(.err, "#jni_call requires target, method name, and signature", null);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
const ret_ty = self.resolveType(fic.return_type);
|
||||
|
||||
const env_ref = if (env_omitted) blk: {
|
||||
if (self.jni_env_stack.items.len == 0) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.add(.err, "#jni_call with omitted env requires an enclosing #jni_env scope", null);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
break :blk self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
|
||||
} else self.lowerExpr(fic.args[0]);
|
||||
const env_ref = if (self.jni_env_stack.items.len > self.jni_env_stack_base)
|
||||
self.jni_env_stack.items[self.jni_env_stack.items.len - 1]
|
||||
else blk: {
|
||||
const fids = self.getJniEnvTlFids();
|
||||
const ptr_ty = self.module.types.ptrTo(.void);
|
||||
break :blk self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
||||
};
|
||||
|
||||
const target_idx: usize = if (env_omitted) 0 else 1;
|
||||
const name_idx: usize = target_idx + 1;
|
||||
const sig_idx: usize = target_idx + 2;
|
||||
const first_method_arg_idx: usize = target_idx + 3;
|
||||
const target_idx: usize = 0;
|
||||
const name_idx: usize = 1;
|
||||
const sig_idx: usize = 2;
|
||||
const first_method_arg_idx: usize = 3;
|
||||
|
||||
const target_ref = self.lowerExpr(fic.args[target_idx]);
|
||||
const name_node = fic.args[name_idx];
|
||||
@@ -8130,6 +8167,37 @@ pub const Lowering = struct {
|
||||
self.foreign_class_map.put(fcd.name, fcd) catch {};
|
||||
}
|
||||
|
||||
/// Lazily declare the `sx_jni_env_tl_get` / `sx_jni_env_tl_set`
|
||||
/// runtime externs (step 2.16c). The storage lives in
|
||||
/// `library/vendors/sx_jni_runtime/sx_jni_env_tl.c` as a
|
||||
/// `_Thread_local` slot — keeping it OUT of the user's IR module
|
||||
/// is what lets the LLVM ORC JIT load the module cleanly without
|
||||
/// orc_rt platform support. AOT targets get the same .c file
|
||||
/// linked in via `needs_jni_env_tl_runtime`, which Compilation
|
||||
/// reads to append a synthetic c_import alongside the user's.
|
||||
fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } {
|
||||
self.needs_jni_env_tl_runtime = true;
|
||||
const ptr_ty = self.module.types.ptrTo(.void);
|
||||
if (self.jni_env_tl_get_fid == null) {
|
||||
const name = self.module.types.internString("sx_jni_env_tl_get");
|
||||
const fid = self.builder.declareExtern(name, &.{}, ptr_ty);
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = .c;
|
||||
self.jni_env_tl_get_fid = fid;
|
||||
}
|
||||
if (self.jni_env_tl_set_fid == null) {
|
||||
const name = self.module.types.internString("sx_jni_env_tl_set");
|
||||
const env_param = self.module.types.internString("env");
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = env_param, .ty = ptr_ty }) catch unreachable;
|
||||
const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void);
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = .c;
|
||||
self.jni_env_tl_set_fid = fid;
|
||||
}
|
||||
return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? };
|
||||
}
|
||||
|
||||
/// When a namespaced import (`Ns :: #import "..."`) contains foreign-class
|
||||
/// declarations, ALSO register them under their qualified name `Ns.Class`
|
||||
/// so receiver types like `*Ns.Class` can find the fcd. The recursive
|
||||
|
||||
Reference in New Issue
Block a user