Commit Graph

8 Commits

Author SHA1 Message Date
agra
632e64512b bundling: Android APK pipeline moved into sx; android.sx state-on-plat
Week 7 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
plus the android.sx refactor + three sx-compiler fixes hit along the way
to get chess on Pixel 7 Pro responding to touch end-to-end.

library/modules/platform/bundle.sx now covers the Android APK shape
alongside macOS / iOS-sim / iOS-device. `android_bundle_main` discovers
the SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT / $HOME/Library/Android/sdk),
picks the highest-versioned build-tools + platforms via
`process.run("ls .. | sort -V | tail -1")`, stages
`<apk>.stage/lib/arm64-v8a/<libfoo.so>`, synthesizes
AndroidManifest.xml (NativeActivity vs `#jni_main` Activity branch),
writes each `#jni_main` decl's Java source under
`<stage>/java/<pkg>/<Cls>.java`, runs javac --release 11 + d8 to
produce classes.dex, aapt2-links the unaligned APK, appends lib/ +
classes.dex + each registered asset tree via zip, zipalign + ensure
debug keystore via keytool + apksigner sign.

Compiler-side accessors (src/ir/compiler_hooks.zig + library/modules/compiler.sx):
- is_android predicate.
- set_manifest_path / manifest_path + set_keystore_path / keystore_path.
- jni_main_count / jni_main_foreign_path_at(i) /
  jni_main_java_source_at(i) surface the `#jni_main` emissions that
  the Zig createApk previously consumed directly.
- main.zig wires manifest_path, keystore_path, and the per-decl
  (foreign_path, java_source) parallel slices into BuildConfig before
  invoking the post-link callback.

CLI `--apk <path>` keeps working as a transitional alias: it now feeds
bundle_path so the existing auto-`post_link_module = "platform.bundle"`
shim fires the same way as `--bundle`. main.zig no longer calls
target.createApk directly.

Deletions in src/target.zig: createApk, compileJniMainSources,
buildJniMainManifest, buildAndroidManifest, ensureDebugKeystore,
libNameFromSoBasename, plus helpers splitForeignPath / discoverJavac /
discoverAndroidSdk / findHighestSubdir / runProcess / runProcessIn
(~400 lines). git grep returns only the obituary comment.

library/modules/platform/android.sx refactor (chess Android dependency):
- Module-level globals retired (g_app_window, g_egl_*, g_viewport_*,
  g_dpi_scale, g_should_stop, g_render_thread*, g_user_main_fn,
  g_touch_*) → AndroidPlatform struct fields.
- All sx_android_* helpers take `plat: *AndroidPlatform` as first arg.
  Render thread receives plat via pthread_create's arg.
- New `logical_w: f32 = 0.0` field. Consumers set it before init() to
  define the design width in points; `recompute_scale` derives
  `dpi_scale = pixel_w / logical_w` (or 1.0 if unset). Called on
  init / set_viewport / egl_init. drain_touches divides incoming
  physical pixel coords by dpi_scale so chess sees logical-space
  positions matching its layout. Touch lands on the right squares.

Three sx-compiler bugs hit + fixed along the way:

1. Top-level `inline if OS == .X { decls }` body decls were silently
   dropped because scanDecls/lowerDecls had no .if_expr arm. New
   `flattenComptimeConditionals` pre-pass in src/imports.zig
   (threaded via ComptimeContext from core.zig) hoists matching arms
   recursively. Regression at examples/124-inline-if-hoist-toplevel.sx.

2. Parser rejected `#import` / `#framework` inside inline-if bodies
   because parseStmt in src/parser.zig only had arms for `#insert`.
   Added the missing arms. Regression at
   examples/123-inline-if-import-in-body.sx (landed earlier).

3. JNI `Call<T>Method` switches in src/ir/emit_llvm.zig (instance /
   nonvirtual / static) were missing `.f32` rows — jfloat returns
   (e.g. MotionEvent.getX/getY) fell into the silent-undef else arm.
   Chess's sx_android_push_touch(plat, getAction(), getX(), getY())
   delivered garbage f32 coords to the touch ring, so taps landed
   nowhere recognisable. Added `.f32 => Jni.Call{Static,Nonvirtual,}FloatMethod`
   rows to all three switches; lifted unsupported-type detection
   from emit_llvm into lowerForeignMethodCall with proper
   source-spanned diagnostics (`isJniReturnTypeSupported`). Regressions
   at examples/ffi-jni-call-10-jfloat-return.sx,
   examples/ffi-jni-class-09-multi-float-args.sx,
   examples/ffi-jni-call-11-unsupported-return-diag.sx.

Stale-snapshot drift in tests/expected/ffi-objc-call-03-selector-sharing.ir
and ffi-objc-call-06-sret-return.ir picks up the new BuildOptions
accessor extern decls (is_android, set_manifest_path,
set_keystore_path, jni_main_count, jni_main_foreign_path_at,
jni_main_java_source_at). Verified diff is dead-decl-only.

Chess on Pixel 7 Pro: tap on e2 white pawn -> yellow selection +
green dots on legal e3/e4 targets; tap on e4 -> board updates with
1. e4, "Black to move" + "1. e4" in info panel.

zig build && zig build test && bash tests/run_examples.sh -> 145/145
green. bash tests/cross_compile.sh -> 7/7 green.
2026-05-23 01:28:32 +03:00
agra
cc29cfa7ce ffi #jni_main: jni_java_emit + android.sx + manifest fixes; chess on Pixel
Combined slice — gets chess rendering on a Pixel 7 Pro via the
`#jni_main` pipeline. Half-dozen jni_java_emit fixes plus the rebuilt
stdlib android module:

  jni_java_emit:
    - `#implements Alias;` body members render as Java `implements`
      clauses on the class header (space-separated, registry-resolved).
    - Drop the implicit `super.<method>(args)` call from the @Override
      delegate — interface impls (SurfaceHolder.Callback) have no
      super; user calls super explicitly from sx-side via
      `super.method(args)` lowered to `CallNonvirtual<T>Method`.
    - `static { System.loadLibrary("<libname>"); }` static init block,
      lib name derived from the build's `-o` basename.
    - `name: Type;` body items render as private Java fields.
    - `$` (JNI nested-class shape) → `.` in Java source: e.g.
      `android/view/SurfaceHolder$Callback` → `android.view.SurfaceHolder.Callback`.
    - Non-void @Override bodies `return` the native delegate's result.

  lower.zig:
    - `super.method(args)` sugar inside a `#jni_main` (or any
      sx-defined `#jni_class`) bodied method lowers to JNI
      `CallNonvirtual<T>Method` with the parent class resolved via
      `#extends` (default Activity).
    - `Alias.new(args)` constructor sugar lowers to JNI
      `FindClass + GetMethodID("<init>", sig) + NewObject`.
    - `jniMapParamType` stops erasing pointer types so method dispatch
      on foreign-class params (`holder.getSurface()`) resolves.
    - synthesizeJniMainStub pushes the env arg onto the lexical
      `#jni_env` stack so omitted-env `#jni_call` and `super.method`
      sites see it.

  target.zig:
    - Manifest synthesised from `#jni_main` adds
      `android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"`
      so sx apps own the whole window (no title strip, no status bar).

  library/modules/platform/android.sx (NEW):
    - Replaces the retired NativeActivity-based module under #jni_main.
    - Foreign-class decls for Bundle / Context / Surface / SurfaceHolder
      / SurfaceView / MotionEvent / View / Activity / SurfaceHolderCallback /
      AssetManagerJ.
    - libandroid / EGL / pthread foreign C decls.
    - Helpers consumers call from their Activity body:
      `sx_android_forward_assets(env, ctx)`,
      `sx_android_attach_window(env, holder)`,
      `sx_android_detach_window()`,
      `sx_android_set_viewport(w, h)`,
      `sx_android_start_render_thread(main_fn)`,
      `sx_android_push_touch(action, x, y)`.
    - Render thread brings up EGL on the ANativeWindow then calls the
      user-supplied entry fn pointer.
    - `AndroidPlatform` struct + `impl Platform` (init / begin_frame /
      end_frame / poll_events / safe_insets / keyboard / show_keyboard /
      hide_keyboard / stop / shutdown / run_frame_loop).

End-to-end verified on a Pixel 7 Pro: chess APK builds via
`sx build --target android --apk ... --bundle-id ... -o ...`, installs
via `adb install -r`, launches and renders the chess board with all
pieces in starting position. No title strip, no flicker. Touch events
reach `sx_android_push_touch` and drain through `poll_events` (debug-
verified) — chess's pipeline-side hit-test routing + DPI-correct
sizing remain as follow-ups.

138 host / 8 cross / `zig build test` all green.
2026-05-20 19:50:25 +03:00
agra
36f40057f7 ffi #jni_main: emit name: Type; body members as private Java fields
`#jni_class` body items of the form `name: Type;` were parsed into
`ForeignFieldDecl` but dropped by jni_java_emit. They now render as
private Java fields between the static init block and the @Override
delegates, using the same primitive / `*Foo` → fully-qualified-name
type mapping as method parameters.

Needed for the chess-on-Pixel `SxApp` Activity to hold its
`SurfaceView` reference: `view: SurfaceView;` → `private
android.view.SurfaceView view;`.
2026-05-20 17:01:24 +03:00
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
agra
22768d9adf ffi #jni_main: drop implicit super call from @Override delegate
The Java @Override no longer injects a `super.<method>(...)` call before
the native delegate. The user calls super from the sx-side body when
needed — via a forthcoming `super.method(args)` dispatch lowered to
`CallNonvirtual<Type>Method` on JNI classes.

Two reasons:

  - Interface method impls (e.g. SurfaceHolder.Callback) have no super
    to call. The previous emit produced javac-rejected code for those.
  - Lifecycle overrides may want to skip super in some cases, or call
    it with different args. The emitter can't second-guess intent.

User-space control of the dispatch keeps the emitter free of "is this
an interface method or a supertype override?" guesswork. The dex
shrinks by one virtual-method bytecode invocation per override.

Caveat: until the sx-side `super.method(args)` dispatch lands, Activity
lifecycle methods (onCreate, onResume, etc.) that mandate `super.<>`
will throw `SuperNotCalledException` at runtime if their bodies don't
do their own JNI dispatch. The slice 2 smoke still launches cleanly
because its onCreate body is empty.

131 host / 4 cross / zig build test all green.
2026-05-20 16:41:13 +03:00
agra
bce5448fe9 ffi #jni_main: emit implements clauses from #implements Alias; members
`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.
2026-05-20 15:38:05 +03:00
agra
8ae4e0c653 ffi #jni_main R.1: manifest synthesis + default parent → android.app.Activity
When `Compilation.lowering_jni_main_decls` is non-empty, `createApk`
synthesises a manifest whose `<activity android:name>` points at the
user's `#jni_main` class (dotted form of the foreign path), sets
`android:hasCode="true"` so Android loads the bundled classes.dex, and
drops the `android.app.lib_name` meta-data (that's the NativeActivity-
specific autoload mechanism — Java-driven Activities load the .so via
`System.loadLibrary` from a Java static initializer slice R.3 will
emit). The legacy NativeActivity path stays as the fallback when no
`#jni_main` decl is present.

`jni_java_emit.zig`'s default superclass moves from
`android.app.NativeActivity` to `android.app.Activity` — the former
requires native_app_glue's `ANativeActivity_onCreate` to be in the .so,
which the next slice (R.2) will stop linking by default.

Verified end-to-end on the slice 2 smoke APK: `aapt2 dump xmltree`
shows `android:name="co.swipelab.sxjnimain.SxApp"` + `hasCode="true"`,
and `dexdump -l plain` confirms SxApp now extends `Landroid/app/Activity;`.
99-android-egl-clear's APK still uses the NativeActivity manifest as
before (legacy path intact for R.2-R.5).
2026-05-20 14:55:26 +03:00
agra
7ea7ad778e ffi #jni_main slice 1: Java source emitter (pure fn + unit tests)
`src/ir/jni_java_emit.zig`'s `emitJavaSource` takes a
`ForeignClassDecl` with `is_main = true` and returns the `.java`
source text. AOT pipeline integration (javac + d8 + APK bundling +
manifest synthesis + RegisterNatives) lands in subsequent slices.

Emission shape per bodied method:

    @Override
    public <ret> <name>(<params>) {
        super.<name>(<args>);
        sx_<name>(<args>);
    }
    private native <ret> sx_<name>(<params>);

Declaration-only methods (no body — references inherited Java
methods that sx wants to *call*) are skipped — no override, no
native delegate.

`#extends Alias` resolves through the supplied class registry to
the parent's foreign Java path. Default parent is
`android.app.NativeActivity` when `#extends` is absent.

Type matrix: primitives (void/bool/s8..s64/u8/u16/f32/f64), `*Self`
elided as the receiver (Java's implicit `this`), `*void` as
`Object`, `*Foo` cross-class refs resolved through the class
registry.

Six unit tests cover: non-main rejection, full void onCreate(Bundle)
shape with @Override delegate, primitive params, declaration-only
skipping, `#extends Alias` resolution, default-package classes.

130/130 examples still green; zig test clean.
2026-05-20 14:16:40 +03:00