also looking at this
feat(android): getDeviceInfo in Zig, and the JNI calls it needed
#137
3 files
+741
-0
| @@ -80,6 +80,23 @@ pub const jthrowable = jobject; | ||
| 80 | 80 | pub const jmethodID = ?*anyopaque; |
| 81 | 81 | pub const jfieldID = ?*anyopaque; |
| 82 | 82 | |
| 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. | |
| 88 | pub 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 | ||
| 83 | 100 | /// `JNIEnv*` as the JVM passes it to a native method. |
| 84 | 101 | pub const JNIEnv = *const *const JNINativeInterface; |
| 85 | 102 | |
| @@ -445,6 +462,104 @@ pub const Jni = struct { | ||
| 445 | 462 | return result; |
| 446 | 463 | } |
| 447 | 464 | |
| 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 | ||
| 448 | 563 | pub fn callIntMethod(self: Self, obj: jobject, id: jmethodID) JniError!jint { |
| 449 | 564 | const call: *const fn (JNIEnv, jobject, jmethodID) callconv(.c) jint = |
| 450 | 565 | @ptrCast(self.table().CallIntMethod orelse return JniError.NotFound); |
| @@ -459,6 +574,31 @@ pub const Jni = struct { | ||
| 459 | 574 | return get(self.env, obj) orelse JniError.NotFound; |
| 460 | 575 | } |
| 461 | 576 | |
| 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 | ||
| 462 | 602 | pub fn deleteLocalRef(self: Self, obj: jobject) void { |
| 463 | 603 | if (obj == null) return; |
| 464 | 604 | const del: *const fn (JNIEnv, jobject) callconv(.c) void = |
| @@ -843,3 +983,122 @@ test "modified UTF-8 decodes to the standard encoding" { | ||
| 843 | 983 | try testing.expectError(JniError.MalformedString, decodeModifiedUtf8(alloc, "\xE6\x97")); |
| 844 | 984 | try testing.expectError(JniError.MalformedString, decodeModifiedUtf8(alloc, "\xFF")); |
| 845 | 985 | } |
| 986 | ||
| 987 | // --- Argument passing ------------------------------------------------------ | |
| 988 | ||
| 989 | var fake_seen_args: [4]jvalue = undefined; | |
| 990 | var fake_arg_count: usize = 0; | |
| 991 | var fake_called_obj: jobject = null; | |
| 992 | var fake_called_id: jmethodID = null; | |
| 993 | ||
| 994 | var fake_returned: u8 = 0; | |
| 995 | fn 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 | ||
| 1005 | fn fakeIntField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jint { | |
| 1006 | return 1080; | |
| 1007 | } | |
| 1008 | fn fakeFloatField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jfloat { | |
| 1009 | return 2.75; | |
| 1010 | } | |
| 1011 | fn fakeLongField(_: JNIEnv, _: jobject, _: jfieldID) callconv(.c) jlong { | |
| 1012 | return 4_000_000_123; | |
| 1013 | } | |
| 1014 | fn fakeFieldId(_: JNIEnv, _: jclass, _: [*:0]const u8, _: [*:0]const u8) callconv(.c) jfieldID { | |
| 1015 | return @ptrCast(&fake_field); | |
| 1016 | } | |
| 1017 | ||
| 1018 | test "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 | ||
| 1035 | test "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 | ||
| 1064 | test "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 | ||
| 1088 | test "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 | } | |