// Java source emission for `#jni_main #jni_class("...") { ... }` decls // (FFI plan, #jni_main pipeline slice 1). // // Given a `ForeignClassDecl` whose `is_main` flag is set, emit a `.java` // source file that: // // - declares a `public class` at the foreign_path's package + simple // name (e.g. `co/swipelab/Test/SxTestActivity` → // `package co.swipelab.Test; public class SxTestActivity`); // - extends the parent specified by `#extends Alias` (or // `android.app.NativeActivity` by default for a #jni_main class); // - for each method with a body, emits an `@Override` Java method // that calls `super` then a private native delegate `sx_`; // - emits the matching `private native ... sx_(...)` decls. // // The downstream pipeline (slice 2+) feeds this through `javac` + `d8` // and bundles the resulting `.dex` into the APK. Slice 3 wires the // manifest's `` to point at this class. // Slice 4 emits a synthetic `JNI_OnLoad` that calls `RegisterNatives` // to bind the `sx_` symbols. // // Type matrix covered today: // - void return + primitive returns (s8/s16/s32/s64, u8/u16, bool, // f32/f64) // - `(self: *Self)` plus primitive params // - cross-class refs (`*Foo` where Foo is another declared // `#foreign #jni_class`) lower to Foo's foreign path → Java // fully-qualified type // - `*void` → `Object` (opaque jobject) const std = @import("std"); const ast = @import("../ast.zig"); const Allocator = std.mem.Allocator; pub const EmitError = error{ OutOfMemory, UnsupportedType, NotAJniMainClass, }; pub const Options = struct { /// Map from sx alias → foreign path of declared `#jni_class` decls. /// Used to resolve `*Foo` cross-class refs in method signatures. classes: ?*const std.StringHashMap([]const u8) = null, /// Default superclass when the user doesn't write `#extends ...;`. /// `android.app.Activity` is the standard base for Java-driven /// 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, }; /// Inject a `static { System.loadLibrary(""); }` block into an already- /// rendered Java source. Used when the output path isn't known until after /// `#run` blocks execute — `collectJniMainEmissions` runs during lowering, /// before `BuildOptions.set_output_path(...)` has populated the lib name. /// Returns a newly-allocated string; caller owns it. pub fn injectLoadLibrary(allocator: Allocator, java_source: []const u8, lib_name: []const u8) ![]u8 { const marker = " {\n"; const class_pos = std.mem.indexOf(u8, java_source, "public class ") orelse return try allocator.dupe(u8, java_source); const brace_rel = std.mem.indexOf(u8, java_source[class_pos..], marker) orelse return try allocator.dupe(u8, java_source); const insert_at = class_pos + brace_rel + marker.len; // Already injected? Skip. if (std.mem.indexOf(u8, java_source, "System.loadLibrary(") != null) { return try allocator.dupe(u8, java_source); } var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(allocator); try buf.appendSlice(allocator, java_source[0..insert_at]); try buf.appendSlice(allocator, " static { System.loadLibrary(\""); try buf.appendSlice(allocator, lib_name); try buf.appendSlice(allocator, "\"); }\n"); try buf.appendSlice(allocator, java_source[insert_at..]); return try buf.toOwnedSlice(allocator); } /// Emit a `.java` source for the given foreign-class decl. Result is /// heap-allocated through `allocator`; caller owns it. pub fn emitJavaSource( allocator: Allocator, fcd: *const ast.ForeignClassDecl, opts: Options, ) EmitError![]u8 { if (!fcd.is_main) return EmitError.NotAJniMainClass; var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(allocator); const parts = splitForeignPath(fcd.foreign_path); if (parts.pkg.len > 0) { try buf.appendSlice(allocator, "package "); try appendDotted(allocator, &buf, parts.pkg); try buf.appendSlice(allocator, ";\n\n"); } var parent: []const u8 = opts.default_extends; var parent_owned = false; for (fcd.members) |m| switch (m) { .extends => |alias| { if (opts.classes) |reg| { if (reg.get(alias)) |path| { parent = try foreignPathToJavaName(allocator, path); parent_owned = true; break; } } parent = alias; break; }, else => {}, }; defer if (parent_owned) allocator.free(parent); try buf.appendSlice(allocator, "public class "); 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"); if (opts.lib_name) |ln| { try buf.appendSlice(allocator, " static { System.loadLibrary(\""); try buf.appendSlice(allocator, ln); try buf.appendSlice(allocator, "\"); }\n"); } // Fields. `name: Type;` body items render as private Java fields — // primitive types pass through, pointer types resolve to fully // qualified Java class names via the class registry. for (fcd.members) |m| switch (m) { .field => |fd| { try buf.appendSlice(allocator, " private "); try emitJavaType(allocator, &buf, fd.field_type, opts); try buf.append(allocator, ' '); try buf.appendSlice(allocator, fd.name); try buf.appendSlice(allocator, ";\n"); }, else => {}, }; // Two passes: @Override stubs + native delegate; then the native // declarations. for (fcd.members) |m| switch (m) { .method => |md| { if (md.body == null) continue; if (md.is_static) continue; // TODO: static native handling try emitOverride(allocator, &buf, md, opts); }, else => {}, }; for (fcd.members) |m| switch (m) { .method => |md| { if (md.body == null) continue; if (md.is_static) continue; try emitNativeDecl(allocator, &buf, md, opts); }, else => {}, }; try buf.appendSlice(allocator, "}\n"); return buf.toOwnedSlice(allocator); } const PathParts = struct { pkg: []const u8, cls: []const u8 }; fn splitForeignPath(foreign_path: []const u8) PathParts { const last_slash = std.mem.lastIndexOfScalar(u8, foreign_path, '/') orelse { return .{ .pkg = "", .cls = foreign_path }; }; return .{ .pkg = foreign_path[0..last_slash], .cls = foreign_path[last_slash + 1 ..], }; } fn appendDotted( allocator: Allocator, buf: *std.ArrayList(u8), slash_path: []const u8, ) EmitError!void { // `/` and `$` both become `.` in Java source: `android/view/SurfaceHolder$Callback` // → `android.view.SurfaceHolder.Callback`. The `$` form is the JNI-descriptor // / class-file shape for nested classes; Java source uses `.` for both. for (slash_path) |c| { try buf.append(allocator, if (c == '/' or c == '$') '.' else c); } } fn foreignPathToJavaName(allocator: Allocator, slash_path: []const u8) EmitError![]u8 { var buf: std.ArrayList(u8) = .empty; try appendDotted(allocator, &buf, slash_path); return buf.toOwnedSlice(allocator); } fn emitOverride( allocator: Allocator, buf: *std.ArrayList(u8), md: ast.ForeignMethodDecl, opts: Options, ) EmitError!void { // The Java @Override only calls the native delegate. `super.(...)` // is NOT injected — if the user wants to invoke the supertype's // implementation (e.g. `super.onCreate(b)` for an Activity lifecycle hook // that requires it) they call super from the sx-side body via JNI // dispatch. This keeps the emitter free of "is this an interface method // or a supertype override?" guesswork and matches the sx principle of // user-space code expressing the dispatch. try buf.appendSlice(allocator, " @Override\n public "); try emitJavaReturnType(allocator, buf, md.return_type, opts); try buf.append(allocator, ' '); try buf.appendSlice(allocator, md.name); try buf.append(allocator, '('); try emitJavaParamList(allocator, buf, md, opts); // Non-void return types `return` the native delegate's result; void // returns just call it. The user's sx-side body decides what to // return — the Java side is a pass-through. const has_ret = md.return_type != null; try buf.appendSlice(allocator, ") {\n "); if (has_ret) try buf.appendSlice(allocator, "return "); try buf.appendSlice(allocator, "sx_"); try buf.appendSlice(allocator, md.name); try buf.append(allocator, '('); try emitJavaArgList(allocator, buf, md); try buf.appendSlice(allocator, ");\n }\n"); } fn emitNativeDecl( allocator: Allocator, buf: *std.ArrayList(u8), md: ast.ForeignMethodDecl, opts: Options, ) EmitError!void { try buf.appendSlice(allocator, " private native "); try emitJavaReturnType(allocator, buf, md.return_type, opts); try buf.appendSlice(allocator, " sx_"); try buf.appendSlice(allocator, md.name); try buf.append(allocator, '('); try emitJavaParamList(allocator, buf, md, opts); try buf.appendSlice(allocator, ");\n"); } fn emitJavaReturnType( allocator: Allocator, buf: *std.ArrayList(u8), ret: ?*ast.Node, opts: Options, ) EmitError!void { if (ret == null) { try buf.appendSlice(allocator, "void"); return; } try emitJavaType(allocator, buf, ret.?, opts); } fn emitJavaParamList( allocator: Allocator, buf: *std.ArrayList(u8), md: ast.ForeignMethodDecl, opts: Options, ) EmitError!void { const start: usize = if (md.is_static) 0 else 1; // skip self for (md.params[start..], 0..) |p, i| { if (i > 0) try buf.appendSlice(allocator, ", "); try emitJavaType(allocator, buf, p, opts); try buf.append(allocator, ' '); try buf.appendSlice(allocator, md.param_names[start + i]); } } fn emitJavaArgList( allocator: Allocator, buf: *std.ArrayList(u8), md: ast.ForeignMethodDecl, ) EmitError!void { const start: usize = if (md.is_static) 0 else 1; for (md.param_names[start..], 0..) |name, i| { if (i > 0) try buf.appendSlice(allocator, ", "); try buf.appendSlice(allocator, name); } } fn emitJavaType( allocator: Allocator, buf: *std.ArrayList(u8), type_node: *ast.Node, opts: Options, ) EmitError!void { switch (type_node.data) { .type_expr => |te| { const name = javaPrimitiveName(te.name) orelse return EmitError.UnsupportedType; try buf.appendSlice(allocator, name); }, .pointer_type_expr => |ptr| { const inner = ptr.pointee_type; if (inner.data != .type_expr) return EmitError.UnsupportedType; const target_name = inner.data.type_expr.name; if (std.mem.eql(u8, target_name, "void")) { try buf.appendSlice(allocator, "Object"); return; } if (opts.classes) |reg| { if (reg.get(target_name)) |path| { try appendDotted(allocator, buf, path); return; } } // Unknown alias — pass through dotted as best-effort. Sema // should catch this earlier; tolerate for now. try appendDotted(allocator, buf, target_name); }, else => return EmitError.UnsupportedType, } } fn javaPrimitiveName(name: []const u8) ?[]const u8 { if (std.mem.eql(u8, name, "void")) return "void"; if (std.mem.eql(u8, name, "bool")) return "boolean"; if (std.mem.eql(u8, name, "s8")) return "byte"; if (std.mem.eql(u8, name, "u8")) return "byte"; if (std.mem.eql(u8, name, "s16")) return "short"; if (std.mem.eql(u8, name, "u16")) return "char"; if (std.mem.eql(u8, name, "s32")) return "int"; if (std.mem.eql(u8, name, "s64")) return "long"; if (std.mem.eql(u8, name, "f32")) return "float"; if (std.mem.eql(u8, name, "f64")) return "double"; return null; }