ffi #jni_main: emit static { System.loadLibrary(...); } in the Java class

Required for Android to resolve the `Java_*` symbols R.3 synthesises:
without `System.loadLibrary(...)` running before the Activity calls its
first native method, JNI lookup fails with UnsatisfiedLinkError.

The lib name comes from the build's `-o` basename — `/tmp/libsxchess.so`
→ `sxchess` — derived in `Compilation.collectJniMainEmissions` and
threaded through new `jni_java_emit.Options.lib_name`. When `-o` is
unset (or doesn't match `lib*.so`), the emitter omits the static init
and the caller must arrange loading another way.

dex confirmation on the slice 2 smoke: `<clinit>` static constructor
appears alongside `<init>` and `sx_onCreate` — the bytecode invokes
`System.loadLibrary("sxjnimain")` matching `/tmp/libsxjnimain.so`.

131 host / 4 cross / zig build test all green.
This commit is contained in:
agra
2026-05-20 16:45:41 +03:00
parent 22768d9adf
commit d43f21f39e
3 changed files with 68 additions and 3 deletions

View File

@@ -244,6 +244,12 @@ pub const Compilation = struct {
try registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path);
}
// Derive the `System.loadLibrary` argument from the `-o` basename
// (e.g. `/tmp/libsxchess.so` → `sxchess`). When `-o` is unset the
// emitter omits the static init block; the user must then arrange
// .so loading via another class.
const lib_name = libNameFromOutputPath(self.target_config.output_path);
var it = lowering.foreign_class_map.iterator();
while (it.next()) |entry| {
const fcd = entry.value_ptr.*;
@@ -253,7 +259,10 @@ pub const Compilation = struct {
if (seen.contains(fcd.foreign_path)) continue;
try seen.put(fcd.foreign_path, {});
const java_source = try ir.jni_java_emit.emitJavaSource(self.allocator, fcd, .{ .classes = &registry });
const java_source = try ir.jni_java_emit.emitJavaSource(self.allocator, fcd, .{
.classes = &registry,
.lib_name = lib_name,
});
try self.lowering_jni_main_decls.append(self.allocator, .{
.foreign_path = try self.allocator.dupe(u8, fcd.foreign_path),
.java_source = java_source,
@@ -261,6 +270,16 @@ pub const Compilation = struct {
}
}
/// `/path/to/libfoo.so` → `foo`. Anything else → null (caller skips
/// emitting the `System.loadLibrary` init block).
fn libNameFromOutputPath(output_path: ?[]const u8) ?[]const u8 {
const path = output_path orelse return null;
const basename = std.fs.path.basename(path);
if (!std.mem.startsWith(u8, basename, "lib")) return null;
if (!std.mem.endsWith(u8, basename, ".so")) return null;
return basename[3 .. basename.len - 3];
}
/// Java sources rendered from `#jni_main #jni_class("...")` decls during
/// lowering. Empty unless `lowerToIR` has run.
pub fn getJniMainEmissions(self: *const Compilation) []const JniMainEmission {

View File

@@ -235,6 +235,40 @@ test "default-package class (no slash in foreign_path)" {
try std.testing.expect(std.mem.indexOf(u8, out, "public class SxNoPackage") != null);
}
test "lib_name renders System.loadLibrary static init block" {
const a = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const aa = arena.allocator();
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
const body = try makeBodyMarker(aa);
const method: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = body,
} };
const fcd: ast.ForeignClassDecl = .{
.name = "SxApp",
.foreign_path = "co/example/SxApp",
.runtime = .jni_class,
.is_main = true,
.members = &.{method},
};
const out = try emit.emitJavaSource(a, &fcd, .{ .lib_name = "sxchess" });
defer a.free(out);
try std.testing.expect(std.mem.indexOf(u8, out, "static { System.loadLibrary(\"sxchess\"); }") != null);
// Without lib_name the static init is omitted.
const out2 = try emit.emitJavaSource(a, &fcd, .{});
defer a.free(out2);
try std.testing.expect(std.mem.indexOf(u8, out2, "System.loadLibrary") == null);
}
test "#implements clauses on the class header" {
const a = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(a);

View File

@@ -47,6 +47,12 @@ pub const Options = struct {
/// Activities — `NativeActivity` is the legacy NDK path that
/// requires native_app_glue's `ANativeActivity_onCreate`.
default_extends: []const u8 = "android.app.Activity",
/// `System.loadLibrary(...)` argument for the emitted static init
/// block. When set, the emitter inserts `static { System.loadLibrary
/// (lib_name); }` so JNI native delegates can resolve at runtime.
/// When null, no static init is emitted (caller must arrange .so
/// loading some other way — e.g. another class's static init).
lib_name: ?[]const u8 = null,
};
/// Emit a `.java` source for the given foreign-class decl. Result is
@@ -113,8 +119,14 @@ pub fn emitJavaSource(
try buf.appendSlice(allocator, " {\n");
// Two passes: @Override stubs that call super + native delegate,
// then the native declarations.
if (opts.lib_name) |ln| {
try buf.appendSlice(allocator, " static { System.loadLibrary(\"");
try buf.appendSlice(allocator, ln);
try buf.appendSlice(allocator, "\"); }\n");
}
// Two passes: @Override stubs + native delegate; then the native
// declarations.
for (fcd.members) |m| switch (m) {
.method => |md| {
if (md.body == null) continue;