From bce5448fe9b95e81c92d361c30c3962e6a6eef40 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 20 May 2026 15:38:05 +0300 Subject: [PATCH] ffi #jni_main: emit `implements` clauses from `#implements Alias;` members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `jni_java_emit` previously dropped `#implements` members on the floor. They now compose into the Java class header — first one prefixed with ` implements `, subsequent ones comma-separated. Aliases resolve through the class registry just like `#extends`: an unmapped alias passes through verbatim (handy for built-in JVM interfaces like `java.lang.Runnable` without declaring a `#jni_class` for them). First building block of the chess-on-Pixel migration: the new Activity needs `implements android.view.SurfaceHolder$Callback` to receive surfaceCreated / surfaceChanged / surfaceDestroyed callbacks from the SurfaceView it hosts. Unit test locks in both the registry-resolved and pass-through paths. 131 host / zig build test green. --- src/ir/jni_java_emit.test.zig | 41 +++++++++++++++++++++++++++++++++++ src/ir/jni_java_emit.zig | 21 ++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/ir/jni_java_emit.test.zig b/src/ir/jni_java_emit.test.zig index b5fad9f..912f683 100644 --- a/src/ir/jni_java_emit.test.zig +++ b/src/ir/jni_java_emit.test.zig @@ -235,3 +235,44 @@ test "default-package class (no slash in foreign_path)" { 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 "#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 = ®istry }); + 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); +} diff --git a/src/ir/jni_java_emit.zig b/src/ir/jni_java_emit.zig index 6e703ef..c133645 100644 --- a/src/ir/jni_java_emit.zig +++ b/src/ir/jni_java_emit.zig @@ -90,6 +90,27 @@ pub fn emitJavaSource( try buf.appendSlice(allocator, parts.cls); try buf.appendSlice(allocator, " extends "); try buf.appendSlice(allocator, parent); + + // `#implements Alias;` body items become Java `implements` clauses on the + // class header. Aliases resolve through the class registry the same way + // `#extends` does — an unmapped alias passes through verbatim (useful for + // referring to built-in JVM interfaces without declaring them). + var first_iface = true; + for (fcd.members) |m| switch (m) { + .implements => |alias| { + try buf.appendSlice(allocator, if (first_iface) " implements " else ", "); + first_iface = false; + if (opts.classes) |reg| { + if (reg.get(alias)) |path| { + try appendDotted(allocator, &buf, path); + continue; + } + } + try buf.appendSlice(allocator, alias); + }, + else => {}, + }; + try buf.appendSlice(allocator, " {\n"); // Two passes: @Override stubs that call super + native delegate,