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:
162
src/ir/lower.zig
162
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_<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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user