Skip to content

Commit dc038a4

Browse files
committed
fix: honor V8 native instance prototype accessors
1 parent 3087119 commit dc038a4

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

NativeScript/ffi/v8/NativeApiV8HostObjects.mm

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,101 @@ void emplace(size_t index, Value&& value) {
4747
alignas(Value) unsigned char inlineStorage_[sizeof(Value) * InlineCount];
4848
};
4949

50+
bool findPrototypeDescriptor(Runtime& runtime, v8::Local<v8::Object> object,
51+
v8::Local<v8::Name> property,
52+
v8::Local<v8::Object>* descriptorOut) {
53+
v8::TryCatch tryCatch(runtime.isolate());
54+
v8::Local<v8::Value> currentValue = object->GetPrototypeV2();
55+
for (size_t depth = 0; depth < 64 && currentValue->IsObject(); depth++) {
56+
v8::Local<v8::Object> current = currentValue.As<v8::Object>();
57+
v8::Local<v8::Value> descriptorValue;
58+
if (!current->GetOwnPropertyDescriptor(runtime.context(), property)
59+
.ToLocal(&descriptorValue)) {
60+
throw JSError(runtime,
61+
currentExceptionMessage(runtime.isolate(), tryCatch));
62+
}
63+
if (descriptorValue->IsObject()) {
64+
*descriptorOut = descriptorValue.As<v8::Object>();
65+
return true;
66+
}
67+
currentValue = current->GetPrototypeV2();
68+
}
69+
return false;
70+
}
71+
72+
bool tryResolvePrototypeGet(Runtime& runtime, v8::Local<v8::Object> object,
73+
v8::Local<v8::Object> receiver,
74+
v8::Local<v8::Name> property,
75+
v8::Local<v8::Value>* resultOut) {
76+
v8::Local<v8::Object> descriptor;
77+
if (!findPrototypeDescriptor(runtime, object, property, &descriptor)) {
78+
return false;
79+
}
80+
81+
v8::TryCatch tryCatch(runtime.isolate());
82+
v8::Local<v8::String> getKey = makeV8String(runtime.isolate(), "get");
83+
v8::Local<v8::Value> getterValue;
84+
if (!descriptor->Get(runtime.context(), getKey).ToLocal(&getterValue)) {
85+
throw JSError(runtime, currentExceptionMessage(runtime.isolate(), tryCatch));
86+
}
87+
if (getterValue->IsFunction()) {
88+
v8::Local<v8::Value> result;
89+
if (!getterValue.As<v8::Function>()
90+
->Call(runtime.context(), receiver, 0, nullptr)
91+
.ToLocal(&result)) {
92+
throw JSError(runtime,
93+
currentExceptionMessage(runtime.isolate(), tryCatch));
94+
}
95+
*resultOut = result;
96+
return true;
97+
}
98+
99+
v8::Local<v8::String> valueKey = makeV8String(runtime.isolate(), "value");
100+
bool hasValue =
101+
descriptor->HasOwnProperty(runtime.context(), valueKey).FromMaybe(false);
102+
if (hasValue) {
103+
v8::Local<v8::Value> value;
104+
if (!descriptor->Get(runtime.context(), valueKey).ToLocal(&value)) {
105+
throw JSError(runtime,
106+
currentExceptionMessage(runtime.isolate(), tryCatch));
107+
}
108+
*resultOut = value;
109+
return true;
110+
}
111+
112+
*resultOut = v8::Undefined(runtime.isolate());
113+
return true;
114+
}
115+
116+
bool tryInvokePrototypeSetter(Runtime& runtime, v8::Local<v8::Object> object,
117+
v8::Local<v8::Object> receiver,
118+
v8::Local<v8::Name> property,
119+
v8::Local<v8::Value> value) {
120+
v8::Local<v8::Object> descriptor;
121+
if (!findPrototypeDescriptor(runtime, object, property, &descriptor)) {
122+
return false;
123+
}
124+
125+
v8::TryCatch tryCatch(runtime.isolate());
126+
v8::Local<v8::String> setKey = makeV8String(runtime.isolate(), "set");
127+
v8::Local<v8::Value> setterValue;
128+
if (!descriptor->Get(runtime.context(), setKey).ToLocal(&setterValue)) {
129+
throw JSError(runtime, currentExceptionMessage(runtime.isolate(), tryCatch));
130+
}
131+
if (!setterValue->IsFunction()) {
132+
return false;
133+
}
134+
135+
v8::Local<v8::Value> args[] = {value};
136+
v8::Local<v8::Value> ignored;
137+
if (!setterValue.As<v8::Function>()
138+
->Call(runtime.context(), receiver, 1, args)
139+
.ToLocal(&ignored)) {
140+
throw JSError(runtime, currentExceptionMessage(runtime.isolate(), tryCatch));
141+
}
142+
return true;
143+
}
144+
50145
v8::Local<v8::ObjectTemplate> hostObjectTemplate(Runtime& runtime) {
51146
auto state = runtime.state();
52147
if (state->hostObjectTemplate.IsEmpty()) {
@@ -251,6 +346,15 @@ void emplace(size_t index, Value&& value) {
251346
if (*utf8 == nullptr) {
252347
return v8::Intercepted::kNo;
253348
}
349+
v8::Local<v8::Object> holderObject = info.Holder();
350+
v8::Local<v8::Object> receiver =
351+
info.This()->IsObject() ? info.This().As<v8::Object>() : holderObject;
352+
v8::Local<v8::Value> prototypeResult;
353+
if (tryResolvePrototypeGet(runtime, holderObject, receiver,
354+
property, &prototypeResult)) {
355+
info.GetReturnValue().Set(prototypeResult);
356+
return v8::Intercepted::kYes;
357+
}
254358
Value result = holder->hostObject->get(
255359
runtime, PropNameID(std::string(*utf8, utf8.length())));
256360
if (!result.isUndefined()) {
@@ -280,6 +384,13 @@ void emplace(size_t index, Value&& value) {
280384
if (*utf8 == nullptr) {
281385
return v8::Intercepted::kNo;
282386
}
387+
v8::Local<v8::Object> holderObject = info.Holder();
388+
v8::Local<v8::Object> receiver =
389+
info.This()->IsObject() ? info.This().As<v8::Object>() : holderObject;
390+
if (tryInvokePrototypeSetter(runtime, holderObject, receiver,
391+
property, value)) {
392+
return v8::Intercepted::kYes;
393+
}
283394
bool handled = holder->hostObject->set(
284395
runtime, PropNameID(std::string(*utf8, utf8.length())),
285396
Value(runtime, value));

PROGRESS.md

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

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

20+
### 2026-06-30 03:57 EDT - V8 native instance interceptors honor JS prototype accessors
21+
22+
- Scope check:
23+
- Still generic NativeScript runtime behavior required by the RN-module
24+
branch. No RNS-specific native implementation and no direct-engine backend
25+
refactor work.
26+
- Simulator-only rule remains active; no physical devices were used.
27+
- CI finding:
28+
- GitHub Actions run `28427007755` on `30871198` kept setup, FFI boundary
29+
check, native build, CLI build, and macOS tests green.
30+
- iOS still had the same single failure:
31+
`ApiTests.js SpecialCaseProperty_When_CustomSelector_ImplementedInJS`.
32+
- The shared host-object prototype fallback did not affect the V8 native
33+
instance path. V8 native instances use `kNonMasking` named property
34+
interceptors, so that interceptor must explicitly honor JS prototype
35+
accessors before dispatching into the native host object.
36+
- Change:
37+
- `NativeApiV8HostObjects.mm` now walks the native instance JS prototype
38+
chain with `GetOwnPropertyDescriptor`.
39+
- The native object named getter calls a JS prototype getter, or returns a
40+
prototype data descriptor value, before falling back to
41+
`holder->hostObject->get`.
42+
- The native object named setter calls a JS prototype setter before falling
43+
back to `holder->hostObject->set`.
44+
- Source coverage now guards that this precedence is attached to the V8
45+
native object template, not the generic host object template.
46+
- Verification:
47+
- Focused V8/native-interceptor source guard passed:
48+
`node packages/react-native/test/runtime-instance-selector-base-dispatch.test.js`.
49+
- Runtime RN JS tests passed:
50+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
51+
- `git diff --check` passed.
52+
- `npm run check:ffi-boundaries` passed.
53+
- Existing generated macOS project compile/link passed with
54+
`xcodebuild -project dist/intermediates/macos/NativeScript.xcodeproj ...`.
55+
- Still next:
56+
- Commit/push this V8 native-interceptor fix and watch fresh PR CI. If iOS
57+
goes green, resume the RNS simulator parity sweep from this handoff.
58+
2059
### 2026-06-30 03:12 EDT - JS subclass accessors use registered class prototype fallback
2160

2261
- Scope check:

packages/react-native/test/runtime-instance-selector-base-dispatch.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,63 @@ for (const relativePath of [
204204
);
205205
}
206206

207+
{
208+
const relativePath = "NativeScript/ffi/v8/NativeApiV8HostObjects.mm";
209+
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
210+
const hostTemplateStart = source.indexOf(
211+
"v8::Local<v8::ObjectTemplate> hostObjectTemplate",
212+
);
213+
const hostTemplateEnd = source.indexOf(
214+
"state->hostObjectTemplate.Reset",
215+
hostTemplateStart,
216+
);
217+
const nativeTemplateStart = source.indexOf(
218+
"v8::Local<v8::ObjectTemplate> nativeObjectTemplate",
219+
);
220+
const nativeTemplateEnd = source.indexOf(
221+
"state->nativeObjectTemplate.Reset",
222+
nativeTemplateStart,
223+
);
224+
const hostTemplate = source.slice(hostTemplateStart, hostTemplateEnd);
225+
const nativeTemplate = source.slice(nativeTemplateStart, nativeTemplateEnd);
226+
const getterIndex = nativeTemplate.indexOf(
227+
"tryResolvePrototypeGet(runtime, holderObject, receiver",
228+
);
229+
const nativeGetIndex = nativeTemplate.indexOf(
230+
"holder->hostObject->get",
231+
getterIndex,
232+
);
233+
const setterIndex = nativeTemplate.indexOf(
234+
"tryInvokePrototypeSetter(runtime, holderObject, receiver",
235+
);
236+
const nativeSetIndex = nativeTemplate.indexOf(
237+
"holder->hostObject->set",
238+
setterIndex,
239+
);
240+
241+
assert(
242+
source.includes("GetPrototypeV2") &&
243+
source.includes("GetOwnPropertyDescriptor") &&
244+
source.includes("tryResolvePrototypeGet") &&
245+
source.includes("tryInvokePrototypeSetter") &&
246+
nativeTemplate.includes("v8::PropertyHandlerFlags::kNonMasking"),
247+
`${relativePath}: V8 native object interceptors should inspect JS prototype descriptors despite kNonMasking handlers`,
248+
);
249+
assert(
250+
getterIndex !== -1 &&
251+
nativeGetIndex !== -1 &&
252+
getterIndex < nativeGetIndex &&
253+
setterIndex !== -1 &&
254+
nativeSetIndex !== -1 &&
255+
setterIndex < nativeSetIndex,
256+
`${relativePath}: V8 native object interceptors should honor JS prototype accessors before native host dispatch`,
257+
);
258+
assert(
259+
!hostTemplate.includes("tryResolvePrototypeGet(runtime, holderObject, receiver"),
260+
`${relativePath}: JS prototype accessor precedence should be scoped to native object instances, not generic host objects`,
261+
);
262+
}
263+
207264
for (const relativePath of [
208265
"NativeScript/ffi/shared/bridge/HostObjects.mm",
209266
"packages/react-native/native-api/ffi/shared/bridge/HostObjects.mm",

0 commit comments

Comments
 (0)