Skip to content

Commit 45c6df6

Browse files
committed
fix: tag static UIAppearance selector results
1 parent 3ad3920 commit 45c6df6

8 files changed

Lines changed: 164 additions & 13 deletions

File tree

HANDOFF.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,33 @@ During this thread `agent-device` was version `0.18.0`; the npm package is
111111

112112
## Current Verified State
113113

114+
Update from 2026-06-29 08:28 EDT:
115+
116+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
117+
- GitHub Actions run `28369500611` on `3ad3920a` kept macOS green and failed
118+
iOS only in `ApiTests.js Appearance`:
119+
`UILabel.appearance().textColor` still read back `undefined`.
120+
- Follow-up runtime fix in progress:
121+
- Static `appearance*` selector results are now tagged at the prepared
122+
selector / selector-group return path, not only at the class-host
123+
`appearance` wrapper.
124+
- V8/JSC/QuickJS/Hermes generated-dispatch fast paths now route static
125+
`appearance*` returns through tag-aware paths, so UIAppearance setter
126+
caches can key values by the target native class.
127+
- Local verification after this patch:
128+
- `npm run build:macos-cli` passed and compiled the edited V8 bridge.
129+
- Runtime RN JS tests passed:
130+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
131+
- `npm run check:ffi-boundaries` passed.
132+
- `git diff --check` passed.
133+
- `npm run build:ios-sim` and a JSC macOS CLI compile attempt were blocked
134+
before runtime compilation by the known local metadata-generator
135+
`libclang.dylib` x86_64/arm64 mismatch.
136+
- Not done:
137+
- Commit/push this static UIAppearance selector-tag fix and watch fresh PR
138+
CI.
139+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
140+
114141
Update from 2026-06-29 04:45 EDT:
115142

116143
- Simulator-only rule was reaffirmed. Do not use physical iPhone/iPad devices

NativeScript/ffi/hermes/NativeApiJsi.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ throw JSError(runtime,
260260
// disown handling, or implicit NSError-out argument).
261261
if (prepared->gsdEngineCallable && gsdDispatchClass == Nil &&
262262
count == prepared->gsdEngineArgumentCount &&
263-
!(!receiverIsClass && prepared->isInitMethod)) {
263+
!(!receiverIsClass && prepared->isInitMethod) &&
264+
!isPreparedStaticAppearanceSelector(*prepared)) {
264265
auto invoker =
265266
reinterpret_cast<ObjCGsdInvoker>(prepared->engineInvoker);
266267
GsdObjCContext ctx{runtime, bridge, receiver, prepared->selector,

NativeScript/ffi/jsc/NativeApiJSCSelectorGroups.mm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ throw JSError(
106106
// return value — bypassing all generic marshalling.
107107
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
108108
providedCount == prepared.gsdEngineArgumentCount &&
109-
!initializerClassWrapper && !isNSErrorOutMethod) {
109+
!initializerClassWrapper && !isNSErrorOutMethod &&
110+
!isPreparedStaticAppearanceSelector(prepared)) {
110111
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
111112
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector,
112113
runtime.context(), arguments, signature.returnType};
@@ -125,6 +126,8 @@ throw JSError(
125126
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
126127
fastArgs, providedCount, Nil,
127128
&fastResult)) {
129+
fastResult = tagPreparedStaticAppearanceSelectorResult(
130+
runtime, bridge, receiver, prepared, std::move(fastResult));
128131
return fastResult.local(runtime);
129132
}
130133
}
@@ -209,6 +212,8 @@ throw JSError(
209212
Value(runtime, *initializerClassWrapper));
210213
}
211214
}
215+
tagPreparedStaticAppearanceNativeReturn(
216+
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
212217
return setJSCEngineReturnValue(runtime, bridge, returnType,
213218
returnStorage.data(), prepared.selectorName);
214219
}

NativeScript/ffi/quickjs/NativeApiQuickJSSelectorGroups.mm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ throw JSError(
105105
// return value — bypassing all generic marshalling.
106106
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
107107
providedCount == prepared.gsdEngineArgumentCount &&
108-
!initializerClassWrapper && !isNSErrorOutMethod) {
108+
!initializerClassWrapper && !isNSErrorOutMethod &&
109+
!isPreparedStaticAppearanceSelector(prepared)) {
109110
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
110111
GsdObjCContext ctx{runtime, bridge, receiver, prepared.selector,
111112
runtime.context(), arguments, signature.returnType};
@@ -124,6 +125,8 @@ throw JSError(
124125
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
125126
fastArgs, providedCount, Nil,
126127
&fastResult)) {
128+
fastResult = tagPreparedStaticAppearanceSelectorResult(
129+
runtime, bridge, receiver, prepared, std::move(fastResult));
127130
return fastResult.local(runtime);
128131
}
129132
}
@@ -209,6 +212,8 @@ throw JSError(
209212
Value(runtime, *initializerClassWrapper));
210213
}
211214
}
215+
tagPreparedStaticAppearanceNativeReturn(
216+
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
212217
return setQuickJSEngineReturnValue(runtime, bridge, returnType,
213218
returnStorage.data(),
214219
prepared.selectorName);

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1847,6 +1847,43 @@ throw JSError(
18471847
std::shared_ptr<NativeApiObjectLifetimeState> lifetimeState_;
18481848
};
18491849

1850+
bool isStaticAppearanceSelector(bool receiverIsClass,
1851+
const std::string& selectorName) {
1852+
return receiverIsClass && selectorName.rfind("appearance", 0) == 0;
1853+
}
1854+
1855+
void tagStaticAppearanceNativeResult(
1856+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
1857+
Class appearanceClass, id native) {
1858+
if (bridge == nullptr || appearanceClass == Nil || native == nil) {
1859+
return;
1860+
}
1861+
const char* className = class_getName(appearanceClass);
1862+
if (className == nullptr || className[0] == '\0') {
1863+
return;
1864+
}
1865+
bridge->setObjectExpando(runtime, native, kNativeApiAppearanceClassNameExpando,
1866+
makeString(runtime, className));
1867+
}
1868+
1869+
Value tagStaticAppearanceSelectorResult(
1870+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
1871+
id receiver, bool receiverIsClass, const std::string& selectorName,
1872+
Value result) {
1873+
if (!isStaticAppearanceSelector(receiverIsClass, selectorName) ||
1874+
!result.isObject()) {
1875+
return result;
1876+
}
1877+
Object resultObject = result.asObject(runtime);
1878+
if (!resultObject.isHostObject<NativeApiObjectHostObject>(runtime)) {
1879+
return result;
1880+
}
1881+
tagStaticAppearanceNativeResult(
1882+
runtime, bridge, static_cast<Class>(receiver),
1883+
resultObject.getHostObject<NativeApiObjectHostObject>(runtime)->object());
1884+
return result;
1885+
}
1886+
18501887
class NativeApiClassHostObject final : public HostObject {
18511888
public:
18521889
NativeApiClassHostObject(std::shared_ptr<NativeApiBridge> bridge,

NativeScript/ffi/shared/bridge/Invocation.mm

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,34 @@ throw JSError(
14871487
return prepared;
14881488
}
14891489

1490+
bool isPreparedStaticAppearanceSelector(
1491+
const NativeApiPreparedObjCInvocation& prepared) {
1492+
return prepared.receiverClass != Nil &&
1493+
prepared.selectorName.rfind("appearance", 0) == 0;
1494+
}
1495+
1496+
Value tagPreparedStaticAppearanceSelectorResult(
1497+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
1498+
id receiver, const NativeApiPreparedObjCInvocation& prepared,
1499+
Value result) {
1500+
return tagStaticAppearanceSelectorResult(
1501+
runtime, bridge, receiver, isPreparedStaticAppearanceSelector(prepared),
1502+
prepared.selectorName, std::move(result));
1503+
}
1504+
1505+
void tagPreparedStaticAppearanceNativeReturn(
1506+
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
1507+
id receiver, const NativeApiPreparedObjCInvocation& prepared,
1508+
const NativeApiType& returnType, void* returnData) {
1509+
if (!isPreparedStaticAppearanceSelector(prepared) ||
1510+
!isObjectiveCObjectType(returnType) || returnData == nullptr) {
1511+
return;
1512+
}
1513+
tagStaticAppearanceNativeResult(
1514+
runtime, bridge, static_cast<Class>(receiver),
1515+
*static_cast<id*>(returnData));
1516+
}
1517+
14901518
Value callPreparedObjCSelector(
14911519
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
14921520
id receiver, bool receiverIsClass,
@@ -1503,11 +1531,13 @@ throw JSError(runtime,
15031531
if (tryCallGeneratedEngineObjCSelector(runtime, bridge, receiver, prepared,
15041532
args, count, dispatchSuperClass,
15051533
&fastResult)) {
1506-
return fastResult;
1534+
return tagPreparedStaticAppearanceSelectorResult(
1535+
runtime, bridge, receiver, prepared, std::move(fastResult));
15071536
}
15081537
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared, args,
15091538
count, dispatchSuperClass, &fastResult)) {
1510-
return fastResult;
1539+
return tagPreparedStaticAppearanceSelectorResult(
1540+
runtime, bridge, receiver, prepared, std::move(fastResult));
15111541
}
15121542

15131543
NativeApiArgumentFrame frame(signature.argumentTypes.size());
@@ -1594,8 +1624,10 @@ NativeApiReturnStorage returnStorage(
15941624
throw JSError(
15951625
runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError");
15961626
}
1597-
return convertNativeReturnValue(runtime, bridge, returnType,
1598-
returnStorage.data());
1627+
Value result = convertNativeReturnValue(runtime, bridge, returnType,
1628+
returnStorage.data());
1629+
return tagPreparedStaticAppearanceSelectorResult(
1630+
runtime, bridge, receiver, prepared, std::move(result));
15991631
}
16001632

16011633
Value callObjCSelector(Runtime& runtime,
@@ -1666,12 +1698,16 @@ throw JSError(
16661698
if (tryCallGeneratedEngineObjCSelector(runtime, bridge, receiver,
16671699
engineInvocation, args, count,
16681700
dispatchSuperClass, &fastResult)) {
1669-
return fastResult;
1701+
return tagStaticAppearanceSelectorResult(
1702+
runtime, bridge, receiver, receiverIsClass, selectorName,
1703+
std::move(fastResult));
16701704
}
16711705
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver,
16721706
engineInvocation, args, count,
16731707
dispatchSuperClass, &fastResult)) {
1674-
return fastResult;
1708+
return tagStaticAppearanceSelectorResult(
1709+
runtime, bridge, receiver, receiverIsClass, selectorName,
1710+
std::move(fastResult));
16751711
}
16761712

16771713
NativeApiArgumentFrame frame(signature->argumentTypes.size());
@@ -1759,6 +1795,9 @@ NativeApiReturnStorage returnStorage(
17591795
throw JSError(
17601796
runtime, errorMessage != nullptr ? errorMessage : "Unknown NSError");
17611797
}
1762-
return convertNativeReturnValue(runtime, bridge, returnType,
1763-
returnStorage.data());
1798+
Value result = convertNativeReturnValue(runtime, bridge, returnType,
1799+
returnStorage.data());
1800+
return tagStaticAppearanceSelectorResult(
1801+
runtime, bridge, receiver, receiverIsClass, selectorName,
1802+
std::move(result));
17641803
}

NativeScript/ffi/v8/NativeApiV8SelectorGroups.mm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ throw JSError(
9494
// generic marshalling.
9595
if (prepared.gsdEngineCallable && dispatchSuperClass == Nil &&
9696
providedCount == prepared.gsdEngineArgumentCount &&
97-
!initializerClassWrapper && !isNSErrorOutMethod) {
97+
!initializerClassWrapper && !isNSErrorOutMethod &&
98+
!isPreparedStaticAppearanceSelector(prepared)) {
9899
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared.engineInvoker);
99100
GsdObjCContext ctx{runtime,
100101
bridge,
@@ -119,6 +120,8 @@ throw JSError(
119120
if (tryCallFastEngineObjCSelector(runtime, bridge, receiver, prepared,
120121
fastArgs, providedCount, Nil,
121122
&fastResult)) {
123+
fastResult = tagPreparedStaticAppearanceSelectorResult(
124+
runtime, bridge, receiver, prepared, std::move(fastResult));
122125
info.GetReturnValue().Set(fastResult.local(runtime));
123126
return;
124127
}
@@ -204,6 +207,8 @@ throw JSError(
204207
Value(runtime, *initializerClassWrapper));
205208
}
206209
}
210+
tagPreparedStaticAppearanceNativeReturn(
211+
runtime, bridge, receiver, prepared, returnType, returnStorage.data());
207212
setV8EngineReturnValue(runtime, bridge, returnType, returnStorage.data(),
208213
prepared.selectorName, info);
209214
}
@@ -372,7 +377,8 @@ throw JSError(runtime,
372377
// generated invoker reads args, calls objc_msgSend, and sets the return.
373378
if (prepared->gsdEngineCallable && dispatchClass == Nil &&
374379
!prepared->isInitMethod &&
375-
count == prepared->gsdEngineArgumentCount) {
380+
count == prepared->gsdEngineArgumentCount &&
381+
!isPreparedStaticAppearanceSelector(*prepared)) {
376382
auto invoker = reinterpret_cast<ObjCGsdInvoker>(prepared->engineInvoker);
377383
GsdObjCContext ctx{runtime,
378384
data->bridge,

PROGRESS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,37 @@ TypeScript/UI worklets.
1717

1818
## Latest Update - 2026-06-29
1919

20+
### 2026-06-29 08:28 EDT - patched static UIAppearance selector result tagging
21+
22+
- Goal:
23+
- Keep PR #46 on the RN-module runtime primitive path and unblock the final
24+
simulator-only RNS parity sweep; no physical devices were used.
25+
- CI finding:
26+
- GitHub Actions run `28369500611` kept macOS green but still failed iOS
27+
`ApiTests.js Appearance` only:
28+
`UILabel.appearance().textColor` read back as `undefined`.
29+
- The custom JS selector regression stayed fixed, confirming broad
30+
get-time Objective-C probing should remain out of the object hot path.
31+
- Changes:
32+
- Static `appearance*` selector results are now tagged at the prepared
33+
selector / selector-group return path, including V8/JSC/QuickJS/Hermes
34+
generated-dispatch fast paths.
35+
- UIAppearance setter caches can therefore key values by the target class
36+
even when `UILabel.appearance()` is served by the generated selector-group
37+
path instead of the class-host `appearance` wrapper.
38+
- Verification:
39+
- Runtime RN JS tests passed:
40+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
41+
- `npm run check:ffi-boundaries` passed.
42+
- `git diff --check` passed.
43+
- `npm run build:macos-cli` passed, proving the edited V8 bridge compiles.
44+
- `npm run build:ios-sim` and a JSC macOS CLI compile attempt both stopped
45+
before runtime compilation on the known local metadata-generator
46+
`libclang.dylib` x86_64/arm64 mismatch.
47+
- Still next:
48+
- Commit/push the selector tag fix, watch fresh PR CI, then return to the
49+
dedicated simulator-only RNS parity sweep if CI is green.
50+
2051
### 2026-06-29 04:45 EDT - fixed macOS CI dynamic class identity blocker
2152

2253
- Goal:

0 commit comments

Comments
 (0)