ffi 2.10: #jni_method_descriptor override precedence in deriveMethod

When a `ForeignMethodDecl` carries a non-null
`jni_descriptor_override` (parsed in step 2.6), `deriveMethod`
short-circuits — auto-derivation is skipped entirely and the override
is returned (duplicated through the caller's allocator so ownership
semantics stay uniform regardless of which branch ran).

Two new tests: override beats normal derivation, and override
bypasses cross-class refs that would otherwise fail with
`UnknownClassAlias`. Confirms the escape-hatch semantics from 2.6 —
users can paste an explicit JNI signature when auto-derivation
doesn't match (synthetic methods, ambiguous overloads, unknown-to-sx
JVM internals).

15 jni_descriptor tests pass; 126/126 examples still green.
This commit is contained in:
agra
2026-05-20 10:27:33 +03:00
parent 51882656a5
commit ca840ff6c8
2 changed files with 59 additions and 0 deletions

View File

@@ -160,6 +160,58 @@ test "cross-class *Foo with empty registry errors" {
try std.testing.expectError(desc.DeriveError.UnknownClassAlias, result);
}
test "deriveMethod respects #jni_method_descriptor override verbatim" {
const a = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const aa = arena.allocator();
// The actual sx signature `(self: *Self) -> s32` would derive to
// `()I`. The override should win regardless.
const self_te = try makeTypeExpr(aa, "Self");
const self_ptr = try makePointer(aa, self_te);
const ret = try makeTypeExpr(aa, "s32");
const method: ast.ForeignMethodDecl = .{
.name = "weirdMethod",
.params = &.{self_ptr},
.param_names = &.{"self"},
.return_type = ret,
.is_static = false,
.jni_descriptor_override = "(Ljava/lang/Object;)Ljava/util/List;",
};
const out = try desc.deriveMethod(a, .{ .enclosing_path = "com/example/Foo" }, method);
defer a.free(out);
try std.testing.expectEqualStrings("(Ljava/lang/Object;)Ljava/util/List;", out);
}
test "deriveMethod override bypasses unresolvable cross-class refs" {
const a = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const aa = arena.allocator();
// The signature references `*UnknownClass` that isn't in the
// registry — would normally fail with `UnknownClassAlias`. The
// override short-circuits derivation, so it succeeds.
const self_te = try makeTypeExpr(aa, "Self");
const self_ptr = try makePointer(aa, self_te);
const unknown = try makeTypeExpr(aa, "UnknownClass");
const unknown_ptr = try makePointer(aa, unknown);
const method: ast.ForeignMethodDecl = .{
.name = "weirdMethod",
.params = &.{self_ptr},
.param_names = &.{"self"},
.return_type = unknown_ptr,
.is_static = false,
.jni_descriptor_override = "()V",
};
const out = try desc.deriveMethod(a, .{ .enclosing_path = "com/example/Foo" }, method);
defer a.free(out);
try std.testing.expectEqualStrings("()V", out);
}
test "deriveMethod chains *Foo returns and params" {
const a = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(a);

View File

@@ -105,6 +105,13 @@ pub fn deriveMethod(
ctx: Context,
method: ast.ForeignMethodDecl,
) DeriveError![]u8 {
// `#jni_method_descriptor("(Sig)Ret")` short-circuits derivation
// entirely. Allocate a copy so the caller has uniform ownership
// semantics regardless of which branch ran.
if (method.jni_descriptor_override) |override| {
return allocator.dupe(u8, override);
}
var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(allocator);