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.
This commit is contained in:
397
library/modules/platform/android.sx
Normal file
397
library/modules/platform/android.sx
Normal file
@@ -0,0 +1,397 @@
|
||||
// Android backend driven by a `#jni_main` Activity (no native_app_glue).
|
||||
//
|
||||
// Lifecycle:
|
||||
//
|
||||
// 1. Java `SxApp.onCreate(b)` → native `sx_onCreate`: stash JNIEnv* +
|
||||
// Activity globals, install the AAssetManager into the C file_utils,
|
||||
// construct a `SurfaceView`, register `SxApp` as its
|
||||
// SurfaceHolder.Callback, set as Activity content view.
|
||||
// 2. Java `SxApp.surfaceCreated(holder)` → native `sx_surfaceCreated`:
|
||||
// extract the ANativeWindow from the holder's Surface, then
|
||||
// `pthread_create` the render thread on first delivery.
|
||||
// 3. Render thread: brings up EGL on the ANativeWindow, then calls
|
||||
// `sx_app_main()` — the user's entry-point, which sets up the
|
||||
// AndroidPlatform / GPU / pipeline globals and ends in
|
||||
// `run_frame_loop(closure(frame))`.
|
||||
// 4. `run_frame_loop` drives the loop: drain touch events queue,
|
||||
// invoke `frame_fn`, `eglSwapBuffers`, sleep ~1ms.
|
||||
// 5. Java `onTouchEvent` → native `sx_onTouchEvent`: push the
|
||||
// (action,x,y) tuple onto a mutex-guarded queue. `poll_events`
|
||||
// drains the queue into the platform's standard `Event` shape.
|
||||
//
|
||||
// Vulkan-compatible: same ANativeWindow drives `vkCreate*SurfaceKHR`
|
||||
// without changing the lifecycle.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/compiler.sx";
|
||||
#import "modules/ui/types.sx";
|
||||
#import "modules/ui/events.sx";
|
||||
#import "modules/platform/types.sx";
|
||||
#import "modules/platform/api.sx";
|
||||
|
||||
// ── Foreign Java types ──────────────────────────────────────────────────
|
||||
|
||||
Bundle :: #foreign #jni_class("android/os/Bundle") { }
|
||||
JContext :: #foreign #jni_class("android/content/Context") {
|
||||
getAssets :: (self: *Self) -> *AssetManagerJ;
|
||||
}
|
||||
AssetManagerJ :: #foreign #jni_class("android/content/res/AssetManager") { }
|
||||
|
||||
Surface :: #foreign #jni_class("android/view/Surface") { }
|
||||
SurfaceHolder :: #foreign #jni_class("android/view/SurfaceHolder") {
|
||||
getSurface :: (self: *Self) -> *Surface;
|
||||
addCallback :: (self: *Self, cb: *SurfaceHolderCallback);
|
||||
}
|
||||
SurfaceView :: #foreign #jni_class("android/view/SurfaceView") {
|
||||
static new :: (ctx: *JContext) -> *Self;
|
||||
getHolder :: (self: *Self) -> *SurfaceHolder;
|
||||
}
|
||||
SurfaceHolderCallback :: #foreign #jni_class("android/view/SurfaceHolder$Callback") { }
|
||||
|
||||
MotionEvent :: #foreign #jni_class("android/view/MotionEvent") {
|
||||
getAction :: (self: *Self) -> s32;
|
||||
getX :: (self: *Self) -> f32;
|
||||
getY :: (self: *Self) -> f32;
|
||||
}
|
||||
|
||||
JView :: #foreign #jni_class("android/view/View") { }
|
||||
ActivityClass :: #foreign #jni_class("android/app/Activity") {
|
||||
setContentView :: (self: *Self, v: *JView);
|
||||
}
|
||||
|
||||
// ── Foreign C/NDK decls ─────────────────────────────────────────────────
|
||||
|
||||
// C side of file_utils — installs the AAssetManager so `read_file_bytes`
|
||||
// can route through `AAssetManager_open` when running on Android.
|
||||
sx_android_set_asset_manager :: (mgr: *void) #foreign;
|
||||
|
||||
__android_log_print :: (prio: s32, tag: *u8, fmt: *u8) -> s32 #foreign;
|
||||
usleep :: (us: u32) -> s32 #foreign;
|
||||
|
||||
// libandroid
|
||||
ANativeWindow_fromSurface :: (env: *void, surface: *void) -> *void #foreign;
|
||||
ANativeWindow_release :: (window: *void) #foreign;
|
||||
ANativeWindow_getWidth :: (window: *void) -> s32 #foreign;
|
||||
ANativeWindow_getHeight :: (window: *void) -> s32 #foreign;
|
||||
ANativeWindow_setBuffersGeometry :: (w: *void, width: s32, height: s32, fmt: s32) -> s32 #foreign;
|
||||
|
||||
AAssetManager_fromJava :: (env: *void, mgr: *void) -> *void #foreign;
|
||||
|
||||
// pthread (link libpthread is built into bionic).
|
||||
pthread_create :: (thread: *u64, attr: *void, start: (*void) -> *void, arg: *void) -> s32 #foreign;
|
||||
pthread_mutex_init :: (m: *void, attr: *void) -> s32 #foreign;
|
||||
pthread_mutex_lock :: (m: *void) -> s32 #foreign;
|
||||
pthread_mutex_unlock :: (m: *void) -> s32 #foreign;
|
||||
|
||||
// EGL. Constants from <EGL/egl.h>. We bring up an ES3 context with a
|
||||
// 24-bit RGB framebuffer + 24-bit depth (same shape chess used under
|
||||
// the legacy NDK path).
|
||||
EGL_DEFAULT_DISPLAY :: 0;
|
||||
EGL_NO_DISPLAY :*void: null;
|
||||
EGL_NO_CONTEXT :*void: null;
|
||||
EGL_NO_SURFACE :*void: null;
|
||||
EGL_TRUE :u32: 1;
|
||||
EGL_FALSE :u32: 0;
|
||||
EGL_NONE :s32: 0x3038;
|
||||
EGL_RED_SIZE :s32: 0x3024;
|
||||
EGL_GREEN_SIZE :s32: 0x3023;
|
||||
EGL_BLUE_SIZE :s32: 0x3022;
|
||||
EGL_ALPHA_SIZE :s32: 0x3021;
|
||||
EGL_DEPTH_SIZE :s32: 0x3025;
|
||||
EGL_RENDERABLE_TYPE :s32: 0x3040;
|
||||
EGL_SURFACE_TYPE :s32: 0x3033;
|
||||
EGL_OPENGL_ES3_BIT :s32: 0x00000040;
|
||||
EGL_WINDOW_BIT :s32: 0x0004;
|
||||
EGL_NATIVE_VISUAL_ID :s32: 0x302E;
|
||||
EGL_CONTEXT_CLIENT_VERSION :s32: 0x3098;
|
||||
|
||||
eglGetDisplay :: (id: u64) -> *void #foreign;
|
||||
eglInitialize :: (d: *void, major: *s32, minor: *s32) -> u32 #foreign;
|
||||
eglChooseConfig :: (d: *void, attrs: *s32, configs: **void, sz: s32, num: *s32) -> u32 #foreign;
|
||||
eglGetConfigAttrib :: (d: *void, cfg: *void, attr: s32, value: *s32) -> u32 #foreign;
|
||||
eglCreateContext :: (d: *void, cfg: *void, share: *void, attrs: *s32) -> *void #foreign;
|
||||
eglCreateWindowSurface :: (d: *void, cfg: *void, window: *void, attrs: *s32) -> *void #foreign;
|
||||
eglMakeCurrent :: (d: *void, draw: *void, read: *void, ctx: *void) -> u32 #foreign;
|
||||
eglSwapBuffers :: (d: *void, surface: *void) -> u32 #foreign;
|
||||
eglDestroyContext :: (d: *void, ctx: *void) -> u32 #foreign;
|
||||
eglDestroySurface :: (d: *void, surface: *void) -> u32 #foreign;
|
||||
eglTerminate :: (d: *void) -> u32 #foreign;
|
||||
|
||||
// ── Module-level state ──────────────────────────────────────────────────
|
||||
|
||||
g_activity : *void = null; // global ref to the SxApp jobject (saved env can't outlive scope)
|
||||
g_app_window : *void = null; // ANativeWindow from surfaceCreated
|
||||
g_egl_display : *void = null;
|
||||
g_egl_context : *void = null;
|
||||
g_egl_surface : *void = null;
|
||||
g_egl_config : *void = null;
|
||||
g_viewport_w : s32 = 0;
|
||||
g_viewport_h : s32 = 0;
|
||||
// Defaults to 1.0 until a proper density query lands. Chess's pipeline
|
||||
// uses `viewport_w/h` as the layout space and `dpi_scale` to scale
|
||||
// rendering; mismatches cause layout drift / shrinking.
|
||||
g_dpi_scale : f32 = 1.0;
|
||||
g_should_stop : bool = false;
|
||||
g_render_thread_started : bool = false;
|
||||
g_render_thread : u64 = 0;
|
||||
g_frame_fn : Closure() = ---;
|
||||
g_frame_fn_set : bool = false;
|
||||
|
||||
// Touch event queue. Single-producer (Java UI thread) / single-consumer
|
||||
// (render thread); a small ring buffer guarded by a pthread mutex is
|
||||
// enough — chess only generates touches on user interaction so contention
|
||||
// is rare.
|
||||
TouchEvent :: struct {
|
||||
action: s32;
|
||||
x: f32;
|
||||
y: f32;
|
||||
}
|
||||
|
||||
g_touch_queue : [64]TouchEvent = ---;
|
||||
g_touch_head : u32 = 0;
|
||||
g_touch_tail : u32 = 0;
|
||||
// pthread_mutex_t is 40 bytes on bionic (NDK 26+); over-size to 64 for safety.
|
||||
g_touch_mutex_storage : [64]u8 = ---;
|
||||
g_touch_mutex_inited : bool = false;
|
||||
|
||||
// ── #jni_main Activity ──────────────────────────────────────────────────
|
||||
|
||||
// ── User-facing helpers for the consumer's `#jni_main` Activity ────────
|
||||
//
|
||||
// The consumer (chess, etc.) writes their own `SxApp :: #jni_main
|
||||
// #jni_class("...")` declaration with `#implements SurfaceHolderCallback`
|
||||
// and the standard lifecycle methods. This file provides the primitives
|
||||
// those methods call:
|
||||
//
|
||||
// - `sx_android_forward_assets(env, activity)` from onCreate.
|
||||
// - `sx_android_attach_window(env, holder)` from surfaceCreated.
|
||||
// - `sx_android_detach_window()` from surfaceDestroyed.
|
||||
// - `sx_android_set_viewport(w, h)` from surfaceChanged.
|
||||
// - `sx_android_start_render_thread(main_fn)` once the surface is up.
|
||||
// - `sx_android_push_touch(action, x, y)` from onTouchEvent.
|
||||
|
||||
// Extract the AAssetManager from the Activity and install it into the
|
||||
// C file_utils so `read_file_bytes` can route through `AAssetManager_open`.
|
||||
// Call this from your Activity's `onCreate` (BEFORE any asset read).
|
||||
sx_android_forward_assets :: (env: *void, activity: *JContext) {
|
||||
#jni_env(env) {
|
||||
assets := activity.getAssets();
|
||||
aam := AAssetManager_fromJava(env, xx assets);
|
||||
sx_android_set_asset_manager(aam);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the ANativeWindow from a SurfaceHolder. Call this from your
|
||||
// Activity's `surfaceCreated`. The window stays valid until
|
||||
// `sx_android_detach_window` runs (typically in `surfaceDestroyed`).
|
||||
sx_android_attach_window :: (env: *void, holder: *SurfaceHolder) {
|
||||
#jni_env(env) {
|
||||
surface := holder.getSurface();
|
||||
g_app_window = ANativeWindow_fromSurface(env, xx surface);
|
||||
}
|
||||
}
|
||||
|
||||
sx_android_detach_window :: () {
|
||||
if g_app_window != null {
|
||||
ANativeWindow_release(g_app_window);
|
||||
g_app_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
sx_android_set_viewport :: (w: s32, h: s32) {
|
||||
g_viewport_w = w;
|
||||
g_viewport_h = h;
|
||||
}
|
||||
|
||||
// Start the render thread that brings up EGL on `g_app_window` and calls
|
||||
// the user-supplied `entry_fn` (typically the user's `main`). Safe to
|
||||
// call once after `sx_android_attach_window` has set the window.
|
||||
sx_android_start_render_thread :: (entry_fn: () -> void) {
|
||||
if g_render_thread_started { return; }
|
||||
g_user_main_fn = entry_fn;
|
||||
pthread_create(@g_render_thread, null, sx_android_render_thread_entry, null);
|
||||
g_render_thread_started = true;
|
||||
}
|
||||
|
||||
g_user_main_fn : () -> void = null;
|
||||
|
||||
sx_android_render_thread_entry :: (arg: *void) -> *void {
|
||||
while g_app_window == null and !g_should_stop {
|
||||
usleep(1000);
|
||||
}
|
||||
if g_should_stop { return null; }
|
||||
|
||||
if !sx_android_egl_init() {
|
||||
__android_log_print(6, "sxapp".ptr, "EGL bootstrap failed\n".ptr);
|
||||
return null;
|
||||
}
|
||||
|
||||
if g_user_main_fn != null {
|
||||
g_user_main_fn();
|
||||
}
|
||||
null;
|
||||
}
|
||||
|
||||
// Bring up EGL on g_app_window. Sets g_egl_display / g_egl_context /
|
||||
// g_egl_surface and makes the context current. Returns false on any
|
||||
// failure — caller bails on the render thread.
|
||||
sx_android_egl_init :: () -> bool {
|
||||
g_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if g_egl_display == EGL_NO_DISPLAY { return false; }
|
||||
|
||||
major : s32 = 0;
|
||||
minor : s32 = 0;
|
||||
if eglInitialize(g_egl_display, @major, @minor) == EGL_FALSE { return false; }
|
||||
|
||||
cfg_attrs : [13]s32 = .{
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 24,
|
||||
EGL_NONE,
|
||||
};
|
||||
num_cfg : s32 = 0;
|
||||
if eglChooseConfig(g_egl_display, @cfg_attrs[0], @g_egl_config, 1, @num_cfg) == EGL_FALSE { return false; }
|
||||
if num_cfg < 1 { return false; }
|
||||
|
||||
visual_id : s32 = 0;
|
||||
eglGetConfigAttrib(g_egl_display, g_egl_config, EGL_NATIVE_VISUAL_ID, @visual_id);
|
||||
ANativeWindow_setBuffersGeometry(g_app_window, 0, 0, visual_id);
|
||||
|
||||
ctx_attrs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
|
||||
g_egl_context = eglCreateContext(g_egl_display, g_egl_config, EGL_NO_CONTEXT, @ctx_attrs[0]);
|
||||
if g_egl_context == EGL_NO_CONTEXT { return false; }
|
||||
|
||||
g_egl_surface = eglCreateWindowSurface(g_egl_display, g_egl_config, g_app_window, null);
|
||||
if g_egl_surface == EGL_NO_SURFACE { return false; }
|
||||
|
||||
if eglMakeCurrent(g_egl_display, g_egl_surface, g_egl_surface, g_egl_context) == EGL_FALSE { return false; }
|
||||
|
||||
g_viewport_w = ANativeWindow_getWidth(g_app_window);
|
||||
g_viewport_h = ANativeWindow_getHeight(g_app_window);
|
||||
true;
|
||||
}
|
||||
|
||||
// ── Touch event queue ───────────────────────────────────────────────────
|
||||
|
||||
sx_android_push_touch :: (action: s32, x: f32, y: f32) {
|
||||
sx_android_ensure_touch_mutex();
|
||||
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
|
||||
next := (g_touch_tail + 1) % 64;
|
||||
if next != g_touch_head { // drop on full
|
||||
g_touch_queue[g_touch_tail] = TouchEvent.{ action = action, x = x, y = y };
|
||||
g_touch_tail = next;
|
||||
}
|
||||
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
|
||||
}
|
||||
|
||||
sx_android_drain_touches :: (out: *List(Event)) {
|
||||
sx_android_ensure_touch_mutex();
|
||||
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
|
||||
while g_touch_head != g_touch_tail {
|
||||
t := g_touch_queue[g_touch_head];
|
||||
g_touch_head = (g_touch_head + 1) % 64;
|
||||
// MotionEvent actions: 0=DOWN, 1=UP, 2=MOVE. Map onto chess's
|
||||
// existing mouse Event variants — touch becomes a left-button
|
||||
// mouse on the same screen coords; delta unused on Android.
|
||||
pos : Point = .{ x = t.x, y = t.y };
|
||||
if t.action == 0 {
|
||||
out.append(.mouse_down(.{ position = pos, button = .left }));
|
||||
} else if t.action == 1 {
|
||||
out.append(.mouse_up(.{ position = pos, button = .left }));
|
||||
} else if t.action == 2 {
|
||||
out.append(.mouse_moved(.{ position = pos, delta = .{ x = 0, y = 0 } }));
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
|
||||
}
|
||||
|
||||
sx_android_ensure_touch_mutex :: () {
|
||||
if g_touch_mutex_inited { return; }
|
||||
pthread_mutex_init(xx @g_touch_mutex_storage[0], null);
|
||||
g_touch_mutex_inited = true;
|
||||
}
|
||||
|
||||
// ── AndroidPlatform ─────────────────────────────────────────────────────
|
||||
|
||||
AndroidPlatform :: struct {
|
||||
title: [:0]u8 = "";
|
||||
width: s32 = 0;
|
||||
height: s32 = 0;
|
||||
events: List(Event) = .{};
|
||||
}
|
||||
|
||||
impl Platform for AndroidPlatform {
|
||||
init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
|
||||
self.title = title;
|
||||
self.width = w;
|
||||
self.height = h;
|
||||
true;
|
||||
}
|
||||
|
||||
begin_frame :: (self: *AndroidPlatform) -> FrameContext {
|
||||
FrameContext.{
|
||||
viewport_w = xx g_viewport_w,
|
||||
viewport_h = xx g_viewport_h,
|
||||
pixel_w = g_viewport_w,
|
||||
pixel_h = g_viewport_h,
|
||||
dpi_scale = g_dpi_scale,
|
||||
delta_time = 0.016,
|
||||
target_present_time = 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
end_frame :: (self: *AndroidPlatform) {
|
||||
if g_egl_display != null and g_egl_surface != null {
|
||||
eglSwapBuffers(g_egl_display, g_egl_surface);
|
||||
}
|
||||
}
|
||||
|
||||
poll_events :: (self: *AndroidPlatform) -> []Event {
|
||||
self.events.len = 0;
|
||||
sx_android_drain_touches(@self.events);
|
||||
result : []Event = ---;
|
||||
result.ptr = self.events.items;
|
||||
result.len = self.events.len;
|
||||
result;
|
||||
}
|
||||
|
||||
safe_insets :: (self: *AndroidPlatform) -> EdgeInsets {
|
||||
EdgeInsets.{};
|
||||
}
|
||||
|
||||
keyboard :: (self: *AndroidPlatform) -> KeyboardState {
|
||||
KeyboardState.zero();
|
||||
}
|
||||
show_keyboard :: (self: *AndroidPlatform) { }
|
||||
hide_keyboard :: (self: *AndroidPlatform) { }
|
||||
|
||||
run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) {
|
||||
g_frame_fn = frame_fn;
|
||||
g_frame_fn_set = true;
|
||||
// `frame_fn` is expected to call `g_plat.end_frame()` which does
|
||||
// the `eglSwapBuffers` — don't swap again here or the back buffer
|
||||
// is presented twice per render, alternating with the previous
|
||||
// frame's contents → visible flicker.
|
||||
while !g_should_stop {
|
||||
frame_fn();
|
||||
usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
stop :: (self: *AndroidPlatform) {
|
||||
g_should_stop = true;
|
||||
}
|
||||
|
||||
shutdown :: (self: *AndroidPlatform) {
|
||||
if g_egl_display != null {
|
||||
eglMakeCurrent(g_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if g_egl_surface != null { eglDestroySurface(g_egl_display, g_egl_surface); g_egl_surface = null; }
|
||||
if g_egl_context != null { eglDestroyContext(g_egl_display, g_egl_context); g_egl_context = null; }
|
||||
eglTerminate(g_egl_display);
|
||||
g_egl_display = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,8 +180,11 @@ fn appendDotted(
|
||||
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 == '/') '.' else c);
|
||||
try buf.append(allocator, if (c == '/' or c == '$') '.' else c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +213,13 @@ fn emitOverride(
|
||||
try buf.appendSlice(allocator, md.name);
|
||||
try buf.append(allocator, '(');
|
||||
try emitJavaParamList(allocator, buf, md, opts);
|
||||
try buf.appendSlice(allocator, ") {\n sx_");
|
||||
// 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);
|
||||
|
||||
126
src/ir/lower.zig
126
src/ir/lower.zig
@@ -334,7 +334,7 @@ pub const Lowering = struct {
|
||||
/// This preserves the old behavior for comptime evaluation contexts.
|
||||
pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void {
|
||||
for (decls) |decl| {
|
||||
self.current_source_file = decl.source_file;
|
||||
self.setCurrentSourceFile(decl.source_file);
|
||||
const is_imported = if (self.main_file) |mf|
|
||||
(if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false)
|
||||
else
|
||||
@@ -393,7 +393,7 @@ pub const Lowering = struct {
|
||||
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
|
||||
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
||||
for (decls) |decl| {
|
||||
self.current_source_file = decl.source_file;
|
||||
self.setCurrentSourceFile(decl.source_file);
|
||||
const is_imported = if (self.main_file) |mf|
|
||||
(if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false)
|
||||
else
|
||||
@@ -478,7 +478,7 @@ pub const Lowering = struct {
|
||||
// Simple value constants with type annotation (e.g. AF_INET :s32: 2)
|
||||
if (cd.type_annotation != null) {
|
||||
switch (cd.value.data) {
|
||||
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal => {
|
||||
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => {
|
||||
const ty = self.resolveType(cd.type_annotation);
|
||||
self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {};
|
||||
},
|
||||
@@ -778,7 +778,7 @@ pub const Lowering = struct {
|
||||
// Function not yet declared — create it fresh via lowerFunction
|
||||
self.lowerFunction(fd, name, false);
|
||||
// Restore builder state
|
||||
self.current_source_file = saved_source_file;
|
||||
self.setCurrentSourceFile(saved_source_file);
|
||||
self.scope = saved_scope;
|
||||
self.func_defer_base = saved_defer_base;
|
||||
self.force_block_value = saved_force_block_value;
|
||||
@@ -792,10 +792,10 @@ pub const Lowering = struct {
|
||||
// Re-use the existing function slot — switch builder to it
|
||||
self.builder.func = fid;
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
self.current_source_file = func.source_file;
|
||||
self.setCurrentSourceFile(func.source_file);
|
||||
if (!func.is_extern) {
|
||||
// Already promoted (e.g., via lowerComptimeDeps) — skip
|
||||
self.current_source_file = saved_source_file;
|
||||
self.setCurrentSourceFile(saved_source_file);
|
||||
self.scope = saved_scope;
|
||||
self.func_defer_base = saved_defer_base;
|
||||
self.block_terminated = saved_block_terminated;
|
||||
@@ -864,7 +864,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
// Restore builder state
|
||||
self.current_source_file = saved_source_file;
|
||||
self.setCurrentSourceFile(saved_source_file);
|
||||
self.scope = saved_scope;
|
||||
self.func_defer_base = saved_defer_base;
|
||||
self.block_terminated = saved_block_terminated;
|
||||
@@ -1811,7 +1811,7 @@ pub const Lowering = struct {
|
||||
.call => |c| self.lowerCall(&c),
|
||||
.ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic),
|
||||
.field_access => |fa| self.lowerFieldAccess(&fa, node.span),
|
||||
.struct_literal => |sl| self.lowerStructLiteral(&sl),
|
||||
.struct_literal => |sl| self.lowerStructLiteral(&sl, node.span),
|
||||
.array_literal => |al| self.lowerArrayLiteral(&al),
|
||||
.index_expr => |ie| self.lowerIndexExpr(&ie),
|
||||
.slice_expr => |se| self.lowerSliceExpr(&se),
|
||||
@@ -2883,7 +2883,7 @@ pub const Lowering = struct {
|
||||
|
||||
// ── Struct/enum/union ops ───────────────────────────────────────
|
||||
|
||||
fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral) Ref {
|
||||
fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref {
|
||||
// Check for tagged enum construction: .Variant.{ payload_fields }
|
||||
// This happens when type_expr is an enum_literal and target_type is a union
|
||||
if (sl.type_expr) |te| {
|
||||
@@ -2893,7 +2893,38 @@ pub const Lowering = struct {
|
||||
if (!union_ty.isBuiltin()) {
|
||||
const union_info = self.module.types.get(union_ty);
|
||||
if (union_info == .tagged_union) {
|
||||
return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union);
|
||||
return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `.{ name = ... }` against a tagged-union target_type. Reject:
|
||||
// the only valid construction forms are `.variant(payload)` and
|
||||
// `.variant.{ field, ... }`. Falling through would lower the
|
||||
// user's values straight into the `(tag, payload_bytes)` slot
|
||||
// pair and emit IR that LLVM later rejects.
|
||||
if (sl.type_expr == null and sl.struct_name == null) {
|
||||
const tu_ty = self.target_type orelse .s64;
|
||||
if (!tu_ty.isBuiltin()) {
|
||||
const tu_info = self.module.types.get(tu_ty);
|
||||
if (tu_info == .tagged_union) {
|
||||
if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) {
|
||||
const first_name = sl.field_inits[0].name.?;
|
||||
if (self.diagnostics) |diags| {
|
||||
const ty_name = self.formatTypeName(tu_ty);
|
||||
if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) {
|
||||
diags.addFmt(
|
||||
.err,
|
||||
span,
|
||||
"cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`",
|
||||
.{ ty_name, first_name, first_name, first_name },
|
||||
);
|
||||
} else {
|
||||
self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span);
|
||||
}
|
||||
}
|
||||
return self.builder.enumInit(0, Ref.none, tu_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3459,7 +3490,13 @@ pub const Lowering = struct {
|
||||
variant_name: []const u8,
|
||||
union_ty: TypeId,
|
||||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||||
span: ast.Span,
|
||||
) Ref {
|
||||
if (self.findTaggedVariant(union_info, variant_name) == null) {
|
||||
self.emitBadVariant(union_ty, union_info, variant_name, span);
|
||||
return self.builder.enumInit(0, Ref.none, union_ty);
|
||||
}
|
||||
|
||||
const tag = self.resolveVariantValue(union_ty, variant_name);
|
||||
const name_id = self.module.types.internString(variant_name);
|
||||
|
||||
@@ -3512,6 +3549,40 @@ pub const Lowering = struct {
|
||||
return self.builder.enumInit(tag, payload, union_ty);
|
||||
}
|
||||
|
||||
fn findTaggedVariant(
|
||||
self: *Lowering,
|
||||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||||
variant_name: []const u8,
|
||||
) ?usize {
|
||||
const name_id = self.module.types.internString(variant_name);
|
||||
for (union_info.fields, 0..) |f, i| {
|
||||
if (f.name == name_id) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn emitBadVariant(
|
||||
self: *Lowering,
|
||||
union_ty: TypeId,
|
||||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||||
variant_name: []const u8,
|
||||
span: ast.Span,
|
||||
) void {
|
||||
const diags = self.diagnostics orelse return;
|
||||
const ty_name = self.formatTypeName(union_ty);
|
||||
var list: std.ArrayList(u8) = .empty;
|
||||
for (union_info.fields, 0..) |f, i| {
|
||||
if (i > 0) list.appendSlice(self.alloc, ", ") catch return;
|
||||
list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return;
|
||||
}
|
||||
diags.addFmt(
|
||||
.err,
|
||||
span,
|
||||
"'{s}' is not a variant of '{s}' (variants are: {s})",
|
||||
.{ variant_name, ty_name, list.items },
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolve a variant name to its runtime value (flags: power-of-2, regular: index).
|
||||
fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 {
|
||||
if (ty.isBuiltin()) return 0;
|
||||
@@ -4144,7 +4215,19 @@ pub const Lowering = struct {
|
||||
return Ref.none;
|
||||
};
|
||||
|
||||
const ret_ty = self.module.types.ptrTo(.void); // jobject
|
||||
// sx-side return type is `*Self` — resolve to a pointer to the
|
||||
// foreign-class struct type so method dispatch on the new
|
||||
// jobject works (`view := SurfaceView.new(ctx); view.getHolder()`).
|
||||
// At LLVM level still ptr; the sx type table is what method
|
||||
// resolution consults.
|
||||
const self_struct_name = self.module.types.internString(fcd.name);
|
||||
const self_struct_id = if (self.module.types.findByName(self_struct_name)) |existing|
|
||||
existing
|
||||
else blk: {
|
||||
const info: types.TypeInfo = .{ .@"struct" = .{ .name = self_struct_name, .fields = &.{} } };
|
||||
break :blk self.module.types.intern(info);
|
||||
};
|
||||
const ret_ty = self.module.types.ptrTo(self_struct_id);
|
||||
|
||||
const name_sid = self.module.types.internString("<init>");
|
||||
const name_ref = self.builder.constString(name_sid);
|
||||
@@ -9703,6 +9786,7 @@ pub const Lowering = struct {
|
||||
return self.builder.constString(sid);
|
||||
},
|
||||
.undef_literal => return self.builder.constUndef(ci.ty),
|
||||
.null_literal => return self.builder.constNull(ci.ty),
|
||||
else => {
|
||||
// Complex expressions (struct_literal, call, etc.) — lower on demand
|
||||
const saved_target = self.target_type;
|
||||
@@ -9730,6 +9814,14 @@ pub const Lowering = struct {
|
||||
return self.module.types.findByName(name_id) != null;
|
||||
}
|
||||
|
||||
/// Update `self.current_source_file` and mirror it onto `diags.current_source_file`,
|
||||
/// so any diagnostic emitted from inside a function lowered from another module is
|
||||
/// attributed to that module — not whichever file the diagnostics list was init'd with.
|
||||
fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void {
|
||||
self.current_source_file = source_file;
|
||||
if (self.diagnostics) |d| d.current_source_file = source_file;
|
||||
}
|
||||
|
||||
fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "unresolved: '{s}'", .{name});
|
||||
@@ -10115,13 +10207,13 @@ pub const Lowering = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// JNI map: pointer types collapse to `*void` (jobject opaque handle);
|
||||
/// primitives pass through unchanged.
|
||||
/// JNI param/return type resolution: user-declared types pass through
|
||||
/// `resolveType` so the method body can dispatch on richer foreign-class
|
||||
/// types (`holder.getSurface()` etc.). At LLVM level both `*SurfaceHolder`
|
||||
/// and `*void` lower to the same `ptr`, so the C ABI shape Java sees is
|
||||
/// unchanged — only sx-side method resolution benefits.
|
||||
fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId {
|
||||
return switch (type_node.data) {
|
||||
.pointer_type_expr => self.module.types.ptrTo(.void),
|
||||
else => self.resolveType(type_node),
|
||||
};
|
||||
return self.resolveType(type_node);
|
||||
}
|
||||
|
||||
/// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol
|
||||
|
||||
@@ -533,6 +533,10 @@ fn buildJniMainManifest(allocator: std.mem.Allocator, package: []const u8, lib_n
|
||||
try class_name.append(allocator, if (ch == '/') '.' else ch);
|
||||
}
|
||||
const activity_name = try class_name.toOwnedSlice(allocator);
|
||||
// `Theme.DeviceDefault.NoActionBar.Fullscreen` removes both the
|
||||
// ActionBar title (the "sxchess" strip) and the status bar — sx-rendered
|
||||
// apps own the whole window. Consumers wanting a different theme will
|
||||
// ship their own manifest via `--manifest`.
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<?xml version="1.0" encoding="utf-8"?>
|
||||
\\<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
@@ -545,6 +549,7 @@ fn buildJniMainManifest(allocator: std.mem.Allocator, package: []const u8, lib_n
|
||||
\\ android:name="{s}"
|
||||
\\ android:exported="true"
|
||||
\\ android:label="{s}"
|
||||
\\ android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
|
||||
\\ android:configChanges="orientation|keyboardHidden|screenSize">
|
||||
\\ <intent-filter>
|
||||
\\ <action android:name="android.intent.action.MAIN" />
|
||||
|
||||
Reference in New Issue
Block a user