Files
sx/src/ir/jni_java_emit.test.zig
agra d43f21f39e 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.
2026-05-20 16:45:41 +03:00

312 lines
11 KiB
Zig

// Tests for jni_java_emit.zig — #jni_main pipeline slice 1.
// Locks in the Java source emitted from `ForeignClassDecl` AST nodes:
// package split, class header, @Override delegate pattern, primitive
// type mapping, cross-class refs through the foreign_class registry.
const std = @import("std");
const ast = @import("../ast.zig");
const emit = @import("jni_java_emit.zig");
const Node = ast.Node;
fn makeTypeExpr(allocator: std.mem.Allocator, name: []const u8) !*Node {
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .type_expr = .{ .name = name } },
};
return node;
}
fn makePointer(allocator: std.mem.Allocator, pointee: *Node) !*Node {
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .pointer_type_expr = .{ .pointee_type = pointee } },
};
return node;
}
/// Marker for "method has a body" — emitJavaSource only checks
/// `body != null`. The actual node contents are unused.
fn makeBodyMarker(allocator: std.mem.Allocator) !*Node {
const node = try allocator.create(Node);
node.* = .{
.span = .{ .start = 0, .end = 0 },
.data = .{ .block = .{ .stmts = &.{} } },
};
return node;
}
test "rejects non-main decl" {
const a = std.testing.allocator;
const fcd: ast.ForeignClassDecl = .{
.name = "Foo",
.foreign_path = "co/example/Foo",
.runtime = .jni_class,
.is_main = false, // ← not main
};
const result = emit.emitJavaSource(a, &fcd, .{});
try std.testing.expectError(emit.EmitError.NotAJniMainClass, result);
}
test "void onCreate(Bundle) with default Activity superclass" {
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 bundle_ty = try makePointer(aa, try makeTypeExpr(aa, "Bundle"));
const body = try makeBodyMarker(aa);
var registry = std.StringHashMap([]const u8).init(a);
defer registry.deinit();
try registry.put("Bundle", "android/os/Bundle");
const member: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{ self_ty, bundle_ty },
.param_names = &.{ "self", "b" },
.return_type = null,
.body = body,
} };
const fcd: ast.ForeignClassDecl = .{
.name = "SxApp",
.foreign_path = "co/swipelab/sx_runtime/SxNativeActivity",
.runtime = .jni_class,
.is_main = true,
.members = &.{member},
};
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = &registry });
defer a.free(out);
const expected =
\\package co.swipelab.sx_runtime;
\\
\\public class SxNativeActivity extends android.app.Activity {
\\ @Override
\\ public void onCreate(android.os.Bundle b) {
\\ sx_onCreate(b);
\\ }
\\ private native void sx_onCreate(android.os.Bundle b);
\\}
\\
;
try std.testing.expectEqualStrings(expected, out);
}
test "primitive params" {
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 bool_ty = try makeTypeExpr(aa, "bool");
const body = try makeBodyMarker(aa);
const member: ast.ForeignClassMember = .{ .method = .{
.name = "onWindowFocusChanged",
.params = &.{ self_ty, bool_ty },
.param_names = &.{ "self", "hasFocus" },
.return_type = null,
.body = body,
} };
const fcd: ast.ForeignClassDecl = .{
.name = "Sx",
.foreign_path = "co/sample/Sx",
.runtime = .jni_class,
.is_main = true,
.members = &.{member},
};
const out = try emit.emitJavaSource(a, &fcd, .{});
defer a.free(out);
try std.testing.expect(std.mem.indexOf(u8, out, "public void onWindowFocusChanged(boolean hasFocus)") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "super.onWindowFocusChanged") == null); // emitter never injects super
try std.testing.expect(std.mem.indexOf(u8, out, "sx_onWindowFocusChanged(hasFocus);") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "private native void sx_onWindowFocusChanged(boolean hasFocus);") != null);
}
test "declaration-only methods are skipped" {
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);
// One bodied (override), one declaration-only (calls inherited).
const bodied: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = body,
} };
const decl_only: ast.ForeignClassMember = .{ .method = .{
.name = "finish",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = null, // sx-side just *calls* this; Java's NativeActivity.finish() provides it
} };
const fcd: ast.ForeignClassDecl = .{
.name = "Sx",
.foreign_path = "co/example/Sx",
.runtime = .jni_class,
.is_main = true,
.members = &.{ bodied, decl_only },
};
const out = try emit.emitJavaSource(a, &fcd, .{});
defer a.free(out);
try std.testing.expect(std.mem.indexOf(u8, out, "sx_onCreate") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "sx_finish") == null);
try std.testing.expect(std.mem.indexOf(u8, out, "void finish(") == null);
}
test "#extends Alias resolves through class registry" {
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 extends_member: ast.ForeignClassMember = .{ .extends = "MyParent" };
const method_member: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = body,
} };
var registry = std.StringHashMap([]const u8).init(a);
defer registry.deinit();
try registry.put("MyParent", "co/example/MyParentActivity");
const fcd: ast.ForeignClassDecl = .{
.name = "Sx",
.foreign_path = "co/example/Sx",
.runtime = .jni_class,
.is_main = true,
.members = &.{ extends_member, method_member },
};
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = &registry });
defer a.free(out);
try std.testing.expect(std.mem.indexOf(u8, out, "extends co.example.MyParentActivity") != null);
}
test "default-package class (no slash in foreign_path)" {
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 member: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = body,
} };
const fcd: ast.ForeignClassDecl = .{
.name = "Sx",
.foreign_path = "SxNoPackage",
.runtime = .jni_class,
.is_main = true,
.members = &.{member},
};
const out = try emit.emitJavaSource(a, &fcd, .{});
defer a.free(out);
// No `package ...;` line when the foreign path has no slashes.
try std.testing.expect(std.mem.indexOf(u8, out, "package ") == null);
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);
defer arena.deinit();
const aa = arena.allocator();
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
const body = try makeBodyMarker(aa);
// Two interfaces: one resolvable via the registry, one passed through verbatim.
const impl_a: ast.ForeignClassMember = .{ .implements = "Callback" };
const impl_b: ast.ForeignClassMember = .{ .implements = "java.lang.Runnable" };
const method: ast.ForeignClassMember = .{ .method = .{
.name = "onCreate",
.params = &.{self_ty},
.param_names = &.{"self"},
.return_type = null,
.body = body,
} };
var registry = std.StringHashMap([]const u8).init(a);
defer registry.deinit();
try registry.put("Callback", "android/view/SurfaceHolder$Callback");
const fcd: ast.ForeignClassDecl = .{
.name = "SxApp",
.foreign_path = "co/example/SxApp",
.runtime = .jni_class,
.is_main = true,
.members = &.{ impl_a, impl_b, method },
};
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = &registry });
defer a.free(out);
try std.testing.expect(std.mem.indexOf(
u8,
out,
"public class SxApp extends android.app.Activity implements android.view.SurfaceHolder$Callback, java.lang.Runnable {",
) != null);
}