diff --git a/NativeScript/runtime/ArgConverter.h b/NativeScript/runtime/ArgConverter.h index 64ab8d5f..8353fbb2 100644 --- a/NativeScript/runtime/ArgConverter.h +++ b/NativeScript/runtime/ArgConverter.h @@ -2,61 +2,128 @@ #define ArgConverter_h #include -#include "libffi.h" -#include "Common.h" + #include "Caches.h" +#include "Common.h" #include "DataWrapper.h" #include "IsolateWrapper.h" +#include "libffi.h" namespace tns { class ArgConverter; struct MethodCallbackWrapper { -public: - MethodCallbackWrapper(v8::Isolate* isolate, std::shared_ptr> callback, const uint8_t initialParamIndex, const uint8_t paramsCount, const TypeEncoding* typeEncoding) - : isolateWrapper_(isolate), - callback_(callback), - initialParamIndex_(initialParamIndex), - paramsCount_(paramsCount), - typeEncoding_(typeEncoding) { - } - IsolateWrapper isolateWrapper_; - std::shared_ptr> callback_; - const uint8_t initialParamIndex_; - const uint8_t paramsCount_; - const TypeEncoding* typeEncoding_; + public: + MethodCallbackWrapper(v8::Isolate* isolate, + std::shared_ptr> callback, + const uint8_t initialParamIndex, + const uint8_t paramsCount, + const TypeEncoding* typeEncoding) + : isolateWrapper_(isolate), + callback_(callback), + initialParamIndex_(initialParamIndex), + paramsCount_(paramsCount), + typeEncoding_(typeEncoding) {} + IsolateWrapper isolateWrapper_; + std::shared_ptr> callback_; + const uint8_t initialParamIndex_; + const uint8_t paramsCount_; + const TypeEncoding* typeEncoding_; }; class ArgConverter { -public: - static void Init(v8::Local context, v8::GenericNamedPropertyGetterCallback structPropertyGetter, v8::GenericNamedPropertySetterCallback structPropertySetter); - static v8::Local Invoke(v8::Local context, Class klass, v8::Local receiver, V8Args& args, const MethodMeta* meta, bool isMethodCallback); - static v8::Local ConvertArgument(v8::Local context, BaseDataWrapper* wrapper, bool skipGCRegistration = false, const std::vector& additionalProtocols = std::vector()); - static v8::Local CreateJsWrapper(v8::Local context, BaseDataWrapper* wrapper, v8::Local receiver, bool skipGCRegistration = false, const std::vector& additionalProtocols = std::vector()); - static std::shared_ptr> CreateEmptyObject(v8::Local context, bool skipGCRegistration = false); - static std::shared_ptr> CreateEmptyStruct(v8::Local context); - static const Meta* FindMeta(Class klass, const TypeEncoding* typeEncoding = nullptr); - static const Meta* GetMeta(std::string name); - static const ProtocolMeta* FindProtocolMeta(Protocol* protocol); - static void MethodCallback(ffi_cif* cif, void* retValue, void** argValues, void* userData); - static void SetValue(v8::Local context, void* retValue, v8::Local value, const TypeEncoding* typeEncoding); - static void ConstructObject(v8::Local context, const v8::FunctionCallbackInfo& info, Class klass, const InterfaceMeta* interfaceMeta = nullptr); -private: - static v8::Local CreateEmptyInstanceFunction(v8::Local context, v8::GenericNamedPropertyGetterCallback propertyGetter = nullptr, v8::GenericNamedPropertySetterCallback propertySetter = nullptr); - static std::shared_ptr> CreateEmptyInstance(v8::Local context, v8::Persistent* ctorFunc, bool skipGCRegistration = false); - static void FindMethodOverloads(Class klass, std::string methodName, MemberType type, std::vector& overloads); - static const MethodMeta* FindInitializer(v8::Local context, Class klass, const InterfaceMeta* interfaceMeta, const v8::FunctionCallbackInfo& info, std::vector>& args); - static bool CanInvoke(v8::Local context, const TypeEncoding* typeEncoding, v8::Local arg); - static bool CanInvoke(v8::Local context, const MethodMeta* candidate, const v8::FunctionCallbackInfo& info); - static std::vector> GetInitializerArgs(v8::Local obj, std::string& constructorTokens); - static void IndexedPropertyGetterCallback(uint32_t index, const v8::PropertyCallbackInfo& args); - static void IndexedPropertySetterCallback(uint32_t index, v8::Local value, const v8::PropertyCallbackInfo& args); - static bool IsErrorOutParameter(const TypeEncoding* typeEncoding); - static std::vector GetInitializers(Caches* cache, Class klass, const InterfaceMeta* interfaceMeta); - static void MethodCallbackInternal(ffi_cif* cif, void* retValue, void** argValues, void* userData); + public: + static void Init(v8::Local context, + v8::GenericNamedPropertyGetterCallback structPropertyGetter, + v8::GenericNamedPropertySetterCallback structPropertySetter); + static v8::Local Invoke(v8::Local context, + Class klass, + v8::Local receiver, + V8Args& args, const MethodMeta* meta, + bool isMethodCallback); + static v8::Local ConvertArgument( + v8::Local context, BaseDataWrapper* wrapper, + bool skipGCRegistration = false, + const std::vector& additionalProtocols = + std::vector()); + static v8::Local CreateJsWrapper( + v8::Local context, BaseDataWrapper* wrapper, + v8::Local receiver, bool skipGCRegistration = false, + const std::vector& additionalProtocols = + std::vector()); + static std::shared_ptr> CreateEmptyObject( + v8::Local context, bool skipGCRegistration = false); + static std::shared_ptr> CreateEmptyStruct( + v8::Local context); + static const Meta* FindMeta(Class klass, + const TypeEncoding* typeEncoding = nullptr); + static const Meta* GetMeta(std::string name); + static const ProtocolMeta* FindProtocolMeta(Protocol* protocol); + static void MethodCallback(ffi_cif* cif, void* retValue, void** argValues, + void* userData); + static void SetValue(v8::Local context, void* retValue, + v8::Local value, + const TypeEncoding* typeEncoding); + // Returns (lazily creating) the per-isolate interop.escapeException brand + // private stored in Caches. Empty handle if the isolate cache is invalid. + static v8::Local GetEscapeExceptionBrand(v8::Isolate* isolate); + // If `value` carries the escapeException brand, returns the NSException to + // throw (typed `id` so this header is usable from the pure-C++ TUs that + // include it) — the original wrapped NSException when present, otherwise one + // synthesized from the branded name/message/stack payload. Returns nil when + // `value` is not branded. Never reports; the caller decides what to do. + static id ExtractEscapedException(v8::Local context, + v8::Local value); + // Shared JS<->native boundary helper. Reads tc.Exception(): if it carries + // the escapeException brand, returns the NSException (as `id`) the caller + // must @throw AFTER closing every live V8 scope (Locker/HandleScope/ + // Context::Scope). Otherwise reports the exception exactly once through the + // uncaught path (error-event dispatch + shims + log) and returns nil, so the + // caller returns its defined default. Resets `tc` in both cases. + static id HandleBoundaryException(v8::Local context, + v8::TryCatch& tc); + static void ConstructObject(v8::Local context, + const v8::FunctionCallbackInfo& info, + Class klass, + const InterfaceMeta* interfaceMeta = nullptr); + + private: + static v8::Local CreateEmptyInstanceFunction( + v8::Local context, + v8::GenericNamedPropertyGetterCallback propertyGetter = nullptr, + v8::GenericNamedPropertySetterCallback propertySetter = nullptr); + static std::shared_ptr> CreateEmptyInstance( + v8::Local context, v8::Persistent* ctorFunc, + bool skipGCRegistration = false); + static void FindMethodOverloads(Class klass, std::string methodName, + MemberType type, + std::vector& overloads); + static const MethodMeta* FindInitializer( + v8::Local context, Class klass, + const InterfaceMeta* interfaceMeta, + const v8::FunctionCallbackInfo& info, + std::vector>& args); + static bool CanInvoke(v8::Local context, + const TypeEncoding* typeEncoding, + v8::Local arg); + static bool CanInvoke(v8::Local context, + const MethodMeta* candidate, + const v8::FunctionCallbackInfo& info); + static std::vector> GetInitializerArgs( + v8::Local obj, std::string& constructorTokens); + static void IndexedPropertyGetterCallback( + uint32_t index, const v8::PropertyCallbackInfo& args); + static void IndexedPropertySetterCallback( + uint32_t index, v8::Local value, + const v8::PropertyCallbackInfo& args); + static bool IsErrorOutParameter(const TypeEncoding* typeEncoding); + static std::vector GetInitializers( + Caches* cache, Class klass, const InterfaceMeta* interfaceMeta); + static void MethodCallbackInternal(ffi_cif* cif, void* retValue, + void** argValues, void* userData); }; -} +} // namespace tns #endif /* ArgConverter_h */ diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 2197c820..c3831bf2 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -128,6 +128,101 @@ return result; } +Local ArgConverter::GetEscapeExceptionBrand(Isolate* isolate) { + auto cache = Caches::Get(isolate); + // Caches::Get never returns null — after isolate removal it returns a dummy + // cache — so validity is the real liveness check before touching V8 state. + if (cache == nullptr || !cache->IsValid()) { + return Local(); + } + if (cache->EscapeExceptionBrand == nullptr) { + Local brand = + Private::New(isolate, tns::ToV8String(isolate, "interop.escapeException")); + cache->EscapeExceptionBrand = std::make_unique>(isolate, brand); + } + return cache->EscapeExceptionBrand->Get(isolate); +} + +id ArgConverter::ExtractEscapedException(Local context, Local value) { + Isolate* isolate = context->GetIsolate(); + if (value.IsEmpty() || !value->IsObject()) { + return nil; + } + Local brand = GetEscapeExceptionBrand(isolate); + if (brand.IsEmpty()) { + return nil; + } + Local obj = value.As(); + Maybe hasBrand = obj->HasPrivate(context, brand); + if (hasBrand.IsNothing() || !hasBrand.FromJust()) { + return nil; + } + Local payloadVal; + if (!obj->GetPrivate(context, brand).ToLocal(&payloadVal) || !payloadVal->IsObject()) { + return nil; + } + Local payload = payloadVal.As(); + + // Original NSException carried through unchanged when present. + Local nativeExc; + if (payload->Get(context, tns::ToV8String(isolate, "nativeException")).ToLocal(&nativeExc) && + nativeExc->IsObject()) { + BaseDataWrapper* wrapper = tns::GetValue(isolate, nativeExc); + if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { + id data = static_cast(wrapper)->Data(); + if ([data isKindOfClass:[NSException class]]) { + return (NSException*)data; + } + } + } + + // Otherwise synthesize an NSException from the branded name/message/stack. + auto readString = [&](const char* key, const std::string& fallback) -> std::string { + Local v; + if (payload->Get(context, tns::ToV8String(isolate, key)).ToLocal(&v) && !v.IsEmpty() && + v->IsString()) { + return tns::ToString(isolate, v); + } + return fallback; + }; + std::string name = readString("name", "NativeScriptException"); + std::string message = readString("message", ""); + std::string stack = readString("stack", ""); + + NSString* reasonText = tns::ToNSString(message); + if (!stack.empty()) { + // Put the JS stack in the reason too so it shows up in native crash logs. + reasonText = [reasonText stringByAppendingFormat:@"\n%@", tns::ToNSString(stack)]; + } + NSDictionary* userInfo = stack.empty() ? nil : @{@"JavaScriptStack" : tns::ToNSString(stack)}; + return [NSException exceptionWithName:tns::ToNSString(name) reason:reasonText userInfo:userInfo]; +} + +id ArgConverter::HandleBoundaryException(Local context, TryCatch& tc) { + if (!tc.HasCaught()) { + return nil; + } + Isolate* isolate = context->GetIsolate(); + Local exception = tc.Exception(); + + id escaped = ExtractEscapedException(context, exception); + if (escaped != nil) { + // Branded escape: convert to an ObjC throw. Clear the V8 exception; the + // caller @throws this AFTER closing all V8 scopes. + tc.Reset(); + return escaped; + } + + // Unbranded: report exactly once through the uncaught path (error-event + // dispatch + shims + log; honors crashOnUncaughtJsExceptions). This is the + // path for the Assert-hardened sites that today never reach the message + // listener at all. + Local message = tc.Message(); + NativeScriptException::ReportToJsHandlersAndLog(isolate, exception, message); + tc.Reset(); + return nil; +} + void ArgConverter::MethodCallback(ffi_cif* cif, void* retValue, void** argValues, void* userData) { MethodCallbackWrapper* data = static_cast(userData); @@ -138,90 +233,123 @@ return; } - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - std::shared_ptr cache = Caches::Get(isolate); + // Declared before all V8 scopes: an ObjC exception must never unwind through a + // live V8 scope (Locker/HandleScope/Context::Scope). A branded escape caught + // below is captured here and @thrown only after the inner block closes every + // V8 scope. + NSException* __strong pendingThrow = nil; - Local context = cache->GetContext(); - Context::Scope context_scope(context); - std::shared_ptr> poCallback = data->callback_; + { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + std::shared_ptr cache = Caches::Get(isolate); - bool hasErrorOutParameter = false; + Local context = cache->GetContext(); + Context::Scope context_scope(context); + std::shared_ptr> poCallback = data->callback_; + + bool hasErrorOutParameter = false; + + std::vector> v8Args; + v8Args.reserve(data->paramsCount_); + const TypeEncoding* typeEncoding = data->typeEncoding_; + for (int i = 0; i < data->paramsCount_; i++) { + typeEncoding = typeEncoding->next(); + if (i == data->paramsCount_ - 1 && ArgConverter::IsErrorOutParameter(typeEncoding)) { + hasErrorOutParameter = true; + // No need to provide the NSError** parameter to the javascript callback + continue; + } - std::vector> v8Args; - v8Args.reserve(data->paramsCount_); - const TypeEncoding* typeEncoding = data->typeEncoding_; - for (int i = 0; i < data->paramsCount_; i++) { - typeEncoding = typeEncoding->next(); - if (i == data->paramsCount_ - 1 && ArgConverter::IsErrorOutParameter(typeEncoding)) { - hasErrorOutParameter = true; - // No need to provide the NSError** parameter to the javascript callback - continue; - } + int argIndex = i + data->initialParamIndex_; - int argIndex = i + data->initialParamIndex_; + uint8_t* argBuffer = (uint8_t*)argValues[argIndex]; + BaseCall call(argBuffer); + Local jsWrapper = Interop::GetResult(context, typeEncoding, &call, true); - uint8_t* argBuffer = (uint8_t*)argValues[argIndex]; - BaseCall call(argBuffer); - Local jsWrapper = Interop::GetResult(context, typeEncoding, &call, true); + if (!jsWrapper.IsEmpty()) { + v8Args.push_back(jsWrapper); + } else { + v8Args.push_back(v8::Undefined(isolate)); + } + } - if (!jsWrapper.IsEmpty()) { - v8Args.push_back(jsWrapper); - } else { - v8Args.push_back(v8::Undefined(isolate)); + Local thiz = context->Global(); + if (data->initialParamIndex_ > 1) { + id self_ = *static_cast(argValues[0]); + auto it = cache->Instances.find(self_); + if (it != cache->Instances.end()) { + thiz = it->second->Get(data->isolateWrapper_.Isolate()).As(); + } else { + ObjCDataWrapper* wrapper = new ObjCDataWrapper(self_); + thiz = ArgConverter::CreateJsWrapper(context, wrapper, Local(), true).As(); + tns::DeleteWrapperIfUnused(isolate, thiz, wrapper); + } } - } - Local thiz = context->Global(); - if (data->initialParamIndex_ > 1) { - id self_ = *static_cast(argValues[0]); - auto it = cache->Instances.find(self_); - if (it != cache->Instances.end()) { - thiz = it->second->Get(data->isolateWrapper_.Isolate()).As(); + Local result; + Local callback = poCallback->Get(isolate).As(); + + bool success = false; + if (hasErrorOutParameter) { + // We don't want the global error handler (NativeScriptException::OnUncaughtError) to be + // called for javascript exceptions occuring inside methods that have NSError* parameters. + // Those js errors will be marshalled to NSError* and sent directly to the calling native + // code. The v8::TryCatch statement prevents the global handler to be called. + TryCatch tc(isolate); + success = callback->Call(context, thiz, (int)v8Args.size(), v8Args.data()).ToLocal(&result); + if (!success && tc.HasCaught()) { + // A branded escapeException follows the @throw path even here, taking + // precedence over NSError-out marshaling. + NSException* escaped = ExtractEscapedException(context, tc.Exception()); + if (escaped != nil) { + pendingThrow = escaped; + } else { + Local exception = tc.Exception(); + std::string message = tns::ToString(isolate, exception); + + int errorParamIndex = data->initialParamIndex_ + data->paramsCount_ - 1; + void* errorParam = argValues[errorParamIndex]; + NSError* __strong** outPtr = static_cast(errorParam); + if (outPtr && *outPtr) { + NSError* error = + [NSError errorWithDomain:@"TNSErrorDomain" + code:164 + userInfo:@{@"TNSJavaScriptError" : tns::ToNSString(message)}]; + **static_cast(outPtr) = error; + } + } + } } else { - ObjCDataWrapper* wrapper = new ObjCDataWrapper(self_); - thiz = ArgConverter::CreateJsWrapper(context, wrapper, Local(), true).As(); - tns::DeleteWrapperIfUnused(isolate, thiz, wrapper); + TryCatch tc(isolate); + success = callback->Call(context, thiz, (int)v8Args.size(), v8Args.data()).ToLocal(&result); + if (!success && tc.HasCaught()) { + NSException* escaped = ExtractEscapedException(context, tc.Exception()); + if (escaped != nil) { + pendingThrow = escaped; + } else { + // Unbranded: re-throw so the pending exception surfaces to V8's message + // listener exactly once (OnUncaughtError), preserving existing behavior. + tc.ReThrow(); + } + } } - } - Local result; - Local callback = poCallback->Get(isolate).As(); - - bool success = false; - if (hasErrorOutParameter) { - // We don't want the global error handler (NativeScriptException::OnUncaughtError) to be called - // for javascript exceptions occuring inside methods that have NSError* parameters. Those js - // errors will be marshalled to NSError* and sent directly to the calling native code. The - // v8::TryCatch statement prevents the global handler to be called. - TryCatch tc(isolate); - success = callback->Call(context, thiz, (int)v8Args.size(), v8Args.data()).ToLocal(&result); - if (!success && tc.HasCaught()) { - Local exception = tc.Exception(); - std::string message = tns::ToString(isolate, exception); - - int errorParamIndex = data->initialParamIndex_ + data->paramsCount_ - 1; - void* errorParam = argValues[errorParamIndex]; - NSError* __strong** outPtr = static_cast(errorParam); - if (outPtr && *outPtr) { - NSError* error = - [NSError errorWithDomain:@"TNSErrorDomain" - code:164 - userInfo:@{@"TNSJavaScriptError" : tns::ToNSString(message)}]; - **static_cast(outPtr) = error; + if (pendingThrow == nil) { + if (!success) { + memset(retValue, 0, cif->rtype->size); + } else { + ArgConverter::SetValue(context, retValue, result, data->typeEncoding_); } + } else { + memset(retValue, 0, cif->rtype->size); } - } else { - success = callback->Call(context, thiz, (int)v8Args.size(), v8Args.data()).ToLocal(&result); - } + } // all V8 scopes destruct here - if (!success) { - memset(retValue, 0, cif->rtype->size); - return; + if (pendingThrow != nil) { + @throw pendingThrow; } - - ArgConverter::SetValue(context, retValue, result, data->typeEncoding_); } void ArgConverter::SetValue(Local context, void* retValue, Local value, diff --git a/NativeScript/runtime/ArrayAdapter.mm b/NativeScript/runtime/ArrayAdapter.mm index 2dfba4ec..ed7d10f3 100644 --- a/NativeScript/runtime/ArrayAdapter.mm +++ b/NativeScript/runtime/ArrayAdapter.mm @@ -1,4 +1,5 @@ #include "ArrayAdapter.h" +#include "ArgConverter.h" #include "DataWrapper.h" #include "Helpers.h" #include "Interop.h" @@ -8,99 +9,123 @@ using namespace v8; @implementation ArrayAdapter { - IsolateWrapper* wrapper_; - std::shared_ptr> object_; - // we're responsible for this wrapper - ObjCDataWrapper* dataWrapper_; + IsolateWrapper* wrapper_; + std::shared_ptr> object_; + // we're responsible for this wrapper + ObjCDataWrapper* dataWrapper_; } - (instancetype)initWithJSObject:(Local)jsObject isolate:(Isolate*)isolate { - if (self) { - self->wrapper_ = new IsolateWrapper(isolate); - self->object_ = std::make_shared>(isolate, jsObject); - self->wrapper_->GetCache()->Instances.emplace(self, self->object_); - tns::SetValue(isolate, jsObject, (self->dataWrapper_ = new ObjCDataWrapper(self))); - } - - return self; + if (self) { + self->wrapper_ = new IsolateWrapper(isolate); + self->object_ = std::make_shared>(isolate, jsObject); + self->wrapper_->GetCache()->Instances.emplace(self, self->object_); + tns::SetValue(isolate, jsObject, (self->dataWrapper_ = new ObjCDataWrapper(self))); + } + + return self; } - (NSUInteger)count { - auto isolate = wrapper_->Isolate(); - if(!wrapper_->IsValid()) { - return 0; - } + auto isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return 0; + } + NSUInteger result = 0; + // Scopes-before-@throw: a branded escape from the JS boundary is @thrown only + // after the inner block's V8 scopes destruct. + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); - + Local object = self->object_->Get(isolate).As(); if (object->IsArray()) { - uint32_t length = object.As()->Length(); - return length; + result = object.As()->Length(); + } else { + Local context = wrapper_->GetCache()->GetContext(); + Local propertyNames; + TryCatch tc(isolate); + if (object->GetPropertyNames(context).ToLocal(&propertyNames)) { + result = propertyNames->Length(); + } else { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } } - - Local context = wrapper_->GetCache()->GetContext(); - Local propertyNames; - bool success = object->GetPropertyNames(context).ToLocal(&propertyNames); - tns::Assert(success, isolate); - uint32_t length = propertyNames->Length(); - return length; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (id)objectAtIndex:(NSUInteger)index { - auto isolate = wrapper_->Isolate(); - if (!wrapper_->IsValid()) { - return nil; - } + auto isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return nil; + } + + if (!(index < [self count])) { + // Out of bounds: return the adapter default rather than aborting. + return nil; + } + + id result = nil; + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); - - if (!(index < [self count])) { - tns::Assert(false, isolate); - } - + Local object = self->object_->Get(isolate).As(); Local context = wrapper_->GetCache()->GetContext(); Local item; - bool success = object->Get(context, (uint)index).ToLocal(&item); - tns::Assert(success, isolate); - - if (item->IsNullOrUndefined()) { - return nil; + TryCatch tc(isolate); + if (!object->Get(context, (uint)index).ToLocal(&item)) { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } else if (!item->IsNullOrUndefined()) { + result = Interop::ToObject(context, item); } - - id value = Interop::ToObject(context, item); - return value; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (void)dealloc { - if (wrapper_->IsValid()) { - auto isolate = wrapper_->Isolate(); - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - wrapper_->GetCache()->Instances.erase(self); - Local value = self->object_->Get(isolate); - BaseDataWrapper* wrapper = tns::GetValue(isolate, value); - if (wrapper != nullptr) { - tns::DeleteValue(isolate, value); - // ensure we don't delete the same wrapper twice - // this is just needed as a failsafe in case some other wrapper is assigned to this object - if (wrapper == dataWrapper_) { - dataWrapper_ = nullptr; - } - delete wrapper; - } - self->object_->Reset(); - } - delete wrapper_; - if (dataWrapper_ != nullptr) { - delete dataWrapper_; + if (wrapper_->IsValid()) { + auto isolate = wrapper_->Isolate(); + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + wrapper_->GetCache()->Instances.erase(self); + Local value = self->object_->Get(isolate); + BaseDataWrapper* wrapper = tns::GetValue(isolate, value); + if (wrapper != nullptr) { + tns::DeleteValue(isolate, value); + // ensure we don't delete the same wrapper twice + // this is just needed as a failsafe in case some other wrapper is assigned to this object + if (wrapper == dataWrapper_) { + dataWrapper_ = nullptr; + } + delete wrapper; } - self->object_ = nullptr; - [super dealloc]; + self->object_->Reset(); + } + delete wrapper_; + if (dataWrapper_ != nullptr) { + delete dataWrapper_; + } + self->object_ = nullptr; + [super dealloc]; } @end diff --git a/NativeScript/runtime/Caches.cpp b/NativeScript/runtime/Caches.cpp index 9810625f..3cff2d29 100644 --- a/NativeScript/runtime/Caches.cpp +++ b/NativeScript/runtime/Caches.cpp @@ -1,47 +1,54 @@ #include "Caches.h" + #include "Constants.h" +#include "NativeScriptException.h" using namespace v8; namespace tns { Caches::Caches(Isolate* isolate, const int& isolateId) - : isolate_(isolate), isolateId_(isolateId) { -} + : PromiseRejections(std::make_unique(isolate)), + isolate_(isolate), + isolateId_(isolateId) {} Caches::~Caches() { - this->Prototypes.clear(); - this->ClassPrototypes.clear(); - this->CtorFuncTemplates.clear(); - this->CtorFuncs.clear(); - this->ProtocolCtorFuncs.clear(); - this->StructConstructorFunctions.clear(); - this->PrimitiveInteropTypes.clear(); - this->CFunctions.clear(); - - this->Instances.clear(); - this->StructInstances.clear(); - this->PointerInstances.clear(); - this->cacheBoundObjects_.clear(); + this->Prototypes.clear(); + this->ClassPrototypes.clear(); + this->CtorFuncTemplates.clear(); + this->CtorFuncs.clear(); + this->ProtocolCtorFuncs.clear(); + this->StructConstructorFunctions.clear(); + this->PrimitiveInteropTypes.clear(); + this->CFunctions.clear(); + + this->Instances.clear(); + this->StructInstances.clear(); + this->PointerInstances.clear(); + this->cacheBoundObjects_.clear(); } void Caches::Remove(v8::Isolate* isolate) { - auto cache = isolate->GetData(Constants::CACHES_ISOLATE_SLOT); - isolate->SetData(Constants::CACHES_ISOLATE_SLOT, nullptr); - if (cache != nullptr) { - delete reinterpret_cast*>(cache); - } + auto cache = isolate->GetData(Constants::CACHES_ISOLATE_SLOT); + isolate->SetData(Constants::CACHES_ISOLATE_SLOT, nullptr); + if (cache != nullptr) { + delete reinterpret_cast*>(cache); + } } void Caches::SetContext(Local context) { - this->context_ = std::make_shared>(this->isolate_, context); + this->context_ = + std::make_shared>(this->isolate_, context); } Local Caches::GetContext() { - return this->context_->Get(this->isolate_); + return this->context_->Get(this->isolate_); } -std::shared_ptr> Caches::Metadata = std::make_shared>(); -std::shared_ptr>> Caches::Workers = std::make_shared>>(); +std::shared_ptr> Caches::Metadata = + std::make_shared>(); +std::shared_ptr>> + Caches::Workers = std::make_shared< + ConcurrentMap>>(); -} +} // namespace tns diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 2126410c..e8fbe186 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -3,140 +3,197 @@ #include #include -#include "ConcurrentMap.h" -#include "robin_hood.h" + #include "Common.h" +#include "ConcurrentMap.h" #include "Metadata.h" +#include "robin_hood.h" namespace tns { struct StructInfo; +class PromiseRejectionTracker; struct pair_hash { - template - std::size_t operator() (const std::pair &pair) const { - return std::hash()(pair.first) ^ std::hash()(pair.second); - } + template + std::size_t operator()(const std::pair& pair) const { + return std::hash()(pair.first) ^ std::hash()(pair.second); + } }; class Caches { -public: - class WorkerState { - public: - WorkerState(v8::Isolate* isolate, std::shared_ptr> poWorker, void* userData) - : isolate_(isolate), - poWorker_(poWorker), - userData_(userData) { - } - - v8::Isolate* GetIsolate() { - return this->isolate_; - } - - std::shared_ptr> GetWorker() { - return this->poWorker_; - } - - void* UserData() { - return this->userData_; - } - private: - v8::Isolate* isolate_; - std::shared_ptr> poWorker_; - void* userData_; - }; - - Caches(v8::Isolate* isolate, const int& isolateId_ = -1); - ~Caches(); - - bool isWorker = false; - - static std::shared_ptr> Metadata; - static std::shared_ptr>> Workers; - - inline static std::shared_ptr Init(v8::Isolate* isolate, const int& isolateId) { - auto cache = std::make_shared(isolate, isolateId); - // create a new shared_ptr that will live until Remove is called - isolate->SetData(0, static_cast(new std::shared_ptr(cache))); - return cache; - } - inline static std::shared_ptr Get(v8::Isolate* isolate) { - auto cache = isolate->GetData(0); - if (cache != nullptr) { - return *reinterpret_cast*>(cache); - } - // this should only happen when an isolate is accessed after disposal - // so we return a dummy cache - return std::make_shared(isolate); - } - static void Remove(v8::Isolate* isolate); - - inline int getIsolateId() { - return isolateId_; - } - - inline void InvalidateIsolate() { - isolateId_ = -1; + public: + class WorkerState { + public: + WorkerState(v8::Isolate* isolate, + std::shared_ptr> poWorker, + void* userData) + : isolate_(isolate), poWorker_(poWorker), userData_(userData) {} + + v8::Isolate* GetIsolate() { return this->isolate_; } + + std::shared_ptr> GetWorker() { + return this->poWorker_; } - inline bool IsValid() { - return isolateId_ != -1; - } + void* UserData() { return this->userData_; } - void SetContext(v8::Local context); - v8::Local GetContext(); - - robin_hood::unordered_map>> Prototypes; - robin_hood::unordered_map>> ClassPrototypes; - robin_hood::unordered_map>> CtorFuncTemplates; - robin_hood::unordered_map>> CtorFuncs; - robin_hood::unordered_map>> ProtocolCtorFuncs; - robin_hood::unordered_map>> StructConstructorFunctions; - robin_hood::unordered_map>> PrimitiveInteropTypes; - robin_hood::unordered_map>> CFunctions; - - robin_hood::unordered_map>> Instances; - robin_hood::unordered_map, std::shared_ptr>, pair_hash> StructInstances; - robin_hood::unordered_map>> PointerInstances; - - std::function(v8::Local, const BaseClassMeta*, KnownUnknownClassPair, const std::vector&)> ObjectCtorInitializer; - std::function(v8::Local, StructInfo)> StructCtorInitializer; - robin_hood::unordered_map Timers; - robin_hood::unordered_map> Initializers; - - std::unique_ptr> EmptyObjCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> EmptyStructCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> SliceFunc = std::unique_ptr>(nullptr); - std::unique_ptr> OriginalExtendsFunc = std::unique_ptr>(nullptr); - std::unique_ptr> WeakRefGetterFunc = std::unique_ptr>(nullptr); - std::unique_ptr> WeakRefClearFunc = std::unique_ptr>(nullptr); - std::unique_ptr> SmartJSONStringifyFunc = std::unique_ptr>(nullptr); - std::unique_ptr> InteropReferenceCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> PointerCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> FunctionReferenceCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> UnmanagedTypeCtorFunc = std::unique_ptr>(nullptr); - - - using unique_void_ptr = std::unique_ptr; - template - auto unique_void(T * ptr) -> unique_void_ptr - { - return unique_void_ptr(ptr, [](void const * data) { - T const * p = static_cast(data); - delete p; - }); - } - std::vector cacheBoundObjects_; - template - void registerCacheBoundObject(T *ptr) { - this->cacheBoundObjects_.push_back(unique_void(ptr)); - } -private: + private: v8::Isolate* isolate_; - std::shared_ptr> context_; - int isolateId_; + std::shared_ptr> poWorker_; + void* userData_; + }; + + Caches(v8::Isolate* isolate, const int& isolateId_ = -1); + ~Caches(); + + bool isWorker = false; + + static std::shared_ptr> Metadata; + static std::shared_ptr< + ConcurrentMap>> + Workers; + + inline static std::shared_ptr Init(v8::Isolate* isolate, + const int& isolateId) { + auto cache = std::make_shared(isolate, isolateId); + // create a new shared_ptr that will live until Remove is called + isolate->SetData(0, static_cast(new std::shared_ptr(cache))); + return cache; + } + inline static std::shared_ptr Get(v8::Isolate* isolate) { + auto cache = isolate->GetData(0); + if (cache != nullptr) { + return *reinterpret_cast*>(cache); + } + // this should only happen when an isolate is accessed after disposal + // so we return a dummy cache + return std::make_shared(isolate); + } + static void Remove(v8::Isolate* isolate); + + inline int getIsolateId() { return isolateId_; } + + inline void InvalidateIsolate() { isolateId_ = -1; } + + inline bool IsValid() { return isolateId_ != -1; } + + void SetContext(v8::Local context); + v8::Local GetContext(); + + // Per-isolate unhandled promise rejection tracking. Fed by + // NativeScriptException::OnPromiseRejected and drained once per runloop turn. + std::unique_ptr PromiseRejections; + + robin_hood::unordered_map>> + Prototypes; + robin_hood::unordered_map>> + ClassPrototypes; + robin_hood::unordered_map< + const BaseClassMeta*, + std::unique_ptr>> + CtorFuncTemplates; + robin_hood::unordered_map>> + CtorFuncs; + robin_hood::unordered_map>> + ProtocolCtorFuncs; + robin_hood::unordered_map>> + StructConstructorFunctions; + robin_hood::unordered_map>> + PrimitiveInteropTypes; + robin_hood::unordered_map>> + CFunctions; + + robin_hood::unordered_map>> + Instances; + robin_hood::unordered_map, + std::shared_ptr>, + pair_hash> + StructInstances; + robin_hood::unordered_map>> + PointerInstances; + + std::function( + v8::Local, const BaseClassMeta*, KnownUnknownClassPair, + const std::vector&)> + ObjectCtorInitializer; + std::function(v8::Local, StructInfo)> + StructCtorInitializer; + robin_hood::unordered_map Timers; + robin_hood::unordered_map> + Initializers; + + std::unique_ptr> EmptyObjCtorFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> EmptyStructCtorFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> SliceFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> OriginalExtendsFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> WeakRefGetterFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> WeakRefClearFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> SmartJSONStringifyFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> InteropReferenceCtorFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> PointerCtorFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> FunctionReferenceCtorFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> UnmanagedTypeCtorFunc = + std::unique_ptr>(nullptr); + + // Phase 2 WHATWG error-events dispatch closures returned by the bootstrap + // IIFE (NativeScriptException::InitErrorEvents). They close over the internal + // listener store, so native dispatch keeps working even if app code + // overwrites globalThis.dispatchEvent. Cleaned up with the other Persistent + // members when Caches is destroyed, before isolate disposal. + std::unique_ptr> DispatchErrorEventFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> DispatchUnhandledRejectionFunc = + std::unique_ptr>(nullptr); + std::unique_ptr> DispatchRejectionHandledFunc = + std::unique_ptr>(nullptr); + + // Phase 3 per-isolate brand for interop.escapeException. An isolate-private + // symbol (v8::Private, not a plain Symbol) so user code cannot discover or + // forge it. Created lazily via ArgConverter::GetEscapeExceptionBrand and used + // to mark/extract escaped native exceptions across JS<->native boundaries. + std::unique_ptr> EscapeExceptionBrand = + std::unique_ptr>(nullptr); + + using unique_void_ptr = std::unique_ptr; + template + auto unique_void(T* ptr) -> unique_void_ptr { + return unique_void_ptr(ptr, [](void const* data) { + T const* p = static_cast(data); + delete p; + }); + } + std::vector cacheBoundObjects_; + template + void registerCacheBoundObject(T* ptr) { + this->cacheBoundObjects_.push_back(unique_void(ptr)); + } + + private: + v8::Isolate* isolate_; + std::shared_ptr> context_; + int isolateId_; }; -} +} // namespace tns #endif /* Caches_h */ diff --git a/NativeScript/runtime/ClassBuilder.mm b/NativeScript/runtime/ClassBuilder.mm index 49c9176d..fe53cef4 100644 --- a/NativeScript/runtime/ClassBuilder.mm +++ b/NativeScript/runtime/ClassBuilder.mm @@ -60,7 +60,8 @@ auto cache = Caches::Get(isolate); auto isolateId = cache->getIsolateId(); - Class extendedClass = ClassBuilder::GetExtendedClass(baseClassName, staticClassName, std::to_string(isolateId) + "_"); + Class extendedClass = ClassBuilder::GetExtendedClass(baseClassName, staticClassName, + std::to_string(isolateId) + "_"); class_addProtocol(extendedClass, @protocol(TNSDerivedClass)); class_addProtocol(object_getClass(extendedClass), @protocol(TNSDerivedClass)); @@ -214,8 +215,8 @@ std::string extendedClassName = tns::ToString(isolate, extendedClassCtorFunc->GetName()); auto isolateId = cache->getIsolateId(); - __block Class extendedClass = - ClassBuilder::GetExtendedClass(baseClassName, extendedClassName, std::to_string(isolateId) + "_"); + __block Class extendedClass = ClassBuilder::GetExtendedClass( + baseClassName, extendedClassName, std::to_string(isolateId) + "_"); class_addProtocol(extendedClass, @protocol(TNSDerivedClass)); class_addProtocol(object_getClass(extendedClass), @protocol(TNSDerivedClass)); @@ -892,24 +893,40 @@ FFIMethodCallback getterCallback = [](ffi_cif* cif, void* retValue, void** argValues, void* userData) { PropertyCallbackContext* context = static_cast(userData); - v8::Locker locker(context->isolate_); - Isolate::Scope isolate_scope(context->isolate_); - HandleScope handle_scope(context->isolate_); - Local getterFunc = context->callback_->Get(context->isolate_); - Local res; - - id thiz = *static_cast(argValues[0]); - auto cache = Caches::Get(context->isolate_); - auto it = cache->Instances.find(thiz); - Local self_ = it != cache->Instances.end() - ? it->second->Get(context->isolate_).As() - : context->implementationObject_->Get(context->isolate_); - Local v8Context = Caches::Get(context->isolate_)->GetContext(); - tns::Assert(getterFunc->Call(v8Context, self_, 0, nullptr).ToLocal(&res), - context->isolate_); - - const TypeEncoding* typeEncoding = context->meta_->getter()->encodings()->first(); - ArgConverter::SetValue(v8Context, retValue, res, typeEncoding); + Isolate* isolate = context->isolate_; + // Scopes-before-@throw: a branded escape is captured and @thrown only + // after every V8 scope in the inner block has destructed. + NSException* __strong pendingThrow = nil; + { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + Local getterFunc = context->callback_->Get(isolate); + Local res; + + id thiz = *static_cast(argValues[0]); + auto cache = Caches::Get(isolate); + auto it = cache->Instances.find(thiz); + Local self_ = it != cache->Instances.end() + ? it->second->Get(isolate).As() + : context->implementationObject_->Get(isolate); + Local v8Context = Caches::Get(isolate)->GetContext(); + TryCatch tc(isolate); + if (!getterFunc->Call(v8Context, self_, 0, nullptr).ToLocal(&res)) { + NSException* ex = ArgConverter::HandleBoundaryException(v8Context, tc); + if (ex != nil) { + pendingThrow = ex; + } + // Default getter result on a JS throw: zeroed return buffer. + memset(retValue, 0, cif->rtype->size); + } else { + const TypeEncoding* typeEncoding = context->meta_->getter()->encodings()->first(); + ArgConverter::SetValue(v8Context, retValue, res, typeEncoding); + } + } + if (pendingThrow != nil) { + @throw pendingThrow; + } }; const TypeEncoding* typeEncoding = propertyMeta->getter()->encodings()->first(); IMP impGetter = Interop::CreateMethod(2, 0, typeEncoding, getterCallback, userData); @@ -926,29 +943,41 @@ FFIMethodCallback setterCallback = [](ffi_cif* cif, void* retValue, void** argValues, void* userData) { PropertyCallbackContext* context = static_cast(userData); - v8::Locker locker(context->isolate_); - Isolate::Scope isolate_scope(context->isolate_); - HandleScope handle_scope(context->isolate_); - Local setterFunc = context->callback_->Get(context->isolate_); - Local res; - - id thiz = *static_cast(argValues[0]); - auto cache = Caches::Get(context->isolate_); - auto it = cache->Instances.find(thiz); - Local self_ = it != cache->Instances.end() - ? it->second->Get(context->isolate_).As() - : context->implementationObject_->Get(context->isolate_); - - uint8_t* argBuffer = (uint8_t*)argValues[2]; - const TypeEncoding* typeEncoding = context->meta_->setter()->encodings()->first()->next(); - BaseCall call(argBuffer); - Local v8Context = Caches::Get(context->isolate_)->GetContext(); - Local jsWrapper = Interop::GetResult(v8Context, typeEncoding, &call, true); - Local params[1] = {jsWrapper}; - - tns::Assert(setterFunc->Call(context->isolate_->GetCurrentContext(), self_, 1, params) - .ToLocal(&res), - context->isolate_); + Isolate* isolate = context->isolate_; + NSException* __strong pendingThrow = nil; + { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + Local setterFunc = context->callback_->Get(isolate); + Local res; + + id thiz = *static_cast(argValues[0]); + auto cache = Caches::Get(isolate); + auto it = cache->Instances.find(thiz); + Local self_ = it != cache->Instances.end() + ? it->second->Get(isolate).As() + : context->implementationObject_->Get(isolate); + + uint8_t* argBuffer = (uint8_t*)argValues[2]; + const TypeEncoding* typeEncoding = context->meta_->setter()->encodings()->first()->next(); + BaseCall call(argBuffer); + Local v8Context = Caches::Get(isolate)->GetContext(); + Local jsWrapper = Interop::GetResult(v8Context, typeEncoding, &call, true); + Local params[1] = {jsWrapper}; + + TryCatch tc(isolate); + if (!setterFunc->Call(v8Context, self_, 1, params).ToLocal(&res)) { + NSException* ex = ArgConverter::HandleBoundaryException(v8Context, tc); + if (ex != nil) { + pendingThrow = ex; + } + // Setter returns void; nothing to write on a JS throw. + } + } + if (pendingThrow != nil) { + @throw pendingThrow; + } }; const TypeEncoding* typeEncoding = propertyMeta->setter()->encodings()->first(); diff --git a/NativeScript/runtime/DataWrapper.h b/NativeScript/runtime/DataWrapper.h index 2625acd0..266a77e9 100644 --- a/NativeScript/runtime/DataWrapper.h +++ b/NativeScript/runtime/DataWrapper.h @@ -524,6 +524,13 @@ class WorkerWrapper : public BaseDataWrapper { const std::string& source, const std::string& stackTrace, int lineNumber, bool async = true); + // Forwards a drained unhandled promise rejection to the main isolate's + // worker.onerror, guarded against teardown. Shares marshaling with the + // exception overload above. + void PassUncaughtRejectionToMain(const std::string& message, + const std::string& source, + const std::string& stackTrace, + int lineNumber, bool async = true); void PostMessage(std::shared_ptr message); void Close(); void Terminate(); @@ -561,6 +568,10 @@ class WorkerWrapper : public BaseDataWrapper { void BackgroundLooper(std::function func); void DrainPendingTasks(); + void ForwardErrorPayloadToMain(const std::string& message, + const std::string& source, + const std::string& stackTrace, int lineNumber, + bool async); v8::Local ConstructErrorObject(v8::Local context, std::string message, std::string source, diff --git a/NativeScript/runtime/DictionaryAdapter.mm b/NativeScript/runtime/DictionaryAdapter.mm index 1c548163..3cc5a6f0 100644 --- a/NativeScript/runtime/DictionaryAdapter.mm +++ b/NativeScript/runtime/DictionaryAdapter.mm @@ -1,9 +1,10 @@ -#import #include "DictionaryAdapter.h" +#import +#include "ArgConverter.h" +#include "Caches.h" #include "DataWrapper.h" #include "Helpers.h" #include "Interop.h" -#include "Caches.h" #include "IsolateWrapper.h" using namespace v8; @@ -11,31 +12,40 @@ @interface DictionaryAdapterMapKeysEnumerator : NSEnumerator -- (instancetype)initWithMap:(std::shared_ptr>)map isolate:(Isolate*)isolate cache:(std::shared_ptr)cache; +- (instancetype)initWithMap:(std::shared_ptr>)map + isolate:(Isolate*)isolate + cache:(std::shared_ptr)cache; @end @implementation DictionaryAdapterMapKeysEnumerator { - IsolateWrapper* wrapper_; - uint32_t index_; - std::shared_ptr> map_; + IsolateWrapper* wrapper_; + uint32_t index_; + std::shared_ptr> map_; } -- (instancetype)initWithMap:(std::shared_ptr>)map isolate:(Isolate*)isolate cache:(std::shared_ptr)cache { - if (self) { - self->wrapper_ = new IsolateWrapper(isolate); - self->index_ = 0; - self->map_ = map; - } +- (instancetype)initWithMap:(std::shared_ptr>)map + isolate:(Isolate*)isolate + cache:(std::shared_ptr)cache { + if (self) { + self->wrapper_ = new IsolateWrapper(isolate); + self->index_ = 0; + self->map_ = map; + } - return self; + return self; } - (id)nextObject { - if (!wrapper_->IsValid()) { - return nil; - } - Isolate* isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return nil; + } + Isolate* isolate = wrapper_->Isolate(); + NSString* result = nil; + // Scopes-before-@throw: keep V8 scopes in an inner block so a branded escape + // is @thrown only after they destruct. + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -44,67 +54,89 @@ - (id)nextObject { Local array = self->map_->Get(isolate).As()->AsArray(); if (self->index_ < array->Length() - 1) { - Local key; - bool success = array->Get(context, self->index_).ToLocal(&key); - tns::Assert(success, isolate); + Local key; + TryCatch tc(isolate); + if (array->Get(context, self->index_).ToLocal(&key)) { self->index_ += 2; - NSString* result = tns::ToNSString(isolate, key); - return result; + result = tns::ToNSString(isolate, key); + } else { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } } - - return nil; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (void)dealloc { - self->map_ = nil; - delete self->wrapper_; - - [super dealloc]; + self->map_ = nil; + delete self->wrapper_; + + [super dealloc]; } @end @interface DictionaryAdapterObjectKeysEnumerator : NSEnumerator -- (instancetype)initWithProperties:(std::shared_ptr>)dictionary isolate:(Isolate*)isolate cache:(std::shared_ptr)cache; +- (instancetype)initWithProperties:(std::shared_ptr>)dictionary + isolate:(Isolate*)isolate + cache:(std::shared_ptr)cache; - (Local)getProperties; @end @implementation DictionaryAdapterObjectKeysEnumerator { - IsolateWrapper* wrapper_; - std::shared_ptr> dictionary_; - NSUInteger index_; + IsolateWrapper* wrapper_; + std::shared_ptr> dictionary_; + NSUInteger index_; } -- (instancetype)initWithProperties:(std::shared_ptr>)dictionary isolate:(Isolate*)isolate cache:(std::shared_ptr)cache { - if (self) { - self->wrapper_ = new IsolateWrapper(isolate); - self->dictionary_ = dictionary; - self->index_ = 0; - } +- (instancetype)initWithProperties:(std::shared_ptr>)dictionary + isolate:(Isolate*)isolate + cache:(std::shared_ptr)cache { + if (self) { + self->wrapper_ = new IsolateWrapper(isolate); + self->dictionary_ = dictionary; + self->index_ = 0; + } - return self; + return self; } - (Local)getProperties { - Isolate* isolate = wrapper_->Isolate(); - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - EscapableHandleScope handle_scope(isolate); - - Local context = wrapper_->GetCache()->GetContext(); - Local properties; - Local dictionary = self->dictionary_->Get(isolate).As(); - tns::Assert(dictionary->GetOwnPropertyNames(context).ToLocal(&properties), isolate); - return handle_scope.Escape(properties); + Isolate* isolate = wrapper_->Isolate(); + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + EscapableHandleScope handle_scope(isolate); + + Local context = wrapper_->GetCache()->GetContext(); + Local properties; + Local dictionary = self->dictionary_->Get(isolate).As(); + TryCatch tc(isolate); + if (!dictionary->GetOwnPropertyNames(context).ToLocal(&properties)) { + // This helper runs under the caller's V8 scopes and returns a Local, so a + // branded escape cannot be safely @thrown from here. Report through the + // uncaught path and return an empty array; the caller yields its default. + ArgConverter::HandleBoundaryException(context, tc); + properties = v8::Array::New(isolate, 0); + } + return handle_scope.Escape(properties); } - (id)nextObject { - if (!wrapper_->IsValid()) { - return nil; - } - Isolate* isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return nil; + } + Isolate* isolate = wrapper_->Isolate(); + NSString* result = nil; + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -112,69 +144,92 @@ - (id)nextObject { Local context = wrapper_->GetCache()->GetContext(); Local properties = [self getProperties]; if (self->index_ < properties->Length()) { - Local value; - bool success = properties->Get(context, (uint)self->index_).ToLocal(&value); - tns::Assert(success, isolate); + Local value; + TryCatch tc(isolate); + if (properties->Get(context, (uint)self->index_).ToLocal(&value)) { self->index_++; - return tns::ToNSString(isolate, value); + result = tns::ToNSString(isolate, value); + } else { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } } - - return nil; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (NSArray*)allObjects { - if (!wrapper_->IsValid()) { - return nil; - } - Isolate* isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return nil; + } + Isolate* isolate = wrapper_->Isolate(); + NSMutableArray* array = [NSMutableArray array]; + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); Local context = wrapper_->GetCache()->GetContext(); - NSMutableArray* array = [NSMutableArray array]; Local properties = [self getProperties]; for (int i = 0; i < properties->Length(); i++) { - Local value; - bool success = properties->Get(context, i).ToLocal(&value); - tns::Assert(success, isolate); - [array addObject:tns::ToNSString(isolate, value)]; + Local value; + TryCatch tc(isolate); + if (!properties->Get(context, i).ToLocal(&value)) { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + break; + } + [array addObject:tns::ToNSString(isolate, value)]; } - - return array; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return array; } - (void)dealloc { - self->dictionary_ = nil; - delete self->wrapper_; - - [super dealloc]; + self->dictionary_ = nil; + delete self->wrapper_; + + [super dealloc]; } @end @implementation DictionaryAdapter { - IsolateWrapper* wrapper_; - std::shared_ptr> object_; - ObjCDataWrapper* dataWrapper_; + IsolateWrapper* wrapper_; + std::shared_ptr> object_; + ObjCDataWrapper* dataWrapper_; } - (instancetype)initWithJSObject:(Local)jsObject isolate:(Isolate*)isolate { - if (self) { - self->wrapper_ = new IsolateWrapper(isolate); - self->object_ = std::make_shared>(isolate, jsObject); - self->wrapper_->GetCache()->Instances.emplace(self, self->object_); - tns::SetValue(isolate, jsObject, (self->dataWrapper_ = new ObjCDataWrapper(self))); - } - - return self; + if (self) { + self->wrapper_ = new IsolateWrapper(isolate); + self->object_ = std::make_shared>(isolate, jsObject); + self->wrapper_->GetCache()->Instances.emplace(self, self->object_); + tns::SetValue(isolate, jsObject, (self->dataWrapper_ = new ObjCDataWrapper(self))); + } + + return self; } - (NSUInteger)count { - if (!wrapper_->IsValid()) { - return 0; - } - Isolate* isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return 0; + } + Isolate* isolate = wrapper_->Isolate(); + NSUInteger result = 0; + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -182,23 +237,35 @@ - (NSUInteger)count { Local obj = self->object_->Get(isolate).As(); if (obj->IsMap()) { - return obj.As()->Size(); + result = obj.As()->Size(); + } else { + Local context = wrapper_->GetCache()->GetContext(); + Local properties; + TryCatch tc(isolate); + if (obj->GetOwnPropertyNames(context).ToLocal(&properties)) { + result = properties->Length(); + } else { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } } - - Local context = wrapper_->GetCache()->GetContext(); - Local properties; - tns::Assert(obj->GetOwnPropertyNames(context).ToLocal(&properties), isolate); - - uint32_t length = properties->Length(); - - return length; + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (id)objectForKey:(id)aKey { - if (!wrapper_->IsValid()) { - return nil; - } - Isolate* isolate = wrapper_->Isolate(); + if (!wrapper_->IsValid()) { + return nil; + } + Isolate* isolate = wrapper_->Isolate(); + id result = nil; + NSException* __strong pendingThrow = nil; + { v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -207,74 +274,89 @@ - (id)objectForKey:(id)aKey { Local obj = self->object_->Get(isolate).As(); Local value; + bool got = false; + TryCatch tc(isolate); if ([aKey isKindOfClass:[NSNumber class]]) { - unsigned int key = [aKey unsignedIntValue]; - bool success = obj->Get(context, key).ToLocal(&value); - tns::Assert(success, isolate); + unsigned int key = [aKey unsignedIntValue]; + got = obj->Get(context, key).ToLocal(&value); } else if ([aKey isKindOfClass:[NSString class]]) { - NSString* key = (NSString*)aKey; - Local keyV8Str = tns::ToV8String(isolate, key); - - if (obj->IsMap()) { - Local map = obj.As(); - bool success = map->Get(context, keyV8Str).ToLocal(&value); - tns::Assert(success, isolate); - } else { - bool success = obj->Get(context, keyV8Str).ToLocal(&value); - tns::Assert(success, isolate); - } + NSString* key = (NSString*)aKey; + Local keyV8Str = tns::ToV8String(isolate, key); + + if (obj->IsMap()) { + Local map = obj.As(); + got = map->Get(context, keyV8Str).ToLocal(&value); + } else { + got = obj->Get(context, keyV8Str).ToLocal(&value); + } } else { - // TODO: unsupported key type - tns::Assert(false, isolate); + // Unsupported key type: return the adapter default rather than aborting. + got = false; } - id result = Interop::ToObject(context, value); - - return result; + if (got) { + result = Interop::ToObject(context, value); + } else if (tc.HasCaught()) { + NSException* ex = ArgConverter::HandleBoundaryException(context, tc); + if (ex != nil) { + pendingThrow = ex; + } + } + } + if (pendingThrow != nil) { + @throw pendingThrow; + } + return result; } - (NSEnumerator*)keyEnumerator { - if (!wrapper_->IsValid()) { - return nil; - } + if (!wrapper_->IsValid()) { + return nil; + } + Isolate* isolate = wrapper_->Isolate(); + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + + Local obj = self->object_->Get(isolate); + + if (obj->IsMap()) { + return + [[[DictionaryAdapterMapKeysEnumerator alloc] initWithMap:self->object_ + isolate:isolate + cache:wrapper_->GetCache()] autorelease]; + } + + return [[[DictionaryAdapterObjectKeysEnumerator alloc] initWithProperties:self->object_ + isolate:isolate + cache:wrapper_->GetCache()] + autorelease]; +} + +- (void)dealloc { + if (wrapper_->IsValid()) { Isolate* isolate = wrapper_->Isolate(); v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); - - Local obj = self->object_->Get(isolate); - - if (obj->IsMap()) { - return [[[DictionaryAdapterMapKeysEnumerator alloc] initWithMap:self->object_ isolate:isolate cache:wrapper_->GetCache()] autorelease]; - } - - return [[[DictionaryAdapterObjectKeysEnumerator alloc] initWithProperties:self->object_ isolate:isolate cache:wrapper_->GetCache()] autorelease]; -} - -- (void)dealloc { - if (wrapper_->IsValid()) { - Isolate* isolate = wrapper_->Isolate(); - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - wrapper_->GetCache()->Instances.erase(self); - Local value = self->object_->Get(isolate); - BaseDataWrapper* wrapper = tns::GetValue(isolate, value); - if (wrapper != nullptr) { - if (wrapper == dataWrapper_) { - dataWrapper_ = nullptr; - } - tns::DeleteValue(isolate, value); - delete wrapper; - } - } - if (dataWrapper_ != nullptr) { - delete dataWrapper_; + wrapper_->GetCache()->Instances.erase(self); + Local value = self->object_->Get(isolate); + BaseDataWrapper* wrapper = tns::GetValue(isolate, value); + if (wrapper != nullptr) { + if (wrapper == dataWrapper_) { + dataWrapper_ = nullptr; + } + tns::DeleteValue(isolate, value); + delete wrapper; } - self->object_ = nullptr; - delete self->wrapper_; - - [super dealloc]; + } + if (dataWrapper_ != nullptr) { + delete dataWrapper_; + } + self->object_ = nullptr; + delete self->wrapper_; + + [super dealloc]; } @end diff --git a/NativeScript/runtime/FastEnumerationAdapter.mm b/NativeScript/runtime/FastEnumerationAdapter.mm index f43700c1..cbb4d1d8 100644 --- a/NativeScript/runtime/FastEnumerationAdapter.mm +++ b/NativeScript/runtime/FastEnumerationAdapter.mm @@ -1,89 +1,100 @@ -#import #include "FastEnumerationAdapter.h" -#include "NativeScriptException.h" -#include "Interop.h" +#import #include "Helpers.h" #include "Interop.h" +#include "NativeScriptException.h" using namespace v8; namespace tns { -NSUInteger FastEnumerationAdapter(Isolate* isolate, id self, NSFastEnumerationState* state, __unsafe_unretained id buffer[], NSUInteger length, Persistent* poIteratorFunc) { - enum State : decltype(state->state) { - Uninitialized = 0, - Iterating, - Done - }; - - NSUInteger count = 0; - - try { - if (state->state == State::Uninitialized) { - Local iteratorRes; - Local context = isolate->GetCurrentContext(); - Local iteratorFunc = poIteratorFunc->Get(isolate); - if (!iteratorFunc->Call(context, context->Global(), 0, {}).ToLocal(&iteratorRes)) { - tns::Assert(false, isolate); - } - - tns::Assert(!iteratorRes.IsEmpty() && iteratorRes->IsObject(), isolate); - Local iteratorObj = iteratorRes.As(); - - void* selfPtr = (__bridge void*)self; - state->mutationsPtr = reinterpret_cast(selfPtr); - state->extra[0] = reinterpret_cast(new Persistent(isolate, iteratorObj)); - state->state = State::Iterating; - } - - if (state->state == State::Done) { - return 0; - } - - Persistent* poIteratorObj = reinterpret_cast*>(state->extra[0]); - Local iteratorObj = poIteratorObj->Get(isolate); - Local context = isolate->GetCurrentContext(); - Local next; - bool success = iteratorObj->Get(context, tns::ToV8String(isolate, "next")).ToLocal(&next); - tns::Assert(success && !next.IsEmpty() && next->IsFunction(), isolate); - - state->itemsPtr = buffer; - while (count < length) { - Local nextFunc = next.As(); - Local context = isolate->GetCurrentContext(); - Local nextResult; - if (!nextFunc->Call(context, iteratorObj, 0, {}).ToLocal(&nextResult)) { - tns::Assert(false, isolate); - } - - if (nextResult.IsEmpty() || !nextResult->IsObject()) { - throw NativeScriptException("The \"next\" method must return an object with at least the \"value\" or \"done\" properties"); - } - - Local done; - bool success = nextResult.As()->Get(context, tns::ToV8String(isolate, "done")).ToLocal(&done); - tns::Assert(success && tns::IsBool(done), isolate); - - if (tns::ToBool(done)) { - poIteratorObj->Reset(); - delete poIteratorObj; - state->state = State::Done; - break; - } - - Local value; - success = nextResult.As()->Get(context, tns::ToV8String(isolate, "value")).ToLocal(&value); - tns::Assert(success && !value.IsEmpty(), isolate); - - id result = Interop::ToObject(context, value); - *buffer++ = result; - count++; - } - } catch (NativeScriptException& ex) { - ex.ReThrowToV8(isolate); +NSUInteger FastEnumerationAdapter(Isolate* isolate, id self, NSFastEnumerationState* state, + __unsafe_unretained id buffer[], NSUInteger length, + Persistent* poIteratorFunc) { + enum State : decltype(state->state) { Uninitialized = 0, Iterating, Done }; + + NSUInteger count = 0; + + try { + if (state->state == State::Uninitialized) { + Local iteratorRes; + Local context = isolate->GetCurrentContext(); + Local iteratorFunc = poIteratorFunc->Get(isolate); + TryCatch tc(isolate); + if (!iteratorFunc->Call(context, context->Global(), 0, {}).ToLocal(&iteratorRes)) { + // JS threw while obtaining the iterator. Surface it to V8 (the + // outer catch ReThrowsToV8) rather than aborting the process. + // NOTE: this native fast-enumeration boundary runs under the + // caller's V8 scopes, so a branded escapeException cannot be + // safely @thrown here; it surfaces as a JS exception instead. + throw NativeScriptException(isolate, tc, "Error calling the iterator function"); + } + + tns::Assert(!iteratorRes.IsEmpty() && iteratorRes->IsObject(), isolate); + Local iteratorObj = iteratorRes.As(); + + void* selfPtr = (__bridge void*)self; + state->mutationsPtr = reinterpret_cast(selfPtr); + state->extra[0] = + reinterpret_cast(new Persistent(isolate, iteratorObj)); + state->state = State::Iterating; } - return count; -} + if (state->state == State::Done) { + return 0; + } + Persistent* poIteratorObj = reinterpret_cast*>(state->extra[0]); + Local iteratorObj = poIteratorObj->Get(isolate); + Local context = isolate->GetCurrentContext(); + Local next; + bool success = iteratorObj->Get(context, tns::ToV8String(isolate, "next")).ToLocal(&next); + tns::Assert(success && !next.IsEmpty() && next->IsFunction(), isolate); + + state->itemsPtr = buffer; + while (count < length) { + Local nextFunc = next.As(); + Local context = isolate->GetCurrentContext(); + Local nextResult; + TryCatch tc(isolate); + if (!nextFunc->Call(context, iteratorObj, 0, {}).ToLocal(&nextResult)) { + // Surface a JS throw from the iterator's next() to V8 rather than + // aborting (see note above about the branded-escape limitation). + throw NativeScriptException(isolate, tc, + "Error calling the \"next\" method of the iterator"); + } + + if (nextResult.IsEmpty() || !nextResult->IsObject()) { + throw NativeScriptException("The \"next\" method must return an object with at least the " + "\"value\" or \"done\" properties"); + } + + Local done; + bool success = + nextResult.As()->Get(context, tns::ToV8String(isolate, "done")).ToLocal(&done); + tns::Assert(success && tns::IsBool(done), isolate); + + if (tns::ToBool(done)) { + poIteratorObj->Reset(); + delete poIteratorObj; + state->state = State::Done; + break; + } + + Local value; + success = + nextResult.As()->Get(context, tns::ToV8String(isolate, "value")).ToLocal(&value); + tns::Assert(success && !value.IsEmpty(), isolate); + + id result = Interop::ToObject(context, value); + *buffer++ = result; + count++; + } + } catch (NativeScriptException& ex) { + ex.ReThrowToV8(isolate); + } + + return count; } + +} // namespace tns diff --git a/NativeScript/runtime/Interop.h b/NativeScript/runtime/Interop.h index 3980be17..db7bd20f 100644 --- a/NativeScript/runtime/Interop.h +++ b/NativeScript/runtime/Interop.h @@ -3,203 +3,221 @@ #include #include -#include "libffi.h" + #include "Common.h" -#include "Metadata.h" #include "DataWrapper.h" #include "FFICall.h" +#include "Metadata.h" +#include "libffi.h" namespace tns { -typedef void (*FFIMethodCallback)(ffi_cif* cif, void* retValue, void** argValues, void* userData); +typedef void (*FFIMethodCallback)(ffi_cif* cif, void* retValue, + void** argValues, void* userData); struct MethodCall { - MethodCall(v8::Local context, - bool isPrimitiveFunction, - void* functionPointer, - const TypeEncoding* typeEncoding, - V8Args& args, - id target, - Class clazz, - SEL selector, - bool callSuper, - MetaType metaType, - bool provideErrorOutParameter, - bool ownsReturnedObject, - bool returnsUnmanaged, - bool isInitializer) - : context_(context), - isPrimitiveFunction_(isPrimitiveFunction), - functionPointer_(functionPointer), - typeEncoding_(typeEncoding), - args_(args), - target_(target), - clazz_(clazz), - selector_(selector), - callSuper_(callSuper), - metaType_(metaType), - provideErrorOutParameter_(provideErrorOutParameter), - ownsReturnedObject_(ownsReturnedObject), - returnsUnmanaged_(returnsUnmanaged), - isInitializer_(isInitializer) { - } + MethodCall(v8::Local context, bool isPrimitiveFunction, + void* functionPointer, const TypeEncoding* typeEncoding, + V8Args& args, id target, Class clazz, SEL selector, bool callSuper, + MetaType metaType, bool provideErrorOutParameter, + bool ownsReturnedObject, bool returnsUnmanaged, bool isInitializer) + : context_(context), + isPrimitiveFunction_(isPrimitiveFunction), + functionPointer_(functionPointer), + typeEncoding_(typeEncoding), + args_(args), + target_(target), + clazz_(clazz), + selector_(selector), + callSuper_(callSuper), + metaType_(metaType), + provideErrorOutParameter_(provideErrorOutParameter), + ownsReturnedObject_(ownsReturnedObject), + returnsUnmanaged_(returnsUnmanaged), + isInitializer_(isInitializer) {} - v8::Local context_; - bool isPrimitiveFunction_; - void* functionPointer_; - const TypeEncoding* typeEncoding_; - V8Args& args_; - id target_; - Class clazz_; - SEL selector_; - bool callSuper_; - MetaType metaType_; - bool provideErrorOutParameter_; - bool ownsReturnedObject_; - bool returnsUnmanaged_; - bool isInitializer_; + v8::Local context_; + bool isPrimitiveFunction_; + void* functionPointer_; + const TypeEncoding* typeEncoding_; + V8Args& args_; + id target_; + Class clazz_; + SEL selector_; + bool callSuper_; + MetaType metaType_; + bool provideErrorOutParameter_; + bool ownsReturnedObject_; + bool returnsUnmanaged_; + bool isInitializer_; }; -struct CMethodCall: MethodCall { - CMethodCall( - v8::Local context, - void* functionPointer, - const TypeEncoding* typeEncoding, - V8Args& args, - bool ownsReturnedObject, - bool returnsUnmanaged) - : MethodCall( - context, - true, - functionPointer, - typeEncoding, - args, - nil, - nil, - nil, - false, - MetaType::Undefined, - false, - ownsReturnedObject, - returnsUnmanaged, - false) { - } +struct CMethodCall : MethodCall { + CMethodCall(v8::Local context, void* functionPointer, + const TypeEncoding* typeEncoding, V8Args& args, + bool ownsReturnedObject, bool returnsUnmanaged) + : MethodCall(context, true, functionPointer, typeEncoding, args, nil, nil, + nil, false, MetaType::Undefined, false, ownsReturnedObject, + returnsUnmanaged, false) {} }; -struct ObjCMethodCall: public MethodCall { - ObjCMethodCall( - v8::Local context, - const MethodMeta* meta, - id target, - Class clazz, - V8Args& args, - bool callSuper) - : MethodCall( - context, - false, - callSuper ? (void*)objc_msgSendSuper : (void*)objc_msgSend, - meta->encodings()->first(), - args, - target, - clazz, - meta->selector(), - callSuper, - meta->type(), - meta->hasErrorOutParameter() && args.Length() < meta->encodings()->count - 1, - meta->ownsReturnedCocoaObject(), - false, - meta->isInitializer()) { - } +struct ObjCMethodCall : public MethodCall { + ObjCMethodCall(v8::Local context, const MethodMeta* meta, + id target, Class clazz, V8Args& args, bool callSuper) + : MethodCall(context, false, + callSuper ? (void*)objc_msgSendSuper : (void*)objc_msgSend, + meta->encodings()->first(), args, target, clazz, + meta->selector(), callSuper, meta->type(), + meta->hasErrorOutParameter() && + args.Length() < meta->encodings()->count - 1, + meta->ownsReturnedCocoaObject(), false, + meta->isInitializer()) {} }; class Interop { -public: - static void RegisterInteropTypes(v8::Local context); - static IMP CreateMethod(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData); - static id CallInitializer(v8::Local context, const MethodMeta* methodMeta, id target, Class clazz, V8Args& args); - static v8::Local CallFunction(ObjCMethodCall& methodCall); - static v8::Local CallFunction(CMethodCall& methodCall); - static v8::Local GetResultByType(v8::Local context, BaseDataWrapper* typeWrapper, BaseCall* call, std::shared_ptr> parentStruct = nullptr); - static v8::Local GetResult(v8::Local context, const TypeEncoding* typeEncoding, BaseCall* call, bool marshalToPrimitive, std::shared_ptr> parentStruct = nullptr, bool isStructMember = false, bool ownsReturnedObject = false, bool returnsUnmanaged = false, bool isInitializer = false); - static void SetStructPropertyValue(v8::Local context, StructWrapper* wrapper, StructField field, v8::Local value); - static void InitializeStruct(v8::Local context, void* destBuffer, std::vector fields, v8::Local inititalizer); - static void WriteTypeValue(v8::Local context, BaseDataWrapper* typeWrapper, void* dest, v8::Local arg); - static void WriteValue(v8::Local context, const TypeEncoding* typeEncoding, void* dest, v8::Local arg); - static id ToObject(v8::Local context, v8::Local arg); - static v8::Local GetPrimitiveReturnType(v8::Local context, BinaryTypeEncodingType type, BaseCall* call); -private: - static void ExecuteWriteValueDebugValidationsIfInDebug(v8::Local context, const TypeEncoding* typeEncoding, void* dest, v8::Local arg); - static std::pair CreateMethodInternal(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData); - static CFTypeRef CreateBlock(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData); - template - static void SetStructValue(v8::Local value, void* destBuffer, ptrdiff_t position); - static void InitializeStruct(v8::Local context, void* destBuffer, std::vector fields, v8::Local inititalizer, ptrdiff_t& position); - static void RegisterInteropType(v8::Local context, v8::Local types, std::string name, PrimitiveDataWrapper* wrapper, bool autoDelete = true); - static void RegisterBufferFromDataFunction(v8::Local context, v8::Local interop); - static void RegisterStringFromCString(v8::Local context, v8::Local interop); - static void RegisterHandleOfFunction(v8::Local context, v8::Local interop); - static void RegisterAllocFunction(v8::Local context, v8::Local interop); - static void RegisterFreeFunction(v8::Local context, v8::Local interop); - static void RegisterAdoptFunction(v8::Local context, v8::Local interop); - static void RegisterSizeOfFunction(v8::Local context, v8::Local interop); - static void SetFFIParams(v8::Local context, const TypeEncoding* typeEncoding, FFICall* call, const int argsCount, const int initialParameterIndex, V8Args& args); - static bool isRefTypeEqual(const TypeEncoding* typeEncoding,const char* clazz); - static v8::Local ToArray(v8::Local object); - static v8::Local StructToValue(v8::Local context, void* result, StructInfo structInfo, std::shared_ptr> parentStruct); - static const TypeEncoding* CreateEncoding(BinaryTypeEncodingType type); - static v8::Local HandleOf(v8::Local context, v8::Local value); - static v8::Local CallFunctionInternal(MethodCall& methodCall); - static bool IsNumbericType(BinaryTypeEncodingType type); - static v8::Local GetInteropType(v8::Local context, BinaryTypeEncodingType type); - static std::vector GetAdditionalProtocols(const TypeEncoding* typeEncoding); - static SEL GetSwizzledMethodSelector(SEL selector); - - template - static inline void SetValue(void* dest, T value) { - if (std::is_same::value) { - memcpy(dest, &value, sizeof(SEL*)); - } else { - *static_cast(dest) = value; - } + public: + static void RegisterInteropTypes(v8::Local context); + static IMP CreateMethod(const uint8_t initialParamIndex, + const uint8_t argsCount, + const TypeEncoding* typeEncoding, + FFIMethodCallback callback, void* userData); + static id CallInitializer(v8::Local context, + const MethodMeta* methodMeta, id target, + Class clazz, V8Args& args); + static v8::Local CallFunction(ObjCMethodCall& methodCall); + static v8::Local CallFunction(CMethodCall& methodCall); + static v8::Local GetResultByType( + v8::Local context, BaseDataWrapper* typeWrapper, + BaseCall* call, + std::shared_ptr> parentStruct = nullptr); + static v8::Local GetResult( + v8::Local context, const TypeEncoding* typeEncoding, + BaseCall* call, bool marshalToPrimitive, + std::shared_ptr> parentStruct = nullptr, + bool isStructMember = false, bool ownsReturnedObject = false, + bool returnsUnmanaged = false, bool isInitializer = false); + static void SetStructPropertyValue(v8::Local context, + StructWrapper* wrapper, StructField field, + v8::Local value); + static void InitializeStruct(v8::Local context, void* destBuffer, + std::vector fields, + v8::Local inititalizer); + static void WriteTypeValue(v8::Local context, + BaseDataWrapper* typeWrapper, void* dest, + v8::Local arg); + static void WriteValue(v8::Local context, + const TypeEncoding* typeEncoding, void* dest, + v8::Local arg); + static id ToObject(v8::Local context, v8::Local arg); + static v8::Local GetPrimitiveReturnType( + v8::Local context, BinaryTypeEncodingType type, + BaseCall* call); + + private: + static void ExecuteWriteValueDebugValidationsIfInDebug( + v8::Local context, const TypeEncoding* typeEncoding, + void* dest, v8::Local arg); + static std::pair CreateMethodInternal( + const uint8_t initialParamIndex, const uint8_t argsCount, + const TypeEncoding* typeEncoding, FFIMethodCallback callback, + void* userData); + static CFTypeRef CreateBlock(const uint8_t initialParamIndex, + const uint8_t argsCount, + const TypeEncoding* typeEncoding, + FFIMethodCallback callback, void* userData); + template + static void SetStructValue(v8::Local value, void* destBuffer, + ptrdiff_t position); + static void InitializeStruct(v8::Local context, void* destBuffer, + std::vector fields, + v8::Local inititalizer, + ptrdiff_t& position); + static void RegisterInteropType(v8::Local context, + v8::Local types, std::string name, + PrimitiveDataWrapper* wrapper, + bool autoDelete = true); + static void RegisterBufferFromDataFunction(v8::Local context, + v8::Local interop); + static void RegisterStringFromCString(v8::Local context, + v8::Local interop); + static void RegisterHandleOfFunction(v8::Local context, + v8::Local interop); + static void RegisterAllocFunction(v8::Local context, + v8::Local interop); + static void RegisterFreeFunction(v8::Local context, + v8::Local interop); + static void RegisterAdoptFunction(v8::Local context, + v8::Local interop); + static void RegisterSizeOfFunction(v8::Local context, + v8::Local interop); + static void RegisterEscapeExceptionFunction(v8::Local context, + v8::Local interop); + static void SetFFIParams(v8::Local context, + const TypeEncoding* typeEncoding, FFICall* call, + const int argsCount, const int initialParameterIndex, + V8Args& args); + static bool isRefTypeEqual(const TypeEncoding* typeEncoding, + const char* clazz); + static v8::Local ToArray(v8::Local object); + static v8::Local StructToValue( + v8::Local context, void* result, StructInfo structInfo, + std::shared_ptr> parentStruct); + static const TypeEncoding* CreateEncoding(BinaryTypeEncodingType type); + static v8::Local HandleOf(v8::Local context, + v8::Local value); + static v8::Local CallFunctionInternal(MethodCall& methodCall); + static bool IsNumbericType(BinaryTypeEncodingType type); + static v8::Local GetInteropType(v8::Local context, + BinaryTypeEncodingType type); + static std::vector GetAdditionalProtocols( + const TypeEncoding* typeEncoding); + static SEL GetSwizzledMethodSelector(SEL selector); + + template + static inline void SetValue(void* dest, T value) { + if (std::is_same::value) { + memcpy(dest, &value, sizeof(SEL*)); + } else { + *static_cast(dest) = value; } + } - template - static void SetNumericValue(void* dest, double value) { - if (value < std::numeric_limits::lowest()) { - Interop::SetValue(dest, std::numeric_limits::lowest()); - } else if (value > std::numeric_limits::max()) { - Interop::SetValue(dest, std::numeric_limits::max()); - } else { - Interop::SetValue(dest, (T)value); - } + template + static void SetNumericValue(void* dest, double value) { + if (value < std::numeric_limits::lowest()) { + Interop::SetValue(dest, std::numeric_limits::lowest()); + } else if (value > std::numeric_limits::max()) { + Interop::SetValue(dest, std::numeric_limits::max()); + } else { + Interop::SetValue(dest, (T)value); } + } + + typedef struct JSBlock { + typedef struct { + uintptr_t reserved; + uintptr_t size; + void (*copy)(struct JSBlock*, const struct JSBlock*); + void (*dispose)(struct JSBlock*); + } JSBlockDescriptor; + + enum { + BLOCK_NEEDS_FREE = (1 << 24), // runtime + BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler + }; + + void* isa; + volatile int32_t flags; // contains ref count + int32_t reserved; + const void* invoke; + JSBlockDescriptor* descriptor; + void* userData; + ffi_closure* ffiClosure; - typedef struct JSBlock { - typedef struct { - uintptr_t reserved; - uintptr_t size; - void (*copy)(struct JSBlock*, const struct JSBlock*); - void (*dispose)(struct JSBlock*); - } JSBlockDescriptor; - - enum { - BLOCK_NEEDS_FREE = (1 << 24), // runtime - BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler - }; - - void* isa; - volatile int32_t flags; // contains ref count - int32_t reserved; - const void* invoke; - JSBlockDescriptor* descriptor; - void* userData; - ffi_closure* ffiClosure; - - static JSBlockDescriptor kJSBlockDescriptor; - } JSBlock; - + static JSBlockDescriptor kJSBlockDescriptor; + } JSBlock; }; -} +} // namespace tns #endif /* Interop_h */ diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 0097ef5b..b9d8a664 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -1651,8 +1651,45 @@ inline bool isBool() { ffi_call(parametrizedCall->Cif, FFI_FN(methodCall.functionPointer_), call.ResultBuffer(), call.ArgsArray()); } @catch (NSException* e) { - std::string message = [[e description] UTF8String]; - throw NativeScriptException(message); + // Preserve the original NSException: build a JS Error carrying its name and + // reason, and attach the wrapped native object as `nativeException` (mirrors + // the NSError-out path below). Throw it via NativeScriptException so the JS + // catch handler receives exactly this object. + Isolate* isolate = methodCall.context_->GetIsolate(); + Local context = isolate->GetCurrentContext(); + + NSString* nsName = [e name]; + NSString* nsReason = [e reason]; + std::string message = + nsReason ? [nsReason UTF8String] : (nsName ? [nsName UTF8String] : "NSException"); + + Local jsErrVal = Exception::Error(tns::ToV8String(isolate, message)); + if (jsErrVal.IsEmpty() || !jsErrVal->IsObject()) { + // Fallback: keep the description-only behavior if Error creation fails. + throw NativeScriptException([[e description] UTF8String]); + } + + Local jsErrObj = jsErrVal.As(); + if (nsName != nil) { + jsErrObj + ->Set(context, tns::ToV8String(isolate, "name"), + tns::ToV8String(isolate, [nsName UTF8String])) + .FromMaybe(false); + } + if (nsReason != nil) { + jsErrObj + ->Set(context, tns::ToV8String(isolate, "message"), + tns::ToV8String(isolate, [nsReason UTF8String])) + .FromMaybe(false); + } + + ObjCDataWrapper* wrapper = new ObjCDataWrapper((id)e); + Local nativeWrapper = + ArgConverter::CreateJsWrapper(context, wrapper, Local(), true); + jsErrObj->Set(context, tns::ToV8String(isolate, "nativeException"), nativeWrapper) + .FromMaybe(false); + + throw NativeScriptException(isolate, jsErrObj.As(), message); } if (errorRef != nullptr) { diff --git a/NativeScript/runtime/InteropTypes.mm b/NativeScript/runtime/InteropTypes.mm index 10758dc2..7542a2e8 100644 --- a/NativeScript/runtime/InteropTypes.mm +++ b/NativeScript/runtime/InteropTypes.mm @@ -1,4 +1,5 @@ #include +#include "ArgConverter.h" #include "Caches.h" #include "Constants.h" #include "FunctionReference.h" @@ -31,6 +32,7 @@ RegisterFreeFunction(context, interop); RegisterAdoptFunction(context, interop); RegisterSizeOfFunction(context, interop); + RegisterEscapeExceptionFunction(context, interop); RegisterInteropType( context, types, "noop", @@ -330,6 +332,134 @@ new PrimitiveDataWrapper(sizeof(void*), tns::Assert(success, isolate); } +// Returns the wrapped NSException value carried by `value` — either `value` +// itself when it wraps an NSException, or its `.nativeException` when `value` is +// an Error carrying a wrapped NSException. Empty handle otherwise. +static Local GetWrappedNSException(Local context, Local value) { + Isolate* isolate = context->GetIsolate(); + auto isWrappedNSException = [&](Local v) -> bool { + if (v.IsEmpty() || !v->IsObject()) { + return false; + } + BaseDataWrapper* wrapper = tns::GetValue(isolate, v); + if (wrapper == nullptr || wrapper->Type() != WrapperType::ObjCObject) { + return false; + } + id data = static_cast(wrapper)->Data(); + return [data isKindOfClass:[NSException class]]; + }; + + if (isWrappedNSException(value)) { + return value; + } + if (value->IsObject()) { + Local nativeExc; + if (value.As() + ->Get(context, tns::ToV8String(isolate, "nativeException")) + .ToLocal(&nativeExc) && + isWrappedNSException(nativeExc)) { + return nativeExc; + } + } + return Local(); +} + +void Interop::RegisterEscapeExceptionFunction(Local context, Local interop) { + Local func; + bool success = + v8::Function::New(context, [](const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + Local context = isolate->GetCurrentContext(); + + if (info.Length() < 1) { + isolate->ThrowException(Exception::TypeError(tns::ToV8String( + isolate, "interop.escapeException requires 1 argument, but only 0 present."))); + return; + } + + Local x = info[0]; + Local brand = ArgConverter::GetEscapeExceptionBrand(isolate); + + // Idempotent: an already-branded value is returned unchanged. + if (!brand.IsEmpty() && x->IsObject()) { + Maybe hasBrand = x.As()->HasPrivate(context, brand); + if (hasBrand.IsJust() && hasBrand.FromJust()) { + info.GetReturnValue().Set(x); + return; + } + } + + // Derive the message string, preferring x.message when x is an Error. + std::string message; + bool xIsObject = x->IsObject(); + if (xIsObject) { + Local msgVal; + if (x.As()->Get(context, tns::ToV8String(isolate, "message")).ToLocal(&msgVal) && + !msgVal->IsNullOrUndefined()) { + message = tns::ToString(isolate, msgVal); + } else { + message = tns::ToString(isolate, x); + } + } else { + message = tns::ToString(isolate, x); + } + + // The returned value is a real JS Error so `throw interop.escapeException(x)` + // behaves like a normal throw in pure-JS paths. + Local errVal = Exception::Error(tns::ToV8String(isolate, message)); + Local errObj = errVal.As(); + + // Copy stack from x when it is an Error carrying one. + std::string stack; + if (xIsObject) { + Local stackVal; + if (x.As()->Get(context, tns::ToV8String(isolate, "stack")).ToLocal(&stackVal) && + stackVal->IsString()) { + stack = tns::ToString(isolate, stackVal); + errObj->Set(context, tns::ToV8String(isolate, "stack"), stackVal).FromMaybe(false); + } + } + + // Build the branded payload: the original NSException when x carries one, + // otherwise synthesis info (name/message/stack). + Local payload = Object::New(isolate); + Local nativeExc = GetWrappedNSException(context, x); + if (!nativeExc.IsEmpty()) { + payload->Set(context, tns::ToV8String(isolate, "nativeException"), nativeExc) + .FromMaybe(false); + } else { + std::string name = "Error"; + if (xIsObject) { + Local nameVal; + if (x.As()->Get(context, tns::ToV8String(isolate, "name")).ToLocal(&nameVal) && + nameVal->IsString()) { + name = tns::ToString(isolate, nameVal); + } + } + payload->Set(context, tns::ToV8String(isolate, "name"), tns::ToV8String(isolate, name)) + .FromMaybe(false); + payload + ->Set(context, tns::ToV8String(isolate, "message"), tns::ToV8String(isolate, message)) + .FromMaybe(false); + payload->Set(context, tns::ToV8String(isolate, "stack"), tns::ToV8String(isolate, stack)) + .FromMaybe(false); + } + + if (!brand.IsEmpty()) { + errObj->SetPrivate(context, brand, payload).FromMaybe(false); + } + + info.GetReturnValue().Set(errObj); + }).ToLocal(&func); + + Isolate* isolate = context->GetIsolate(); + tns::Assert(success, isolate); + + success = + interop->Set(context, tns::ToV8String(isolate, "escapeException"), func).FromMaybe(false); + tns::Assert(success, isolate); +} + void Interop::RegisterFreeFunction(Local context, Local interop) { Local func; bool success = v8::Function::New(context, [](const FunctionCallbackInfo& info) { diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index b7c296fd..9cedfd01 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -1,1049 +1,1214 @@ -#include #include "MetadataBuilder.h" -#include "NativeScriptException.h" +#include #include "ArgConverter.h" -#include "ObjectManager.h" -#include "InlineFunctions.h" -#include "SymbolLoader.h" +#include "Caches.h" #include "Constants.h" #include "Helpers.h" +#include "InlineFunctions.h" #include "Interop.h" -#include "Worker.h" -#include "Caches.h" -#include "Tasks.h" +#include "NativeScriptException.h" +#include "ObjectManager.h" #include "Runtime.h" #include "RuntimeConfig.h" +#include "SymbolLoader.h" +#include "Tasks.h" +#include "Worker.h" using namespace v8; namespace tns { -void MetadataBuilder::RegisterConstantsOnGlobalObject(Isolate* isolate, Local globalTemplate, bool isWorkerThread) { - GlobalHandlerContext* handlerContext = new GlobalHandlerContext(isWorkerThread); - Caches::Get(isolate)->registerCacheBoundObject(handlerContext); - Local ext = External::New(isolate, handlerContext); - - NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, nullptr, nullptr, nullptr, nullptr, ext, PropertyHandlerFlags::kNonMasking); - globalTemplate->SetHandler(config); +void MetadataBuilder::RegisterConstantsOnGlobalObject(Isolate* isolate, + Local globalTemplate, + bool isWorkerThread) { + GlobalHandlerContext* handlerContext = new GlobalHandlerContext(isWorkerThread); + Caches::Get(isolate)->registerCacheBoundObject(handlerContext); + Local ext = External::New(isolate, handlerContext); + + NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, nullptr, nullptr, + nullptr, nullptr, ext, + PropertyHandlerFlags::kNonMasking); + globalTemplate->SetHandler(config); } -void MetadataBuilder::GlobalPropertyGetter(Local property, const PropertyCallbackInfo& info) { - Isolate* isolate = info.GetIsolate(); - std::string propName = tns::ToString(isolate, property); - - GlobalHandlerContext* ctx = static_cast(info.Data().As()->Value()); - - if (ctx->isWorkerThread_ && std::find(Worker::GlobalFunctions.begin(), Worker::GlobalFunctions.end(), propName) != Worker::GlobalFunctions.end()) { - return; - } +void MetadataBuilder::GlobalPropertyGetter(Local property, + const PropertyCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + std::string propName = tns::ToString(isolate, property); - if (InlineFunctions::IsGlobalFunction(propName)) { - return; - } + GlobalHandlerContext* ctx = + static_cast(info.Data().As()->Value()); - const Meta* meta = ArgConverter::GetMeta(propName); - if (meta == nullptr || !meta->isAvailable()) { - return; - } + if (ctx->isWorkerThread_ && + std::find(Worker::GlobalFunctions.begin(), Worker::GlobalFunctions.end(), propName) != + Worker::GlobalFunctions.end()) { + return; + } - Local context = isolate->GetCurrentContext(); + if (InlineFunctions::IsGlobalFunction(propName)) { + return; + } - if (meta->type() == MetaType::Interface || meta->type() == MetaType::ProtocolType) { - const BaseClassMeta* classMeta = static_cast(meta); - Class knownClass = meta->type() == MetaType::Interface ? objc_getClass(meta->name()) : nil; - KnownUnknownClassPair pair(knownClass); - MetadataBuilder::GetOrCreateConstructorFunctionTemplate(context, classMeta, pair); - - bool isInterface = meta->type() == MetaType::Interface; - std::string name = meta->name(); - std::shared_ptr cache = Caches::Get(isolate); - if (isInterface) { - auto it = cache->CtorFuncs.find(name); - if (it != cache->CtorFuncs.end()) { - Local func = it->second->Get(isolate); - info.GetReturnValue().Set(func); - } - } else { - auto it = cache->ProtocolCtorFuncs.find(name); - if (it != cache->ProtocolCtorFuncs.end()) { - Local func = it->second->Get(isolate); - info.GetReturnValue().Set(func); - } - } - } else if (meta->type() == MetaType::Function) { - auto cache = Caches::Get(isolate); - std::string funcName = meta->name(); - auto it = cache->CFunctions.find(funcName); - if (it != cache->CFunctions.end()) { - Local func = it->second->Get(isolate); - info.GetReturnValue().Set(func); - return; - } - - const FunctionMeta* funcMeta = static_cast(meta); - void* functionPointer = SymbolLoader::instance().loadFunctionSymbol(meta->topLevelModule(), meta->name()); - if (functionPointer == nullptr) { - Log(@"Unable to load \"%s\" function", meta->name()); - tns::Assert(false, isolate); - } - - CacheItem* item = new CacheItem(funcMeta, std::string(), functionPointer); - Caches::Get(isolate)->registerCacheBoundObject(item); - Local ext = External::New(isolate, item); - Local func; - bool success = v8::Function::New(context, CFunctionCallback, ext).ToLocal(&func); - tns::Assert(success, isolate); + const Meta* meta = ArgConverter::GetMeta(propName); + if (meta == nullptr || !meta->isAvailable()) { + return; + } - tns::SetValue(isolate, func, new FunctionWrapper(funcMeta)); - MetadataBuilder::DefineFunctionLengthProperty(context, funcMeta->encodings(), func); + Local context = isolate->GetCurrentContext(); - auto uniquePersistent = std::make_unique>(isolate, func); - uniquePersistent->SetWrapperClassId(Constants::ClassTypes::DataWrapper); - cache->CFunctions.emplace(funcName, std::move(uniquePersistent)); + if (meta->type() == MetaType::Interface || meta->type() == MetaType::ProtocolType) { + const BaseClassMeta* classMeta = static_cast(meta); + Class knownClass = meta->type() == MetaType::Interface ? objc_getClass(meta->name()) : nil; + KnownUnknownClassPair pair(knownClass); + MetadataBuilder::GetOrCreateConstructorFunctionTemplate(context, classMeta, pair); + bool isInterface = meta->type() == MetaType::Interface; + std::string name = meta->name(); + std::shared_ptr cache = Caches::Get(isolate); + if (isInterface) { + auto it = cache->CtorFuncs.find(name); + if (it != cache->CtorFuncs.end()) { + Local func = it->second->Get(isolate); info.GetReturnValue().Set(func); - } else if (meta->type() == MetaType::Var) { - void* dataSymbol = SymbolLoader::instance().loadDataSymbol(meta->topLevelModule(), meta->name()); - if (!dataSymbol) { - return; - } - - const VarMeta* varMeta = static_cast(meta); - - BaseCall bc((uint8_t*)dataSymbol); - const TypeEncoding* typeEncoding = varMeta->encoding(); - Local result = Interop::GetResult(context, typeEncoding, &bc, true); - info.GetReturnValue().Set(result); - } else if (meta->type() == MetaType::JsCode) { - const JsCodeMeta* jsCodeMeta = static_cast(meta); - std::string jsCode = jsCodeMeta->jsCode(); - Local