ReviewOS

also looking at this

craft-native/craft

feat(android): the motion pair, and org.json's numbers — ratchet 54 → 52

#187
Merged glennmichael123 wants to merge feat/android-motion into main
7 files +577 -3
packages/zig/src/android_json_number.zigadded+98-0
Changes to packages/zig/src/android_json_number.zig
@@ -0,0 +1,98 @@
1//! Numbers, printed the way `org.json` prints them.
2//!
3//! Four migrated modules have declined to format a float, and the reason is
4//! always the same: `JSONObject` does not print numbers with a format string.
5//! Android's `numberToString` is
6//!
7//! ```java
8//! double doubleValue = number.doubleValue();
9//! JSON.checkDouble(doubleValue); // throws on NaN, ±Inf
10//! if (number.equals(NEGATIVE_ZERO)) return "-0";
11//! long longValue = number.longValue();
12//! if (doubleValue == (double) longValue) return Long.toString(longValue);
13//! return number.toString(); // Float or Double
14//! ```
15//!
16//! Three of those five lines are decisions a reimplementation would have to
17//! rediscover — an integral double prints as an integer, negative zero prints
18//! as `-0`, and the fallback is `Float.toString` or `Double.toString`
19//! depending on which box the number arrived in, which are *different*
20//! functions for the same value.
21//!
22//! So this asks Java. It is the same judgement `bridge_android_notifications`
23//! makes about `hashCode` and `bridge_android_files` about `Base64.decode`:
24//! where the platform already has the thing, ask it rather than rebuild it —
25//! and here rebuilding means reproducing the shortest-round-trip algorithm for
26//! two float widths, where being wrong is a silently different number rather
27//! than a failure.
28//!
29//! ## What it costs
30//!
31//! Three JNI calls per number: box, format, read back. `getCurrentPosition`
32//! makes seven of those once. A motion stream at 50 Hz makes six per event,
33//! which is 900 calls a second — real, and still far below what the sensor
34//! delivery itself costs.
35
36const std = ;
37const jni = ;
38
39const Jni = jni.Jni;
40
41/// `JSONObject.numberToString(Float.valueOf(value))`.
42pub fn fromFloat(j: Jni, allocator: std.mem.Allocator, value: f32) ![]u8 {
43 try j.pushLocalFrame(8);
44 defer _ = j.popLocalFrame(null);
45
46 const float_cls = try j.findClass("java/lang/Float");
47 const boxed = try j.callStaticObjectMethodA(
48 float_cls,
49 try j.staticMethodId(float_cls, "valueOf", "(F)Ljava/lang/Float;"),
50 &.{.{ .f = value }},
51 );
52 return numberToString(j, allocator, boxed);
53}
54
55/// `JSONObject.numberToString(Double.valueOf(value))`.
56///
57/// A separate function rather than a widened `fromFloat`, because the two do
58/// not agree: `Float.toString(0.1f)` is "0.1" and `Double.toString(0.1f)` is
59/// "0.10000000149011612". Which box a number arrives in is part of what the
60/// shim's output means.
61pub fn fromDouble(j: Jni, allocator: std.mem.Allocator, value: f64) ![]u8 {
62 try j.pushLocalFrame(8);
63 defer _ = j.popLocalFrame(null);
64
65 const double_cls = try j.findClass("java/lang/Double");
66 const boxed = try j.callStaticObjectMethodA(
67 double_cls,
68 try j.staticMethodId(double_cls, "valueOf", "(D)Ljava/lang/Double;"),
69 &.{.{ .d = value }},
70 );
71 return numberToString(j, allocator, boxed);
72}
73
74fn numberToString(j: Jni, allocator: std.mem.Allocator, boxed: jni.jobject) ![]u8 {
75 const json_cls = try j.findClass("org/json/JSONObject");
76 const text = try j.callStaticObjectMethodA(
77 json_cls,
78 try j.staticMethodId(
79 json_cls,
80 "numberToString",
81 "(Ljava/lang/Number;)Ljava/lang/String;",
82 ),
83 &.{.{ .l = boxed }},
84 );
85 return j.stringToUtf8(allocator, text);
86}
87
88// No tests, deliberately.
89//
90// Everything here is three JNI calls and a descriptor; there is no formatting
91// to check, because not formatting is the point. A host test would have to
92// stand up a fake `numberToString` and would then be asserting that a fake
93// agrees with itself.
94//
95// What *is* checked lives with the callers: `bridge_android_motion` tests the
96// shape of the object these numbers go into, with the formatted strings
97// injected, so the part this file does not own is tested and the part it does
98// own is Java's.