ffi #jni_main R.3: synthesize JNI-mangled exports for bodied methods

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_<pkg-mangled>_<Class>_sx_1<method-mangled>`

(`/` → `_`, `_` → `_1`). Android's JNI runtime resolves `private native
sx_<method>(...)` 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.
This commit is contained in:
agra
2026-05-20 15:10:33 +03:00
parent 2461218111
commit 063bbb5419

View File

@@ -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_<m>(...)` declared in
// the bundled classes.dex by looking up the symbol
// `Java_<pkg-mangled>_<Class>_sx_1<m-mangled>` 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_<method>(...)` (declared in the bundled
/// classes.dex by `jni_java_emit`) without an explicit `RegisterNatives`
/// call — i.e. `Java_<pkg-mangled>_<Class>_sx_1<method-mangled>`.
///
/// 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_<pkg-mangled>_<Class>_sx_1<method-mangled>`. JNI mangling:
/// `/` → `_`, `_` → `_1`. The `sx_` prefix matches the Java-side
/// `private native sx_<name>(...)` 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);
}