Skip to content

Commit ac94954

Browse files
committed
fix: avoid UIAppearance proxy getter hangs
1 parent c7454bc commit ac94954

4 files changed

Lines changed: 193 additions & 29 deletions

File tree

HANDOFF.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,45 @@ 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 13:21 EDT:
115+
116+
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.
117+
- GitHub Actions run `28387410928` on `c7454bc4` kept setup, dependency
118+
install, FFI boundary, V8 download, libffi build, metadata generation,
119+
NativeScript build, CLI build, and macOS tests green, but iOS timed out
120+
without a Jasmine summary or `junit-result.xml`.
121+
- The timeout happened after `Application Start!` and five skipped-spec `S`
122+
markers. The previous failing run reached the same point and then reported
123+
`ApiTests.js Appearance`, so the target-prototype experiment likely fixed
124+
the old `undefined` readback only by letting the test fall through to a
125+
forwarded UIKit appearance getter hang.
126+
- Current runtime fix:
127+
- Static `appearance*` results still tag the native proxy with the
128+
customizable target class.
129+
- Returned `UIAppearance` proxies now get own JS accessors for target-class
130+
metadata properties instead of having their prototype replaced with the
131+
target class prototype.
132+
- Setters forward to UIKit and populate the target-class-scoped appearance
133+
cache; getters read that cache only, avoiding speculative native getter
134+
calls on UIKit's forwarded proxy.
135+
- Source coverage now requires the safe accessor install and blocks
136+
`SetNativeApiObjectPrototype(runtime, resultObject, ...)` for appearance
137+
proxy results.
138+
- Local verification after this correction:
139+
- Runtime RN JS tests passed:
140+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
141+
- `npm run check:ffi-boundaries` passed.
142+
- `git diff --check` passed.
143+
- `npm run build:macos-cli` passed.
144+
- Focused simulator attempt on
145+
`BF759806-2EBB-49ED-AD8E-413A7790ADE0`:
146+
`IOS_DESTINATION=BF759806-2EBB-49ED-AD8E-413A7790ADE0 IOS_TESTS=ApiTests IOS_SPECS=Appearance IOS_TEST_VERBOSE_SPECS=1 IOS_LOG_JUNIT=1 IOS_TEST_TIMEOUT_MS=180000 IOS_TEST_INACTIVITY_TIMEOUT_MS=60000 npm run test:ios`
147+
was blocked before app launch by the known local metadata-generator
148+
`libclang.dylib` x86_64/arm64 mismatch.
149+
- Not done:
150+
- Commit/push the safe UIAppearance accessor correction and watch fresh PR CI.
151+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
152+
114153
Update from 2026-06-29 12:30 EDT:
115154

116155
- Simulator-only rule remains active. Do not use physical iPhone/iPad devices.

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,32 +1919,111 @@ Class tagStaticAppearanceNativeResult(
19191919
return customizableClass;
19201920
}
19211921

1922-
void installAppearanceProxyTargetPrototype(
1922+
std::shared_ptr<void> retainAppearanceProxyForAccessor(id native) {
1923+
id retained = [native retain];
1924+
return std::shared_ptr<void>(static_cast<void*>(retained), [](void* value) {
1925+
[(id)value release];
1926+
});
1927+
}
1928+
1929+
bool shouldInstallAppearanceProxyAccessor(const NativeApiMember& member) {
1930+
if (!member.property || member.name.empty()) {
1931+
return false;
1932+
}
1933+
bool memberIsStatic = (member.flags & metagen::mdMemberStatic) != 0;
1934+
if (memberIsStatic) {
1935+
return false;
1936+
}
1937+
return member.name != "superclass" && member.name != "class" &&
1938+
member.name != "constructor" && member.name != "debugDescription" &&
1939+
member.name != "className" && member.name != "description";
1940+
}
1941+
1942+
Function makeAppearanceProxyPropertyGetter(
1943+
Runtime& runtime, std::shared_ptr<NativeApiBridge> bridge, id native,
1944+
std::shared_ptr<void> retainedNative, std::string property) {
1945+
return Function::createFromHostFunction(
1946+
runtime, PropNameID::forAscii(runtime, property.c_str()), 0,
1947+
[bridge = std::move(bridge), native, retainedNative = std::move(retainedNative),
1948+
property = std::move(property)](Runtime& runtime, const Value&,
1949+
const Value*, size_t) -> Value {
1950+
return cachedAppearanceProxyPropertyValue(runtime, bridge, native,
1951+
property);
1952+
});
1953+
}
1954+
1955+
Function makeAppearanceProxyPropertySetter(
1956+
Runtime& runtime, std::shared_ptr<NativeApiBridge> bridge, id native,
1957+
std::shared_ptr<void> retainedNative, NativeApiMember member) {
1958+
std::string functionName = member.setterSelectorName.empty()
1959+
? member.name
1960+
: member.setterSelectorName;
1961+
return Function::createFromHostFunction(
1962+
runtime, PropNameID::forAscii(runtime, functionName.c_str()), 1,
1963+
[bridge = std::move(bridge), native, retainedNative = std::move(retainedNative),
1964+
member = std::move(member)](Runtime& runtime, const Value&, const Value* args,
1965+
size_t count) -> Value {
1966+
if (count < 1) {
1967+
throw JSError(runtime,
1968+
"UIAppearance property setter expects a value.");
1969+
}
1970+
NativeApiMember setterMember = member;
1971+
setterMember.selectorName = member.setterSelectorName;
1972+
setterMember.signatureOffset = member.setterSignatureOffset;
1973+
Value setterArgs[] = {Value(runtime, args[0])};
1974+
callObjCSelector(runtime, bridge, native, false,
1975+
setterMember.selectorName, &setterMember, setterArgs,
1976+
1);
1977+
cacheAppearanceProxyPropertyValue(runtime, bridge, native, member.name,
1978+
args[0]);
1979+
return Value::undefined();
1980+
});
1981+
}
1982+
1983+
void installAppearanceProxyPropertyAccessors(
19231984
Runtime& runtime, const std::shared_ptr<NativeApiBridge>& bridge,
1924-
Class customizableClass, Object& resultObject) {
1985+
Class customizableClass, id native, Object& resultObject) {
19251986
if (bridge == nullptr || customizableClass == Nil) {
19261987
return;
19271988
}
1989+
const NativeApiSymbol* symbol =
1990+
bridge->findClassForRuntimeClass(customizableClass);
1991+
if (symbol == nullptr) {
1992+
return;
1993+
}
19281994

1929-
Value prototypeValue =
1930-
bridge->findClassPrototype(runtime, customizableClass);
1931-
if (!prototypeValue.isObject()) {
1932-
Value classWrapperValue =
1933-
bridge->findClassValue(runtime, customizableClass);
1934-
if (!classWrapperValue.isObject()) {
1935-
classWrapperValue = makeNativeClassValue(
1936-
runtime, bridge,
1937-
nativeApiSymbolForRuntimeClass(bridge, customizableClass));
1938-
}
1939-
if (classWrapperValue.isObject()) {
1940-
prototypeValue =
1941-
classWrapperValue.asObject(runtime).getProperty(runtime, "prototype");
1995+
Object objectConstructor =
1996+
runtime.global().getPropertyAsObject(runtime, "Object");
1997+
Function defineProperty =
1998+
objectConstructor.getPropertyAsFunction(runtime, "defineProperty");
1999+
std::unordered_set<std::string> installed;
2000+
std::shared_ptr<void> retainedNative =
2001+
retainAppearanceProxyForAccessor(native);
2002+
const auto& members = bridge->membersForClass(*symbol);
2003+
for (const auto& member : members) {
2004+
if (!shouldInstallAppearanceProxyAccessor(member) ||
2005+
!installed.insert(member.name).second) {
2006+
continue;
19422007
}
1943-
}
19442008

1945-
if (prototypeValue.isObject()) {
1946-
Object prototype = prototypeValue.asObject(runtime);
1947-
SetNativeApiObjectPrototype(runtime, resultObject, prototype);
2009+
try {
2010+
Object descriptor(runtime);
2011+
descriptor.setProperty(runtime, "configurable", true);
2012+
descriptor.setProperty(runtime, "enumerable", false);
2013+
descriptor.setProperty(
2014+
runtime, "get",
2015+
makeAppearanceProxyPropertyGetter(runtime, bridge, native,
2016+
retainedNative, member.name));
2017+
if (!member.readonly && !member.setterSelectorName.empty()) {
2018+
descriptor.setProperty(
2019+
runtime, "set",
2020+
makeAppearanceProxyPropertySetter(runtime, bridge, native,
2021+
retainedNative, member));
2022+
}
2023+
defineProperty.call(runtime, resultObject, makeString(runtime, member.name),
2024+
descriptor);
2025+
} catch (const std::exception&) {
2026+
}
19482027
}
19492028
}
19502029

@@ -1963,8 +2042,10 @@ Value tagStaticAppearanceSelectorResult(
19632042
Class customizableClass = tagStaticAppearanceNativeResult(
19642043
runtime, bridge, static_cast<Class>(receiver),
19652044
resultObject.getHostObject<NativeApiObjectHostObject>(runtime)->object());
1966-
installAppearanceProxyTargetPrototype(runtime, bridge, customizableClass,
1967-
resultObject);
2045+
installAppearanceProxyPropertyAccessors(
2046+
runtime, bridge, customizableClass,
2047+
resultObject.getHostObject<NativeApiObjectHostObject>(runtime)->object(),
2048+
resultObject);
19682049
return result;
19692050
}
19702051

@@ -2220,9 +2301,8 @@ throw JSError(runtime,
22202301
Class customizableClass =
22212302
tagStaticAppearanceNativeResult(runtime, bridge, cls,
22222303
native);
2223-
installAppearanceProxyTargetPrototype(runtime, bridge,
2224-
customizableClass,
2225-
resultObject);
2304+
installAppearanceProxyPropertyAccessors(
2305+
runtime, bridge, customizableClass, native, resultObject);
22262306
}
22272307
}
22282308
return result;

PROGRESS.md

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

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

20+
### 2026-06-29 13:21 EDT - safe UIAppearance proxy accessors
21+
22+
- Goal:
23+
- Keep PR #46 on generic runtime primitives and stay simulator-only; no
24+
physical devices were used.
25+
- CI finding:
26+
- GitHub Actions run `28387410928` on `c7454bc4` kept setup, dependency
27+
install, FFI boundary check, V8 download, libffi build, metadata
28+
generation, NativeScript build, CLI build, and macOS tests green, but iOS
29+
timed out before a Jasmine summary or `junit-result.xml`.
30+
- The iOS log reached `Application Start!`, printed five skipped-spec `S`
31+
markers, then went silent until the 10 minute watchdog. The previous run
32+
reached the same point and failed in `ApiTests.js Appearance`, so the
33+
target-prototype approach likely moved the test into a forwarded UIKit
34+
appearance getter hang.
35+
- Changes:
36+
- Replaced target-class prototype mutation with own property accessors on
37+
returned `UIAppearance` proxies.
38+
- The accessors are installed from the customizable target class metadata:
39+
setters forward to UIKit and populate the target-class-scoped appearance
40+
cache; getters read only that cache and avoid speculative native getter
41+
calls on UIKit's forwarded proxy.
42+
- Source coverage now guards both the safe accessor install and the absence
43+
of `SetNativeApiObjectPrototype(runtime, resultObject, ...)` on appearance
44+
proxies.
45+
- Verification:
46+
- Runtime RN JS tests passed:
47+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
48+
- `npm run check:ffi-boundaries` passed.
49+
- `git diff --check` passed.
50+
- `npm run build:macos-cli` passed and compiled the edited V8 bridge.
51+
- Focused simulator attempt on the dedicated sim:
52+
`IOS_DESTINATION=BF759806-2EBB-49ED-AD8E-413A7790ADE0 IOS_TESTS=ApiTests IOS_SPECS=Appearance IOS_TEST_VERBOSE_SPECS=1 IOS_LOG_JUNIT=1 IOS_TEST_TIMEOUT_MS=180000 IOS_TEST_INACTIVITY_TIMEOUT_MS=60000 npm run test:ios`
53+
did not launch the app because local `build_metadata_generator.sh` still
54+
fails linking the x86_64 metadata generator against an arm64-only
55+
`libclang.dylib`.
56+
- Still next:
57+
- Commit/push the safe-accessor correction and watch fresh PR CI for the
58+
authoritative iOS simulator result.
59+
- If CI turns green, resume the dedicated simulator-only RNS parity sweep.
60+
2061
### 2026-06-29 12:30 EDT - installed UIAppearance target prototypes
2162

2263
- Goal:

packages/react-native/test/runtime-objc-property-setter.test.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ assert(
4747
"runtime UIAppearance result tagging should return the customizable target class",
4848
);
4949
assert(
50-
runtimeHostObjects.includes("installAppearanceProxyTargetPrototype") &&
51-
runtimeHostObjects.includes(
52-
"SetNativeApiObjectPrototype(runtime, resultObject, prototype);",
53-
),
54-
"runtime UIAppearance proxies should install the target class prototype",
50+
runtimeHostObjects.includes("installAppearanceProxyPropertyAccessors") &&
51+
runtimeHostObjects.includes("makeAppearanceProxyPropertySetter") &&
52+
runtimeHostObjects.includes("makeAppearanceProxyPropertyGetter") &&
53+
runtimeHostObjects.includes("cacheAppearanceProxyPropertyValue("),
54+
"runtime UIAppearance proxies should install safe property accessors backed by the appearance cache",
55+
);
56+
assert(
57+
!runtimeHostObjects.includes("SetNativeApiObjectPrototype(runtime, resultObject"),
58+
"runtime UIAppearance proxies should not replace their JS prototype with the target class prototype",
5559
);
5660

5761
console.log("runtime Objective-C property setter tests passed");

0 commit comments

Comments
 (0)