ffi #jni_main slice 2: AOT pipeline — .java + javac + d8 → classes.dex in APK

Compilation.lowering_jni_main_decls is populated by lowerToIR (iterating
foreign_class_map for is_main && !is_foreign && runtime==jni_class,
deduped by foreign_path); each entry carries the pre-rendered Java source
from jni_java_emit.emitJavaSource.

createApk extended: when the emission list is non-empty, write each
.java under <stage>/java/<pkg>/<Class>.java, javac --release 11 to
<stage>/classes/, d8 --release --lib <android_jar> --output <stage>
to produce <stage>/classes.dex, then zip the .dex into the unaligned
APK at root level. javac discovery: $JAVA_HOME/bin/javac first, then
`which javac`.

Manifest still hardcodes android.app.NativeActivity (slice 3 wires the
user's class name + android:hasCode="true"), so the bundled .dex is
present but unreferenced at runtime. End-to-end verified via dexdump on
the smoke example's APK — Lco/swipelab/sxjnimain/SxApp; extending
NativeActivity shows up in classes.dex. Non-#jni_main APK builds
(99-android-egl-clear.sx) produce the same shape as before.

Cross-compile tuple added for examples/ffi-jni-main-01-emit.sx
(compile-only — APK exercise is manual).
This commit is contained in:
agra
2026-05-20 14:42:03 +03:00
parent 7ea7ad778e
commit 1ae43495c2
7 changed files with 236 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ const target_mod = @import("target.zig");
const Node = ast.Node;
pub const TargetConfig = target_mod.TargetConfig;
pub const JniMainEmission = target_mod.JniMainEmission;
pub const Compilation = struct {
allocator: std.mem.Allocator,
@@ -32,6 +33,10 @@ pub const Compilation = struct {
/// E.g. the JNI env TL runtime when `#jni_env` is used. Merged with
/// AST sources in `collectCImportSources`.
lowering_extra_c_sources: std.ArrayList(c_import.CImportInfo) = .empty,
/// `#jni_main #jni_class("...")` declarations whose Java sources were
/// rendered during lowering. Read by the APK pipeline (`createApk`)
/// to write `.java` files + run `javac` + `d8` + bundle `classes.dex`.
lowering_jni_main_decls: std.ArrayList(JniMainEmission) = .empty,
pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, target_config: TargetConfig, stdlib_paths: []const []const u8) Compilation {
return .{
@@ -215,9 +220,53 @@ pub const Compilation = struct {
}
}
try self.collectJniMainEmissions(&lowering);
return module;
}
/// Walk `lowering.foreign_class_map` and render Java sources for every
/// `#jni_main #jni_class("...")` declaration. Renders happen here so the
/// AST + class-registry snapshot stay confined to the lowering pass; the
/// downstream APK pipeline only needs `{foreign_path, java_source}` pairs.
fn collectJniMainEmissions(self: *Compilation, lowering: *ir.Lowering) !void {
// `foreign_class_map` registers each decl under bare + qualified names —
// dedupe by foreign_path so a single decl emits one .java.
var seen = std.StringHashMap(void).init(self.allocator);
defer seen.deinit();
// Class registry passed to jni_java_emit for `*Foo` cross-class refs
// and `#extends Alias` resolution.
var registry = std.StringHashMap([]const u8).init(self.allocator);
defer registry.deinit();
var it_reg = lowering.foreign_class_map.iterator();
while (it_reg.next()) |entry| {
try registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path);
}
var it = lowering.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;
try seen.put(fcd.foreign_path, {});
const java_source = try ir.jni_java_emit.emitJavaSource(self.allocator, fcd, .{ .classes = &registry });
try self.lowering_jni_main_decls.append(self.allocator, .{
.foreign_path = try self.allocator.dupe(u8, fcd.foreign_path),
.java_source = java_source,
});
}
}
/// Java sources rendered from `#jni_main #jni_class("...")` decls during
/// lowering. Empty unless `lowerToIR` has run.
pub fn getJniMainEmissions(self: *const Compilation) []const JniMainEmission {
return self.lowering_jni_main_decls.items;
}
pub fn renderErrors(self: *const Compilation) void {
self.diagnostics.renderDebug();
}