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..b705e1d1 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -4,6 +4,7 @@ #include "DictionaryAdapter.h" #include "Helpers.h" #include "Interop.h" +#include "NSExceptionSupport.h" #include "NativeScriptException.h" #include "ObjectManager.h" #include "Runtime.h" @@ -128,6 +129,161 @@ 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); +} + +// Builds the combined JS stack string attached to an escaped exception via the +// associated object: the origin (propagation) stack of the wrapping error and +// the escape-site stack, each under a labeled section. Empty or duplicate +// sections are omitted; returns nil when nothing is available. +static NSString* BuildCombinedJSStack(const std::string& origin, const std::string& escape) { + NSMutableArray* parts = [NSMutableArray array]; + if (!origin.empty()) { + [parts addObject:[NSString stringWithFormat:@"JS stack:\n%@", tns::ToNSString(origin)]]; + } + if (!escape.empty() && escape != origin) { + [parts addObject:[NSString stringWithFormat:@"Escaped at:\n%@", tns::ToNSString(escape)]]; + } + if (parts.count == 0) { + return nil; + } + return [parts componentsJoinedByString:@"\n\n"]; +} + +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(); + + 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; + }; + + // The escape-site stack always travels with the payload; the origin stack is + // the wrapping error's own `.stack`, carried when present. + std::string escapeStack = readString("escapeStack", ""); + + // 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]]) { + // Attach the JS stack WITHOUT mutating the original exception's identity + // or userInfo — the round-trip contract requires the very same object to + // reach the native @catch. The category accessor surfaces it uniformly. + std::string originStack = readString("stack", ""); + NSString* combined = BuildCombinedJSStack(originStack, escapeStack); + if (combined != nil) { + tns::SetJSStackOnException((NSException*)data, combined); + } + return (NSException*)data; + } + } + } + + // Otherwise synthesize an NSException from the branded name/message/stack. + 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)]; + } + NSMutableDictionary* userInfo = [NSMutableDictionary dictionary]; + if (!stack.empty()) { + userInfo[TNSJavaScriptStackTraceKey] = tns::ToNSString(stack); + } + // Carry the escape-site stack separately only when it adds information beyond + // the origin stack. + if (!escapeStack.empty() && escapeStack != stack) { + userInfo[TNSJavaScriptEscapeStackTraceKey] = tns::ToNSString(escapeStack); + } + NSException* synthesized = [NSException exceptionWithName:tns::ToNSString(name) + reason:reasonText + userInfo:userInfo.count ? userInfo : nil]; + // Attach the combined stack via associated object so the category accessor is + // uniform across synthesized and rethrown-original exceptions. + NSString* combined = BuildCombinedJSStack(stack, escapeStack); + if (combined != nil) { + tns::SetJSStackOnException(synthesized, combined); + } + return synthesized; +} + +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 uncaughtErrorPolicy). 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(); + + // uncaughtErrorPolicy "throw": the report above runs synchronously here (still + // under the boundary's live V8 scopes). Under the "throw" policy it deposited + // the NSException into the per-isolate slot; claim it and hand it back so the + // caller @throws it AFTER closing every V8 scope (same scopes-before-@throw + // discipline as a branded escape) — a native @try/@catch around the boundary + // then catches it, matching Android. Under the default "report" policy nothing + // is deposited and this is nil. + id policyThrow = NativeScriptException::ClaimPendingPolicyThrow(isolate); + if (policyThrow != nil) { + return policyThrow; + } + return nil; +} + void ArgConverter::MethodCallback(ffi_cif* cif, void* retValue, void** argValues, void* userData) { MethodCallbackWrapper* data = static_cast(userData); @@ -138,90 +294,143 @@ 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(); + } + } + } + + // uncaughtErrorPolicy "throw" boundary claim (best-effort). Unlike the + // HandleBoundaryException sites (property accessors, DictionaryAdapter), + // which report the throw SYNCHRONOUSLY within their own frame and can + // therefore claim the deposit here, this path uses tc.ReThrow(): the + // unbranded exception is surfaced to V8 as pending and only reported when it + // reaches an uncaught V8 Invoke boundary UP THE STACK (or is caught in JS + // first). Empirically (verified with instrumentation over the suite, + // including a native-origin NSOperationQueue-invoked block) the message + // listener does NOT run during `tc`'s teardown, so under the "throw" policy + // nothing has been deposited by the time we reach this point and this claim + // is a no-op — those errors are instead handled by the deferred clean-frame + // fallback. The claim is kept as a defensive hook (harmless when the slot is + // empty; identity-checked so it can never steal an unrelated deposit) and to + // mirror the boundary discipline. Under the default "report" policy nothing + // is ever deposited. The NSError-out branch neither reports nor deposits. + // Skip when a branded escape already set pendingThrow. + if (pendingThrow == nil) { + pendingThrow = NativeScriptException::ClaimPendingPolicyThrow(isolate); } - } - 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..148e1efd 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -3,140 +3,205 @@ #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); + + // Internal EventTarget instance backing the global, returned by the generic + // event-primitives bootstrap IIFE (Events::Init). Holds the real listener + // store, so native layers dispatch through it without going through + // overwritable globals. Cleaned up with the other Persistent members when + // Caches is destroyed, before isolate disposal. + std::unique_ptr> GlobalEventTarget = + std::unique_ptr>(nullptr); + + // Phase 2 WHATWG error-events dispatch closures returned by the bootstrap + // IIFE (ErrorEvents::Init). 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/ErrorEvents.cpp b/NativeScript/runtime/ErrorEvents.cpp new file mode 100644 index 00000000..29ef1c2f --- /dev/null +++ b/NativeScript/runtime/ErrorEvents.cpp @@ -0,0 +1,234 @@ +#include "ErrorEvents.h" + +#include "Caches.h" +#include "Helpers.h" +#include "NativeScriptException.h" + +using namespace v8; + +namespace tns { + +// Native function handed to the bootstrap IIFE as `nativeReportFatal(error, +// stackString)`. It runs the terminal tail (shim + fatal log) WITHOUT +// re-dispatching an event: reportError and listener-thrown errors have already +// gone through JS dispatch, so dispatching again here would recurse. +static void NativeReportFatalCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + Local error = + info.Length() > 0 ? info[0] : v8::Undefined(isolate).As(); + std::string stack = info.Length() > 1 ? tns::ToString(isolate, info[1]) : ""; + NativeScriptException::ReportFatalTail(isolate, error, Local(), + stack, ""); +} + +void ErrorEvents::Init(Local context) { + // WHATWG error-events layer, layered on top of the generic event primitives + // installed by Events::Init. Plain (module-free) script, strict inside the + // IIFE, ES5-ish so it never depends on other runtime extensions. The IIFE is + // invoked with two arguments — the internal EventTarget backing the global + // (so native dispatch survives app code overwriting globalThis.dispatchEvent) + // and the native nativeReportFatal(error, stack) function that runs the + // terminal tail — and returns three closures bound to that backing store. + // ErrorEvent/PromiseRejectionEvent subclass the Event captured off globalThis + // at init time, which runs before any user code. + std::string source = R"( + (function (globalTarget, nativeReportFatal) { + "use strict"; + var g = globalThis; + var Event = g.Event; + + function ErrorEvent(type, opts) { + opts = opts || {}; + Event.call(this, type, opts); + this.message = opts.message !== undefined ? String(opts.message) : ""; + this.filename = opts.filename !== undefined ? String(opts.filename) : ""; + this.lineno = opts.lineno !== undefined ? (opts.lineno | 0) : 0; + this.colno = opts.colno !== undefined ? (opts.colno | 0) : 0; + this.error = opts.error !== undefined ? opts.error : null; + } + ErrorEvent.prototype = Object.create(Event.prototype); + ErrorEvent.prototype.constructor = ErrorEvent; + + function PromiseRejectionEvent(type, opts) { + opts = opts || {}; + Event.call(this, type, opts); + this.promise = opts.promise; + this.reason = opts.reason; + } + PromiseRejectionEvent.prototype = Object.create(Event.prototype); + PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent; + + // A listener that throws must not stop other listeners: route the thrown + // value to the native fatal tail instead of ever recursively dispatching + // another `error` event from inside dispatch. + globalTarget._installListenerErrorReporter(function (e) { + try { nativeReportFatal(e, (e && e.stack) || ""); } catch (ignored) {} + }); + + g.reportError = function (e) { + if (arguments.length === 0) { + throw new TypeError("Failed to execute 'reportError': 1 argument required, but only 0 present."); + } + var ev = new ErrorEvent("error", { + message: (e && e.message !== undefined && e.message !== null) ? String(e.message) : String(e), + error: e, + cancelable: true + }); + if (globalTarget.dispatchEvent(ev)) { + nativeReportFatal(e, (e && e.stack) || ""); + } + }; + + g.ErrorEvent = ErrorEvent; + g.PromiseRejectionEvent = PromiseRejectionEvent; + + // Closures called by C++. They never look up globalThis.dispatchEvent, so + // they keep working even if app code overwrites it. + function dispatchErrorEvent(error, message, stack) { + var ev = new ErrorEvent("error", { + message: message !== undefined && message !== null ? String(message) : "", + error: error, + cancelable: true + }); + globalTarget.dispatchEvent(ev); + return ev.defaultPrevented; + } + function dispatchUnhandledRejection(promise, reason) { + var ev = new PromiseRejectionEvent("unhandledrejection", { + promise: promise, + reason: reason, + cancelable: true + }); + globalTarget.dispatchEvent(ev); + return ev.defaultPrevented; + } + function dispatchRejectionHandled(promise, reason) { + var ev = new PromiseRejectionEvent("rejectionhandled", { + promise: promise, + reason: reason, + cancelable: false + }); + globalTarget.dispatchEvent(ev); + } + + return [dispatchErrorEvent, dispatchUnhandledRejection, dispatchRejectionHandled]; + }) + )"; + + Isolate* isolate = context->GetIsolate(); + + auto cache = Caches::Get(isolate); + tns::Assert(cache != nullptr && cache->GlobalEventTarget != nullptr, isolate); + Local globalTarget = cache->GlobalEventTarget->Get(isolate); + + Local