From b0a562b8b08495d4dbdc7e06422b06d33e00bccb Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Sat, 31 Oct 2026 19:05:31 -0400 Subject: [PATCH 1/2] perf(runtime): cache the instance prototype per class meta CreateJsWrapper only had a prototype cache for JS-extended classes (ClassPrototypes). Every other native object returned to JS rebuilt its prototype on each wrap: objc_getClass(meta->name()), the constructor template lookup, GetFunction, a new "prototype" string and a property Get. Without extra protocols the constructor template is cached per meta, so its prototype is fixed. MetaInstancePrototypes keeps it per meta and the wrap goes straight to SetPrototype. --- NativeScript/runtime/ArgConverter.mm | 38 ++++++++++++++++++---------- NativeScript/runtime/Caches.cpp | 1 + NativeScript/runtime/Caches.h | 3 +++ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index d5464db8..ed32c9d7 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -928,19 +928,31 @@ tns::Assert(false, isolate); } } else { - Class knownClass = objc_getClass(meta->name()); - KnownUnknownClassPair pair(knownClass, klass); - Local ctorFuncTemplate = cache->ObjectCtorInitializer( - context, static_cast(meta), pair, additionalProtocols); - Local ctorFunc; - bool success = ctorFuncTemplate->GetFunction(context).ToLocal(&ctorFunc); - tns::Assert(success, isolate); - - Local prototypeValue; - success = - ctorFunc->Get(context, tns::ToV8String(isolate, "prototype")).ToLocal(&prototypeValue); - tns::Assert(success, isolate); - Local prototype = prototypeValue.As(); + auto cached = additionalProtocols.empty() ? cache->MetaInstancePrototypes.find(meta) + : cache->MetaInstancePrototypes.end(); + Local prototype; + bool success; + if (cached != cache->MetaInstancePrototypes.end()) { + prototype = cached->second->Get(isolate); + } else { + Class knownClass = objc_getClass(meta->name()); + KnownUnknownClassPair pair(knownClass, klass); + Local ctorFuncTemplate = cache->ObjectCtorInitializer( + context, static_cast(meta), pair, additionalProtocols); + Local ctorFunc; + success = ctorFuncTemplate->GetFunction(context).ToLocal(&ctorFunc); + tns::Assert(success, isolate); + + Local prototypeValue; + success = + ctorFunc->Get(context, tns::ToV8String(isolate, "prototype")).ToLocal(&prototypeValue); + tns::Assert(success, isolate); + prototype = prototypeValue.As(); + if (additionalProtocols.empty()) { + cache->MetaInstancePrototypes.emplace( + meta, std::make_unique>(isolate, prototype)); + } + } if (!receiver->SetPrototype(context, prototype).To(&success) || !success) { tns::Assert(false, isolate); diff --git a/NativeScript/runtime/Caches.cpp b/NativeScript/runtime/Caches.cpp index dc1a3344..75e42bcb 100644 --- a/NativeScript/runtime/Caches.cpp +++ b/NativeScript/runtime/Caches.cpp @@ -22,6 +22,7 @@ Caches::~Caches() { this->Prototypes.clear(); this->ClassPrototypes.clear(); + this->MetaInstancePrototypes.clear(); this->CtorFuncTemplates.clear(); this->CtorFuncs.clear(); this->ProtocolCtorFuncs.clear(); diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 666d7e63..be8281a8 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -145,6 +145,9 @@ class Caches { std::unique_ptr>, TransparentStringHash, TransparentStringEqual> ClassPrototypes; + robin_hood::unordered_map>> + MetaInstancePrototypes; robin_hood::unordered_map< const BaseClassMeta*, std::unique_ptr>> From f6131a93e44f635df8a4775ffa6b3fcde197b4bd Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Thu, 5 Nov 2026 19:24:41 -0400 Subject: [PATCH 2/2] fix(runtime): keep native constructor prototypes read-only MetaInstancePrototypes caches a native constructor's prototype per meta, which is only correct if C.prototype cannot be replaced. Interface and protocol constructor templates now call ReadOnlyPrototype(), as struct constructors already do. Adding members still works; a strict-mode assignment throws and a sloppy one is ignored. Before, the assignment succeeded but only objects wrapped by CreateJsWrapper saw it; alloc() kept the snapshot in Caches::Prototypes. --- NativeScript/runtime/MetadataBuilder.mm | 2 ++ TestRunner/app/tests/ApiTests.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index d45d980c..c42c83f8 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -546,6 +546,8 @@ throw NativeScriptException( Local ext = External::New(isolate, item, v8::kExternalPointerTypeTagDefault); ctorFuncTemplate = FunctionTemplate::New(isolate, ClassConstructorCallback, ext); + // MetaInstancePrototypes caches this prototype. + ctorFuncTemplate->ReadOnlyPrototype(); ctorFuncTemplate->InstanceTemplate()->SetInternalFieldCount(1); ctorFuncTemplate->SetClassName(tns::ToV8String(isolate, meta->name())); diff --git a/TestRunner/app/tests/ApiTests.js b/TestRunner/app/tests/ApiTests.js index ddd27494..ae50257c 100644 --- a/TestRunner/app/tests/ApiTests.js +++ b/TestRunner/app/tests/ApiTests.js @@ -150,6 +150,21 @@ describe(module.id, function () { expect(array instanceof NSObject).toBe(true); }); + it("keeps a native class prototype read-only", function () { + var original = NSMutableArray.prototype; + var descriptor = Object.getOwnPropertyDescriptor(NSMutableArray, "prototype"); + expect(descriptor.writable).toBe(false); + try { + expect(function () { "use strict"; NSMutableArray.prototype = {}; }).toThrow(); + } finally { + // Keep later array tests working if the prototype became writable again. + NSMutableArray.prototype = original; + } + var array = NSMutableArray.arrayWithCapacity(1); + expect(Object.getPrototypeOf(array)).toBe(original); + expect(array instanceof NSMutableArray).toBe(true); + }); + it("instanceOfDerivedClass", function () { var JSObject = TNSDerivedInterface.extend({}); var object = JSObject.alloc().init();