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
|
/// 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
|
/// sx code) is the caller. Without this they'd default to internal and
|
||||||
/// either DCE away or stay hidden from the dynamic symbol table.
|
/// 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 {
|
fn isExportedEntryName(name: []const u8) bool {
|
||||||
return std.mem.eql(u8, name, "main") or
|
return std.mem.eql(u8, name, "main") or
|
||||||
std.mem.eql(u8, name, "android_main") or
|
std.mem.eql(u8, name, "android_main") or
|
||||||
std.mem.eql(u8, name, "ANativeActivity_onCreate") 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 ───────────────────────────────────────────────────────────────
|
// ── Scope ───────────────────────────────────────────────────────────────
|
||||||
@@ -217,6 +220,15 @@ pub const Lowering = struct {
|
|||||||
self.lowerDeferredTypeFns();
|
self.lowerDeferredTypeFns();
|
||||||
// Pass 4: target-specific entry-point sanity checks
|
// Pass 4: target-specific entry-point sanity checks
|
||||||
self.checkRequiredEntryPoints();
|
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:
|
/// 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);
|
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