Pinned version: 14.9.207.39 (branch-heads/14.9). The libraries are built by
NativeScript/v8-buildscripts
and installed by download_v8.sh; the resurrecting-finalizer patch is described in
v8-resurrecting-finalizers.md.
enable_ios_bitcodeanduse_xcode_clangno longer exist upstream — removed.ios_deployment_targetmust be a string ("13.0", was a bare13).v8_enable_temporal_support=falseis now required. Temporal is implemented in Rust, and enabling it emits Rust objects that reference a Rust sysroot (__rust_allocand friends) which we do not link. V8 10.3 had no Temporal, so disabling keeps parity.- arm64 simulator objects come out as
minos 14.0regardless of the requested 13.0 — that is an Apple floor for arm64 simulator slices, not a project setting.
14.9 pulls in third-party code that 10.3 did not. libv8_third_party.a is archived from
everything under out.gn/*/obj/third_party except zlib and inspector_protocol (already
vendored as libzip.a / libcrdtp*.a), and linked via -lv8_third_party. It covers abseil,
simdutf and highway. Without it the link fails on absl::hash_internal::* and simdutf::*.
ninja now emits thin archives (references, not objects), so the vendored .a files must
be rebuilt from the .o files — a plain cp of libinspector.a silently produces a broken
library.
The packaging step globs obj/<target>/*.o, and ninja never deletes outputs that a config
change orphaned. Reusing an out.gn directory across V8 versions therefore archives stale
objects: a first attempt here silently vendored 42 objects dated 2022 (from the previous
10.3 build in the same directory) into libv8_libbase.a, libv8_bigint.a and libcppgc_base.a.
It surfaced as ld: duplicate symbol 'v8::base::OS::SignalCodeMovingGC()', because the archive
held both the current platform-darwin.o and a stale platform-macos.o.
Delete the output directory before building a new version. The same hazard applies to the Rust
objects left behind by flipping v8_enable_temporal_support off, which is why the third_party
glob excludes */rust/*.
| Change | Sites | Migration |
|---|---|---|
Context::GetIsolate() removed |
113 | v8::Isolate::GetCurrent() |
External::New / External::Value() take a type tag |
84 | v8::kExternalPointerTypeTagDefault |
Get/SetAlignedPointerInInternalField take a tag |
13 | v8::kEmbedderDataTypeTagDefault |
Interceptor callbacks return v8::Intercepted |
13 fns | see below |
PropertyCallbackInfo::This() removed |
43 | Holder() |
ObjectTemplate/Object::SetAccessor |
15 | SetNativeDataProperty |
Accessor callbacks take Local<Name> |
44 | was Local<String> |
String::Value deprecated |
2 | String::ValueView |
GetInternalField returns Local<Data> |
6 | .As<v8::Value>() |
ScriptOrigin no longer takes an Isolate* |
7 | drop the first argument |
GetCreationContext[Checked]() need an isolate |
9 | pass v8::Isolate::GetCurrent() |
AccessControl removed |
2 | drop the argument |
ArrayBuffer::Detach() needs a key |
1 | Detach(Local<Value>()) — null key is allowed |
V8Inspector::connect needs a trust level |
3 | kFullyTrusted |
| dynamic-import callback reshaped | 1 | ScriptOrModule referrer → host_defined_options + resource_name |
V8ConsoleMessage::createForConsoleAPI takes a span |
2 | {args.data(), args.size()} |
Interceptor callbacks used to signal "I handled this" implicitly, by whether they set a return
value. They now return v8::Intercepted explicitly. The conversion rule applied throughout was:
- a path that called
GetReturnValue().Set*()(or threw) →return Intercepted::kYes - a path that returned without setting one, including falling off the end →
return Intercepted::kNo
That reproduces the old semantics exactly, and the fall-off-the-end case is load-bearing rather
than an edge case: the swizzling definer depends on V8 still running the real defineProperty
to install the JS accessor, and the setter interceptors depend on V8 still performing the
ordinary store. Every getter here ends by setting a value (kYes); every setter and the definer
fall through (kNo).
Getting it backwards is silent in both directions: kNo where the old code intercepted lets V8
continue the lookup up the prototype chain, and kYes where it did not suppresses the default
operation entirely.
Note the setter typedefs are inconsistent upstream — for named properties the current form is
NamedPropertySetterCallbackV2 (PropertyCallbackInfo<Boolean>), but for indexed properties
it is the non-V2 IndexedPropertySetterCallback that takes <Boolean>, while the V2 spelling is
the deprecated one taking <void>. Both current forms use <Boolean>.
Two separate aborts, both inside SetFlagsFromString in Runtime::CreateIsolate(), before a
line of JS ran:
- Flags are frozen after initialization.
V8::Initialize()callsFlagList::FreezeFlags()(freeze_flags_after_initdefaults on), and changing a flag afterwards is fatal. The runtime set its flags afterV8::Initialize(); that call now happens first. --jitlessis read-only in lite-mode builds. 14.9 declares it asDEFINE_BOOL_READONLY(jitless, true)underV8_JITLESS, and setting a read-only flag is also fatal. It is now omitted — lite mode already implies it, so behaviour is unchanged.
Worth remembering that both failures look identical from the stack (SetFlagsFromString →
V8_Fatal → Abort); the distinguishing detail is only in V8's message.
V8 commit 026ce4d1 ("[api] Disallow empty context in CallDepthScope", Dec 2023 — after 10.3,
before 14.9) removed the if (!context.IsEmpty()) guard. CallDepthScope now runs
*Utils::OpenDirectHandle(*context) unconditionally, so passing an empty context segfaults at
address 0 instead of quietly skipping the context switch.
This turns "call a context-needing V8 API without an entered context" from silently working into
a crash. The exposure is anything reaching isolate->GetCurrentContext() — tns::ToString,
ToNSString, ToUtf16String and raw String::Utf8Value all do, via V8's own
Utf8Value/ToString internals. APIs taking an explicit Local<Context> are unaffected.
It is invisible on strings: Value::ToString returns early when the value already is one, so a
site only crashes once it converts a non-string. JsV8InspectorClient::dispatchMessage had
Locker + Isolate::Scope + HandleScope but no Context::Scope, and survived until a JS
domain handler returned true and the ack path converted the numeric request id.
Entering the context at the thread entry point is the fix; the scope has to cover every conversion, not just the ones that call into JS.
Unused Local<T> locals now trip -Wunused-variable, which with -Werror is fatal. Five dead
declarations were removed; they were genuinely unused, not merely unread.
v8config.h went from #if __cplusplus < 201703L (C++17 is enough) to #if __cplusplus <= 201703L
(C++17 is rejected). Any plugin whose sources include v8.h must therefore compile at c++20 or
later, and several pin exactly CLANG_CXX_LANGUAGE_STANDARD = c++17 in their
platforms/ios/build.xcconfig — which sat precisely on the old floor.
The app can't override this from App_Resources/iOS/build.xcconfig. The CLI merges every plugin's
xcconfig and the app's into plugins-{debug,release}.xcconfig, and the merge is first writer
wins — XcconfigService.mergeFiles deletes a key from the incoming file when the destination
already has it. mergeProjectXcconfigFiles merges the plugins first and the app's file last, so
for any key a plugin already set, the app's value is discarded outright; among plugins the winner
is whichever comes first in dependency-resolution order. That merged file is then the last
#include in build-{debug,release}.xcconfig, so it also beats the project-level setting in the
pbxproj.
Plugins are expected to declare c++20 themselves. Worth knowing that a single stale transitive
plugin is enough to hold an entire app below the floor, and that the resulting error points at our
headers rather than at the plugin that pinned the standard. An app that needs to force the floor
regardless can assign CLANG_CXX_LANGUAGE_STANDARD after the plugins-*.xcconfig include in
build-{debug,release}.xcconfig, which is the only layer that beats the merge.
Both project templates now default to gnu++20; they were on gnu++0x, which is C++11 in the
pre-ratification spelling and two floors below what v8config.h accepts. That fixes the baseline
for an app carrying its own C++ or ObjC++ sources, but by the ordering above it is a project-level
setting, so a plugin pinning a lower standard still wins over it.
GCC_C_LANGUAGE_STANDARD is the only language setting the metadata generator reads:
build-step-metadata-generator.py passes it straight through as clang's -std= when parsing the
SDK, and never looks at CLANG_CXX_LANGUAGE_STANDARD. The templates were on gnu99, which
predates C11; they are now on gnu17, Xcode's current default.
This changes no metadata. Regenerating against the iOS 26.2 SDK under both standards produces byte-identical output — the binary, the umbrella header, and all 193 YAML modules. The only observable difference is one fewer spurious diagnostic, "redefinition of typedef 'MPSImageBatch' is a C11 feature", which C11 and later allow.
NativeScript/inspector/src is a copy of V8's inspector/base/debug headers, previously pinned to
10.3. Because libinspector.a is now built from 14.9, keeping 10.3 headers would be an ABI
mismatch, so the tree was re-vendored: 107 files from v8/src, 9 generated protocol headers from
out.gn/*/gen/src/inspector/protocol, plus 7 newly-required headers.
14.9's src/common/globals.h reaches abseil via base/numbers/double.h → diy-fp.h →
absl/numeric/int128.h, so 11 abseil headers are vendored under NativeScript/inspector/absl
(that directory is already on the header search path, so no project change was needed).
Ten 10.3-era files no longer exist upstream and were left in place; nothing includes them now:
base/{cpu,functional,optional,safe_conversions*,v8-fallthrough}.h,
common/allow-deprecated.h, debug/debug-type-profile.h, inspector/v8-webdriver-serializer.h.
./run_tests.sh on an iOS 18.5 simulator: 904 specs, 892 passing, 1 failing, 11 skipped.
All worker and teardown specs pass.
The one failure, TNS require :: should throw error if cant find node module, is not a
migration regression. IsLikelyOptionalModule() treats any bare specifier without a path
separator as optional, so require('nonExistingFileName.js') yields a lazily-throwing
placeholder rather than throwing, and the test never touches the result. That logic is
version-independent and predates this work (it arrived with ESM support in e72977ab), so it is
left alone here rather than changed as a side effect of the V8 upgrade.
Three separate failures had one root cause: property callbacks can no longer see the
receiver, and SetNativeDataProperty is not a drop-in replacement for SetAccessor on an
object that is inherited from.
ClassBuilder'ssuper(on the implementation object, which instances inherit) andReference.value(on aPrototypeTemplate) both need the receiver, andHolder()gives the prototype instead. Both are now function-backed accessors, whoseFunctionCallbackInfo::This()still returns the receiver.- Static properties on constructor functions behaved worse:
SetNativeDataPropertyinstalls something data-like, soDerived.baseProperty = xshadowed the base's property with an own data property and never reached the native setter. Now installed withSetAccessorProperty.
The rule of thumb: if an accessor lives on anything that is inherited from — a prototype
template, a base constructor, an implementation object — use SetAccessorProperty with
function-backed getter/setter. SetNativeDataProperty is only safe on instance templates,
where Holder() == This() and nothing inherits it.
Nothing blocking. One follow-up:
DisposerPHV.{h,mm}is now dead code --Isolate::VisitHandlesWithClassIdsno longer exists, so the visitor can never be driven. Its logic moved toObjectManager::DisposeAllRegistered().
Two places where the new APIs quietly change semantics unless the old values are carried over deliberately. Neither is covered by a test, so both would have shipped silently:
- Accessor attributes.
Object::SetAccessordefaulted toPropertyAttribute::None;SetAccessorPropertytakes the attributes explicitly. PassingDontEnumforsuper(an easy assumption, since it looks internal) removes it fromObject.keys()andfor...inon the implementation object. The defaults are preserved instead. ArrayBuffer::Detach. The deprecatedDetach()returned void and could not report failure. The replacement returnsMaybe<bool>and isV8_WARN_UNUSED_RESULT, so it invites a.Check()— which aborts the process when the buffer has an[[ArrayBufferDetachKey]]that does not match. This is on the workerpostMessagetransfer path, so the result is discarded rather than checked.
Two runs during this work hung for ~10 minutes instead of finishing. Sampling the stuck process showed a lock-order inversion that does not involve any of the above:
- the main thread, inside a posted notification (
___CFXRegistrationPost→ArgConverter::MethodCallback), blocked acquiring a worker isolate'sv8::Locker; - worker threads, running JS that dispatched an ObjC method, blocked in
initializeNonMetaClasson the ObjC runtime's class-initialization lock.
It has not reproduced in the three runs since, so it is timing-dependent. It is recorded here because the symptom (suite hangs with no crash report, last log line a spec well before the worker tests) is otherwise very hard to place.
Isolate::VisitHandlesWithClassIds was removed upstream, so wrappers can no longer be found by
walking V8's handles. Caches now keeps a weak_ptr registry of every handle
ObjectManager::Register() hands out -- weak, so tracking never keeps a wrapper alive, and
compacted whenever it would grow so collected entries do not accumulate.
ObjectManager::DisposeAllRegistered() walks that registry at teardown, then disposes the
Caches collections that used to carry the DataWrapper class id (CtorFuncs,
ProtocolCtorFuncs, CFunctions, PrimitiveInteropTypes and the three interop ctor handles).
~Caches() cleared those maps but never disposed their wrappers, so they leaked too.
It has to enter the isolate itself: ~Runtime holds a Locker but never enters, and creating
handles without that segfaults in HandleScope::CreateHandle. The same applies to
Worker::SetWorkerId, which runs after Runtime::Init() has unwound its own Isolate::Scope.