ReviewOS

also looking at this

craft-native/craft

feat(android): getDeviceInfo in Zig, and the JNI calls it needed

#137
Merged glennmichael123 wants to merge feat/android-jni-args into main
3 files +741 -0
packages/zig/src/jni_runtime.zigmodified+259-0
Changes to packages/zig/src/jni_runtime.zig
@@ -80,6 +80,23 @@ pub const jthrowable = jobject;
8080pub const jmethodID = ?*anyopaque;
8181pub const jfieldID = ?*anyopaque;
8282
83/// One Java argument, as the `A`-form calls take them.
84///
85/// A C union of every primitive plus a reference, eight bytes wide. Arguments
86/// are passed as an array of these rather than variadically — see
87/// `Jni.callObjectMethodA` for why that is the only form offered.
88pub const jvalue = extern union {
89 z: jboolean,
90 b: jbyte,
91 c: jchar,
92 s: jshort,
93 i: jint,
94 j: jlong,
95 f: jfloat,
96 d: jdouble,
97 l: jobject,
98};
99
83100/// `JNIEnv*` as the JVM passes it to a native method.
84101pub const JNIEnv = *const *const JNINativeInterface;
85102
@@ -445,6 +462,104 @@ pub const Jni = struct {
445462 return result;
446463 }
447464
465 // --- The argument-taking forms -------------------------------------
466 //
467 // Every one of these is the `A` variant, taking a `[]const jvalue`. The
468 // bare `CallObjectMethod` is variadic in C, and calling a variadic
469 // function through a `callconv(.c)` pointer means reproducing the
470 // platform's argument-passing rules exactly — which arm64 and x86-64
471 // Android do not share. The `A` form takes an array instead, so there is
472 // no ABI to reproduce and one implementation is right on both.
473 //
474 // `args.ptr` is passed even when the slice is empty; a zero-argument call
475 // never reads it, and a null there would be a second thing to reason about.
476
477 pub fn callObjectMethodA(self: Self, obj: jobject, id: jmethodID, args: []const jvalue) JniError!jobject {
478 const call: *const fn (JNIEnv, jobject, jmethodID, [*]const jvalue) callconv(.c) jobject =
479 @ptrCast(self.table().CallObjectMethodA orelse return JniError.NotFound);
480 const result = call(self.env, obj, id, args.ptr);
481 try self.check();
482 return result;
483 }
484
485 pub fn callVoidMethodA(self: Self, obj: jobject, id: jmethodID, args: []const jvalue) JniError!void {
486 const call: *const fn (JNIEnv, jobject, jmethodID, [*]const jvalue) callconv(.c) void =
487 @ptrCast(self.table().CallVoidMethodA orelse return JniError.NotFound);
488 call(self.env, obj, id, args.ptr);
489 try self.check();
490 }
491
492 /// `new <cls>(args)`. `id` must be the `<init>` method id.
493 pub fn newObjectA(self: Self, cls: jclass, id: jmethodID, args: []const jvalue) JniError!jobject {
494 const new: *const fn (JNIEnv, jclass, jmethodID, [*]const jvalue) callconv(.c) jobject =
495 @ptrCast(self.table().NewObjectA orelse return JniError.NotFound);
496 const obj = new(self.env, cls, id, args.ptr);
497 try self.check();
498 return obj orelse JniError.NotFound;
499 }
500
501 // --- Instance fields -------------------------------------------------
502 //
503 // Needed because Android exposes plain public fields on several of the
504 // classes this bridge reads — `DisplayMetrics.widthPixels`,
505 // `PackageInfo.versionName` — with no getter to call.
506
507 pub fn fieldId(self: Self, cls: jclass, name: [*:0]const u8, sig: [*:0]const u8) JniError!jfieldID {
508 const get: *const fn (JNIEnv, jclass, [*:0]const u8, [*:0]const u8) callconv(.c) jfieldID =
509 @ptrCast(self.table().GetFieldID orelse return JniError.NotFound);
510 const id = get(self.env, cls, name, sig);
511 try self.check();
512 return id orelse JniError.NotFound;
513 }
514
515 pub fn objectField(self: Self, obj: jobject, id: jfieldID) JniError!jobject {
516 const get: *const fn (JNIEnv, jobject, jfieldID) callconv(.c) jobject =
517 @ptrCast(self.table().GetObjectField orelse return JniError.NotFound);
518 const value = get(self.env, obj, id);
519 try self.check();
520 return value;
521 }
522
523 pub fn intField(self: Self, obj: jobject, id: jfieldID) JniError!jint {
524 const get: *const fn (JNIEnv, jobject, jfieldID) callconv(.c) jint =
525 @ptrCast(self.table().GetIntField orelse return JniError.NotFound);
526 const value = get(self.env, obj, id);
527 try self.check();
528 return value;
529 }
530
531 pub fn longField(self: Self, obj: jobject, id: jfieldID) JniError!jlong {
532 const get: *const fn (JNIEnv, jobject, jfieldID) callconv(.c) jlong =
533 @ptrCast(self.table().GetLongField orelse return JniError.NotFound);
534 const value = get(self.env, obj, id);
535 try self.check();
536 return value;
537 }
538
539 pub fn floatField(self: Self, obj: jobject, id: jfieldID) JniError!jfloat {
540 const get: *const fn (JNIEnv, jobject, jfieldID) callconv(.c) jfloat =
541 @ptrCast(self.table().GetFloatField orelse return JniError.NotFound);
542 const value = get(self.env, obj, id);
543 try self.check();
544 return value;
545 }
546
547 /// Make a Java `String` from UTF-8.
548 ///
549 /// The JVM wants *modified* UTF-8 here, the same encoding `stringToUtf8`
550 /// decodes on the way back — so a string carrying a NUL or an astral
551 /// character has to be re-encoded rather than handed over. Callers pass
552 /// NUL-terminated literals and ASCII package names today, which are
553 /// identical in both encodings; `encodeModifiedUtf8` is the function to
554 /// add when that stops being true, and this is where it goes.
555 pub fn newStringUtf(self: Self, text: [*:0]const u8) JniError!jstring {
556 const new: *const fn (JNIEnv, [*:0]const u8) callconv(.c) jstring =
557 @ptrCast(self.table().NewStringUTF orelse return JniError.NotFound);
558 const str = new(self.env, text);
559 try self.check();
560 return str orelse JniError.OutOfMemory;
561 }
562
448563 pub fn callIntMethod(self: Self, obj: jobject, id: jmethodID) JniError!jint {
449564 const call: *const fn (JNIEnv, jobject, jmethodID) callconv(.c) jint =
450565 @ptrCast(self.table().CallIntMethod orelse return JniError.NotFound);
@@ -459,6 +574,31 @@ pub const Jni = struct {
459574 return get(self.env, obj) orelse JniError.NotFound;
460575 }
461576
577 /// Open a scope that owns every local reference created inside it.
578 ///
579 /// The JVM only guarantees sixteen local reference slots. A function that
580 /// walks a few objects — an Activity to a WindowManager to a Display to a
581 /// DisplayMetrics — spends them without ever looking like it is
582 /// allocating, and overflowing aborts the process rather than failing a
583 /// call. `popLocalFrame` frees the whole scope in one go, which is both
584 /// less code than a `defer` per reference and impossible to get half-right.
585 pub fn pushLocalFrame(self: Self, capacity: jint) JniError!void {
586 const push: *const fn (JNIEnv, jint) callconv(.c) jint =
587 @ptrCast(self.table().PushLocalFrame orelse return JniError.NotFound);
588 if (push(self.env, capacity) != 0) {
589 try self.check();
590 return JniError.OutOfMemory;
591 }
592 }
593
594 /// Close the scope. `keep` is the one reference to survive it, promoted
595 /// into the enclosing frame — null when nothing needs to.
596 pub fn popLocalFrame(self: Self, keep: jobject) jobject {
597 const pop: *const fn (JNIEnv, jobject) callconv(.c) jobject =
598 @ptrCast(self.table().PopLocalFrame orelse return null);
599 return pop(self.env, keep);
600 }
601
462602 pub fn deleteLocalRef(self: Self, obj: jobject) void {
463603 if (obj == null) return;
464604 const del: *const fn (JNIEnv, jobject) callconv(.c) void =
@@ -843,3 +983,122 @@ test "modified UTF-8 decodes to the standard encoding" {
843983 try testing.expectError(JniError.MalformedString, decodeModifiedUtf8(alloc, "\xE6\x97"));
844984 try testing.expectError(JniError.MalformedString, decodeModifiedUtf8(alloc, "\xFF"));
845985}
986
987// --- Argument passing ------------------------------------------------------
988
989var fake_seen_args: [4]jvalue = undefined;
990var fake_arg_count: usize = 0;
991var fake_called_obj: jobject = null;
992var fake_called_id: jmethodID = null;
993
994var fake_returned: u8 = 0;
995fn fakeCallObjectMethodA(_: JNIEnv, obj: jobject, id: jmethodID, args: [*]const jvalue) callconv(.c) jobject {
996 fake_called_obj = obj;
997 fake_called_id = id;
998 // The fake knows its own arity; a real one is told by the signature.
999 fake_seen_args[0] = args[0];
1000 fake_seen_args[1] = args[1];
1001 fake_arg_count = 2;
1002 return @ptrCast(&fake_returned);
1003}
1004
1005fn fakeIntField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jint {
1006 return 1080;
1007}
1008fn fakeFloatField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jfloat {
1009 return 2.75;
1010}
1011fn fakeLongField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jlong {
1012 return 4_000_000_123;
1013}
1014fn fakeFieldId(_: JNIEnv, _: jclass, _: [*:0]const u8, _: [*:0]const u8) callconv(.c) jfieldID {
1015 return @ptrCast(&fake_field);
1016}
1017
1018test "jvalue is the eight-byte union the A-form calls expect" {
1019 // Wrong size here would misalign every argument after the first, and the
1020 // call would still run — the classic JNI failure that looks like garbage
1021 // data rather than a crash.
1022 try testing.expectEqual(@as(usize, 8), @sizeOf(jvalue));
1023 try testing.expectEqual(@as(usize, 8), @alignOf(jvalue));
1024
1025 // Each member reads back what was written, which is what says the union is
1026 // laid out rather than merely sized.
1027 var v = jvalue{ .i = -7 };
1028 try testing.expectEqual(@as(jint, -7), v.i);
1029 v = jvalue{ .j = 4_000_000_123 };
1030 try testing.expectEqual(@as(jlong, 4_000_000_123), v.j);
1031 v = jvalue{ .d = 0.5 };
1032 try testing.expectEqual(@as(jdouble, 0.5), v.d);
1033}
1034
1035test "arguments arrive in the array, in order, undamaged" {
1036 resetFakes();
1037 var table = emptyTable();
1038 table.CallObjectMethodA = @ptrCast(&fakeCallObjectMethodA);
1039 table.ExceptionOccurred = @ptrCast(&fakeExceptionOccurred);
1040
1041 const ptr: *const JNINativeInterface = &table;
1042 const jni = Jni.init(&ptr);
1043
1044 var receiver: u8 = 0;
1045 var name_string: u8 = 0;
1046 var method: u8 = 0;
1047
1048 // `getPackageInfo(String, int)` — the exact shape getDeviceInfo needs, and
1049 // the one a variadic call would be most likely to get wrong: a reference
1050 // and an int, which travel in different register files.
1051 const args = [_]jvalue{
1052 .{ .l = @ptrCast(&name_string) },
1053 .{ .i = 0 },
1054 };
1055 const result = try jni.callObjectMethodA(@ptrCast(&receiver), @ptrCast(&method), &args);
1056
1057 try testing.expect(result != null);
1058 try testing.expectEqual(@as(jobject, @ptrCast(&receiver)), fake_called_obj);
1059 try testing.expectEqual(@as(usize, 2), fake_arg_count);
1060 try testing.expectEqual(@as(jobject, @ptrCast(&name_string)), fake_seen_args[0].l);
1061 try testing.expectEqual(@as(jint, 0), fake_seen_args[1].i);
1062}
1063
1064test "instance fields read through the slots they name" {
1065 resetFakes();
1066 var table = emptyTable();
1067 table.FindClass = @ptrCast(&fakeFindClass);
1068 table.GetFieldID = @ptrCast(&fakeFieldId);
1069 table.GetIntField = @ptrCast(&fakeIntField);
1070 table.GetFloatField = @ptrCast(&fakeFloatField);
1071 table.GetLongField = @ptrCast(&fakeLongField);
1072 table.ExceptionOccurred = @ptrCast(&fakeExceptionOccurred);
1073
1074 const ptr: *const JNINativeInterface = &table;
1075 const jni = Jni.init(&ptr);
1076
1077 const cls = try jni.findClass("android/util/DisplayMetrics");
1078 var obj: u8 = 0;
1079 const metrics: jobject = @ptrCast(&obj);
1080
1081 // Three different widths off three adjacent table slots — an off-by-one in
1082 // the transcription would read the wrong one and still return a number.
1083 try testing.expectEqual(@as(jint, 1080), try jni.intField(metrics, try jni.fieldId(cls, "widthPixels", "I")));
1084 try testing.expectEqual(@as(jfloat, 2.75), try jni.floatField(metrics, try jni.fieldId(cls, "density", "F")));
1085 try testing.expectEqual(@as(jlong, 4_000_000_123), try jni.longField(metrics, try jni.fieldId(cls, "longVersionCode", "J")));
1086}
1087
1088test "a field read that throws reports the exception, not the value" {
1089 resetFakes();
1090 var table = emptyTable();
1091 table.GetIntField = @ptrCast(&fakeIntField);
1092 table.ExceptionOccurred = @ptrCast(&fakeExceptionOccurred);
1093 table.ExceptionDescribe = @ptrCast(&fakeExceptionDescribe);
1094 table.ExceptionClear = @ptrCast(&fakeExceptionClear);
1095 table.DeleteLocalRef = @ptrCast(&fakeDeleteLocalRef);
1096
1097 const ptr: *const JNINativeInterface = &table;
1098 const jni = Jni.init(&ptr);
1099
1100 var throwable: u8 = 0;
1101 fake_pending_exception = @ptrCast(&throwable);
1102 var obj: u8 = 0;
1103 try testing.expectError(JniError.JavaException, jni.intField(@ptrCast(&obj), null));
1104}