From 063bbb54191d3f70f3e6505a4df78222ce6d5420 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 20 May 2026 15:10:33 +0300 Subject: [PATCH] ffi #jni_main R.3: synthesize JNI-mangled exports for bodied methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After lowering completes, a new pass walks `foreign_class_map` and, for every bodied non-static method on a `#jni_main #jni_class("...")` decl, synthesises a C-ABI exported function whose name follows JNI's name- mangling convention: `Java___sx_1` (`/` → `_`, `_` → `_1`). Android's JNI runtime resolves `private native sx_(...)` declared in the bundled classes.dex via this symbol without needing an explicit `JNI_OnLoad`/`RegisterNatives` — the name-mangling fallback is enough. Param ABI: `(env: *void, self: *void)` prepended (JNIEnv* + jobject receiver), followed by the user-declared params with pointer types type-erased to `*void`. The user's body is lowered through the normal fn-body pipeline with `env`, `self`, and the user-named params bound in scope. `isExportedEntryName` now also returns true for any name starting with `Java_` so emit_llvm sets external linkage. Verified end-to-end: `llvm-nm -D` on the slice 2 smoke .so shows `Java_co_swipelab_sxjnimain_SxApp_sx_1onCreate` as an exported T symbol. 131 host / 4 cross / zig build test all green. Future work (R.3b territory): richer typing inside bodies so `*Self` / `*Bundle` params support method dispatch through the foreign-class slot interning. For now `self`/`b` are opaque `*void` jobjects in scope — fine for stub bodies and `#jni_call`-driven dispatch. --- src/ir/lower.zig | 162 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2fd2ac8..c8f453c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -24,11 +24,14 @@ const Builder = mod_mod.Builder; /// Names that must keep external LLVM linkage because the OS loader (not /// sx code) is the caller. Without this they'd default to internal and /// either DCE away or stay hidden from the dynamic symbol table. +/// Anything starting with `Java_` is a JNI native method that Android's +/// runtime resolves by name mangling — same rule. fn isExportedEntryName(name: []const u8) bool { return std.mem.eql(u8, name, "main") or std.mem.eql(u8, name, "android_main") or std.mem.eql(u8, name, "ANativeActivity_onCreate") or - std.mem.eql(u8, name, "JNI_OnLoad"); + std.mem.eql(u8, name, "JNI_OnLoad") or + std.mem.startsWith(u8, name, "Java_"); } // ── Scope ─────────────────────────────────────────────────────────────── @@ -217,6 +220,15 @@ pub const Lowering = struct { self.lowerDeferredTypeFns(); // Pass 4: target-specific entry-point sanity checks self.checkRequiredEntryPoints(); + // Pass 5: synthesize JNI-mangled exports for `#jni_main` bodied methods. + // Android's JNI runtime resolves `private native sx_(...)` declared in + // the bundled classes.dex by looking up the symbol + // `Java___sx_1` in the loaded .so. Each + // bodied method on a `#jni_main #jni_class` decl becomes an exported + // C-ABI fn with that name; the JNIEnv* / jobject params are prepended, + // then the user-declared params (with type-erased pointers since JNI + // doesn't carry sx-side types across the binding). + self.synthesizeJniMainStubs(); } /// On Android, the OS loads the .so via one of two entry paths: @@ -9750,4 +9762,152 @@ pub const Lowering = struct { self.builder.ret(default_val, ret_ty); } } + + /// Emit a C-ABI exported function for every bodied method on a + /// `#jni_main #jni_class("...")` declaration. The symbol name follows + /// JNI's name-mangling convention so Android's JNI runtime can resolve + /// `private native sx_(...)` (declared in the bundled + /// classes.dex by `jni_java_emit`) without an explicit `RegisterNatives` + /// call — i.e. `Java___sx_1`. + /// + /// Param ABI: prepended `(env: *void, self: *void)` (JNIEnv* + jobject + /// receiver), followed by the user-declared params with pointer types + /// type-erased to `*void` (JNI carries jobjects, not sx-typed handles — + /// future work can keep richer typing inside the body when needed). + fn synthesizeJniMainStubs(self: *Lowering) void { + var seen = std.StringHashMap(void).init(self.alloc); + defer seen.deinit(); + + var it = self.foreign_class_map.iterator(); + while (it.next()) |entry| { + const fcd = entry.value_ptr.*; + if (!fcd.is_main) continue; + if (fcd.is_foreign) continue; + if (fcd.runtime != .jni_class) continue; + if (seen.contains(fcd.foreign_path)) continue; + seen.put(fcd.foreign_path, {}) catch continue; + + for (fcd.members) |m| switch (m) { + .method => |md| { + if (md.body == null) continue; + if (md.is_static) continue; // future: emit static native ABI without `self` + self.synthesizeJniMainStub(fcd, md); + }, + else => {}, + }; + } + } + + fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void { + const mangled = jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return; + const name_id = self.module.types.internString(mangled); + + const ptr_void = self.module.types.ptrTo(.void); + var params = std.ArrayList(inst_mod.Function.Param).empty; + params.append(self.alloc, .{ + .name = self.module.types.internString("env"), + .ty = ptr_void, + }) catch return; + params.append(self.alloc, .{ + .name = self.module.types.internString("self"), + .ty = ptr_void, + }) catch return; + + // User's declared params (skip the implicit `*Self` at index 0 for + // instance methods — we synthesized `self` above as the jobject). + const param_start: usize = 1; + for (md.params[param_start..], 0..) |p_node, i| { + const pty = jniMapParamType(self, p_node); + params.append(self.alloc, .{ + .name = self.module.types.internString(md.param_names[param_start + i]), + .ty = pty, + }) catch return; + } + + const ret_ty = if (md.return_type) |rt| jniMapParamType(self, rt) else .void; + const params_slice = params.toOwnedSlice(self.alloc) catch return; + + _ = self.builder.beginFunction(name_id, params_slice, ret_ty); + self.builder.currentFunc().linkage = .external; + self.builder.currentFunc().call_conv = .c; + + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + + var scope = Scope.init(self.alloc, self.scope); + defer scope.deinit(); + const saved_scope = self.scope; + self.scope = &scope; + defer self.scope = saved_scope; + + for (params_slice, 0..) |p, i| { + const slot = self.builder.alloca(p.ty); + const param_ref = Ref.fromIndex(@intCast(i)); + self.builder.store(slot, param_ref); + scope.put(self.module.types.getString(p.name), .{ .ref = slot, .ty = p.ty, .is_alloca = true }); + } + + const saved_target = self.target_type; + self.target_type = if (ret_ty != .void) ret_ty else null; + if (ret_ty != .void) { + const body_val = self.lowerBlockValue(md.body.?); + if (!self.currentBlockHasTerminator()) { + if (body_val) |val| { + const val_ty = self.builder.getRefType(val); + if (val_ty == .void) { + self.ensureTerminator(ret_ty); + } else { + const coerced = self.coerceToType(val, val_ty, ret_ty); + self.builder.ret(coerced, ret_ty); + } + } else { + self.ensureTerminator(ret_ty); + } + } + } else { + self.lowerBlock(md.body.?); + self.ensureTerminator(ret_ty); + } + self.target_type = saved_target; + + self.builder.finalize(); + } }; + +/// JNI map: pointer types collapse to `*void` (jobject opaque handle); +/// primitives pass through unchanged. +fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId { + return switch (type_node.data) { + .pointer_type_expr => self.module.types.ptrTo(.void), + else => self.resolveType(type_node), + }; +} + +/// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol +/// `Java___sx_1`. JNI mangling: +/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side +/// `private native sx_(...)` delegate. +fn jniMangleNativeName(allocator: std.mem.Allocator, foreign_path: []const u8, method_name: []const u8) ![]u8 { + var buf = std.ArrayList(u8).empty; + try buf.appendSlice(allocator, "Java_"); + for (foreign_path) |ch| { + if (ch == '/') { + try buf.append(allocator, '_'); + } else if (ch == '_') { + try buf.appendSlice(allocator, "_1"); + } else { + try buf.append(allocator, ch); + } + } + try buf.append(allocator, '_'); + try buf.appendSlice(allocator, "sx_1"); + for (method_name) |ch| { + if (ch == '_') { + try buf.appendSlice(allocator, "_1"); + } else { + try buf.append(allocator, ch); + } + } + return buf.toOwnedSlice(allocator); +}