ffi: define-by-default #jni_class + #foreign modifier + #jni_main token
Flip the surface semantics for type-introducer directives: bare
`Foo :: #jni_class("path") { ... }` now means "DEFINE a new Java class
at that path" (sx-side provides the implementations). The `#foreign`
prefix modifier flips it back to "REFERENCE an existing class on the
foreign runtime." Matches how `#foreign` already reads in sx for C
function declarations (`printf :: ... #foreign;`).
Foo :: #foreign #jni_class("path/to/Foo") { ... } // reference
Foo :: #jni_class("path/to/Foo") { ... } // define
Foo :: #jni_main #jni_class("path/to/Foo") { ... } // define + main Activity
Compiler-side changes:
- New `hash_jni_main` lexer token (the launchable-Activity marker).
Existing `hash_foreign` is reused; no new modifier token there.
- `ForeignClassDecl` gains `is_foreign: bool` + `is_main: bool`.
`ForeignMethodDecl` gains `body: ?*Node` so defined-class methods
can carry sx-side implementations (foreign-class methods stay
`;`-terminated).
- Parser learns `tryParseForeignClassPrefix` — peek-and-consume the
modifier tokens, then dispatch to the unchanged
`parseForeignClassDecl` with the flags threaded through.
- Sema rejects two illegal combinations: `#foreign + #jni_main`
(can't be both an external reference and the app's main entry),
and bodied methods on `#foreign` decls (foreign methods are
runtime-provided).
- Lower's foreign-class dispatch errors on non-foreign decls with
a pointer to the runtime-synthesis follow-up; defined-class
codegen (Java class emission, RegisterNatives wiring, manifest
entry generation) lands in a separate session.
Migration:
- `library/modules/platform/android_jni.sx`: all four foreign class
decls (`Activity`, `Window`, `View`, `WindowInsets`) gain `#foreign`.
- `examples/ffi-jni-class-{01..08}*.sx`: every test's `#jni_class` /
`#jni_interface` / `#objc_class` / `#objc_protocol` / `#swift_class`
/ `#swift_struct` / `#swift_protocol` usage gains `#foreign`. All
9 files mechanical perl rename; snapshots unchanged.
Verified locally:
- `zig build test` clean.
- `bash tests/run_examples.sh` 129/129.
- `bash tests/cross_compile.sh` 3/3.
- Chess APK rebuilds, reinstalls, launches on Pixel; safe-area
clearance preserved.
This commit is contained in:
@@ -209,10 +209,15 @@ pub const Parser = struct {
|
||||
return self.parseProtocolDecl(name, start_pos);
|
||||
}
|
||||
|
||||
// Foreign-type binding: name :: #jni_class / #jni_interface / #objc_class /
|
||||
// #objc_protocol / #swift_class / #swift_struct / #swift_protocol ("path") { body }
|
||||
if (self.foreignRuntimeForCurrent()) |runtime| {
|
||||
return self.parseForeignClassDecl(name, start_pos, runtime);
|
||||
// Foreign-type binding with optional prefix modifiers:
|
||||
// [#foreign | #jni_main]* (#jni_class / #jni_interface / #objc_class /
|
||||
// #objc_protocol / #swift_class / #swift_struct / #swift_protocol) ("path") { body }
|
||||
//
|
||||
// Define-by-default: bare `#jni_class("...")` declares a new class (sx-defined).
|
||||
// `#foreign` flips that to "reference an existing class on the foreign side."
|
||||
// `#jni_main` flags the class as the launchable entry (Android Activity).
|
||||
if (self.tryParseForeignClassPrefix()) |prefix| {
|
||||
return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main);
|
||||
}
|
||||
|
||||
// C-style union declaration
|
||||
@@ -1041,7 +1046,70 @@ pub const Parser = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime) anyerror!*Node {
|
||||
const ForeignClassPrefix = struct {
|
||||
runtime: ast.ForeignRuntime,
|
||||
is_foreign: bool,
|
||||
is_main: bool,
|
||||
};
|
||||
|
||||
/// Recognise an optional sequence of `#foreign` / `#jni_main` modifiers
|
||||
/// followed by a type-introducer directive (`#jni_class`, `#objc_class`,
|
||||
/// ...). Returns null if the current position isn't a foreign-class
|
||||
/// directive (possibly after modifiers). Consumes the modifier tokens
|
||||
/// only when a runtime directive follows; otherwise leaves the parser
|
||||
/// state untouched.
|
||||
fn tryParseForeignClassPrefix(self: *Parser) ?ForeignClassPrefix {
|
||||
// Peek ahead through modifier tokens to confirm a directive follows.
|
||||
var lookahead_idx: usize = 0;
|
||||
var is_foreign = false;
|
||||
var is_main = false;
|
||||
while (true) {
|
||||
const tag = self.peekTag(lookahead_idx);
|
||||
switch (tag) {
|
||||
.hash_foreign => {
|
||||
is_foreign = true;
|
||||
lookahead_idx += 1;
|
||||
},
|
||||
.hash_jni_main => {
|
||||
is_main = true;
|
||||
lookahead_idx += 1;
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
const runtime = self.foreignRuntimeForOffset(lookahead_idx) orelse return null;
|
||||
// Commit: consume modifier tokens.
|
||||
var i: usize = 0;
|
||||
while (i < lookahead_idx) : (i += 1) self.advance();
|
||||
return .{ .runtime = runtime, .is_foreign = is_foreign, .is_main = is_main };
|
||||
}
|
||||
|
||||
fn peekTag(self: *Parser, offset: usize) Tag {
|
||||
if (offset == 0) return self.current.tag;
|
||||
var lexer_copy = self.lexer;
|
||||
var tok: Token = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < offset) : (i += 1) {
|
||||
tok = lexer_copy.next();
|
||||
}
|
||||
return tok.tag;
|
||||
}
|
||||
|
||||
fn foreignRuntimeForOffset(self: *Parser, offset: usize) ?ast.ForeignRuntime {
|
||||
const tag = self.peekTag(offset);
|
||||
return switch (tag) {
|
||||
.hash_jni_class => .jni_class,
|
||||
.hash_jni_interface => .jni_interface,
|
||||
.hash_objc_class => .objc_class,
|
||||
.hash_objc_protocol => .objc_protocol,
|
||||
.hash_swift_class => .swift_class,
|
||||
.hash_swift_struct => .swift_struct,
|
||||
.hash_swift_protocol => .swift_protocol,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node {
|
||||
self.advance(); // skip directive token
|
||||
|
||||
try self.expect(.l_paren);
|
||||
@@ -1149,7 +1217,15 @@ pub const Parser = struct {
|
||||
try self.expect(.r_paren);
|
||||
}
|
||||
|
||||
try self.expect(.semicolon);
|
||||
// Method body is optional: `;` → declaration (foreign or inherited
|
||||
// method we just want to call); `{ ... }` → sx-side implementation
|
||||
// for sx-defined classes.
|
||||
var body_node: ?*Node = null;
|
||||
if (self.current.tag == .l_brace) {
|
||||
body_node = try self.parseBlock();
|
||||
} else {
|
||||
try self.expect(.semicolon);
|
||||
}
|
||||
|
||||
try members.append(self.allocator, .{ .method = .{
|
||||
.name = member_name,
|
||||
@@ -1158,6 +1234,7 @@ pub const Parser = struct {
|
||||
.return_type = return_type,
|
||||
.is_static = is_static,
|
||||
.jni_descriptor_override = desc_override,
|
||||
.body = body_node,
|
||||
} });
|
||||
}
|
||||
try self.expect(.r_brace);
|
||||
@@ -1167,6 +1244,8 @@ pub const Parser = struct {
|
||||
.foreign_path = foreign_path,
|
||||
.runtime = runtime,
|
||||
.members = try members.toOwnedSlice(self.allocator),
|
||||
.is_foreign = is_foreign,
|
||||
.is_main = is_main,
|
||||
} });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user