diff --git a/NativeScript/runtime/ClassBuilder.h b/NativeScript/runtime/ClassBuilder.h index 087bd266..0a485706 100644 --- a/NativeScript/runtime/ClassBuilder.h +++ b/NativeScript/runtime/ClassBuilder.h @@ -1,6 +1,9 @@ #ifndef ClassBuilder_h #define ClassBuilder_h +#include +#include + #include "Common.h" #include "Metadata.h" @@ -28,6 +31,16 @@ class ClassBuilder { static void RegisterBaseTypeScriptExtendsFunction(v8::Local context); static void RegisterNativeTypeScriptExtendsFunction(v8::Local context); static std::string GetTypeEncoding(const TypeEncoding* typeEncoding, int argsCount); + + // Lazily registers an Objective-C subclass for a plain ES `class X extends NativeBase {}` + // constructor function. Returns the registered class, or nil when ctorFunc is not part of a + // native inheritance chain (or the chain goes through a legacy `.extend()`-created class). + // Idempotent: subsequent calls return the cached class from the ctor's ObjCClassWrapper. + static Class EnsureExtendedClass(v8::Local context, v8::Local ctorFunc); + + // Resolves the Objective-C class that should be instantiated for a construct call, honoring + // `new.target` so that plain ES subclasses of native classes get their own registered class. + static Class ResolveConstructedClass(v8::Local context, v8::Local newTarget, Class fallback); private: static std::atomic classNameCounter_; @@ -35,7 +48,8 @@ class ClassBuilder { static void SuperAccessorGetterCallback(v8::Local name, const v8::PropertyCallbackInfo& info); static void ExtendedClassConstructorCallback(const v8::FunctionCallbackInfo& info); - static void ExposeDynamicMethods(v8::Local context, Class extendedClass, v8::Local exposedMethods, v8::Local exposedProtocols, v8::Local implementationObject); + static void SwizzleRetainRelease(v8::Isolate* isolate, Class extendedClass); + static void ExposeDynamicMethods(v8::Local context, Class extendedClass, v8::Local exposedMethods, v8::Local exposedProtocols, v8::Local implementationObject, std::unordered_set* visitedNames = nullptr); static void ExposeDynamicMembers(v8::Local context, Class extendedClass, v8::Local implementationObject, v8::Local nativeSignature); static void VisitMethods(Class extendedClass, std::string methodName, const BaseClassMeta* meta, std::vector& methodMetas, std::vector exposedProtocols); static void VisitProperties(std::string propertyName, const BaseClassMeta* meta, std::vector& propertyMetas, std::vector exposedProtocols); diff --git a/NativeScript/runtime/ClassBuilder.mm b/NativeScript/runtime/ClassBuilder.mm index 49c9176d..2c938856 100644 --- a/NativeScript/runtime/ClassBuilder.mm +++ b/NativeScript/runtime/ClassBuilder.mm @@ -141,7 +141,7 @@ try { CacheItem* item = static_cast(info.Data().As()->Value()); - Class klass = item->data_; + Class klass = ClassBuilder::ResolveConstructedClass(context, info.NewTarget(), item->data_); ArgConverter::ConstructObject(context, info, klass); } catch (NativeScriptException& ex) { @@ -295,104 +295,7 @@ class_addMethod(object_getClass(extendedClass), @selector(initialize), newInitialize, "v@:"); - /// We swizzle the retain and release methods for the following reason: - /// When we instantiate a native class via a JavaScript call we add it to the object - /// instances map thus incrementing the retainCount by 1. Then, when the native object is - /// referenced somewhere else its count will become more than 1. Since we want to keep the - /// corresponding JavaScript object alive even if it is not used anywhere, we call GcProtect - /// on it. Whenever the native object is released so that its retainCount is 1 (the object - /// instances map), we unprotect the corresponding JavaScript object in order to make both - /// of them destroyable/GC-able. When the JavaScript object is GC-ed we release the native - /// counterpart as well. - void (*retain)(id, SEL) = - (void (*)(id, SEL))FindNotOverridenMethod(extendedClass, @selector(retain)); - IMP newRetain = imp_implementationWithBlock(^(id self) { - if (!isolateWrapper.IsValid()) { - return retain(self, @selector(retain)); - } - if ([self retainCount] == 1) { - auto runtime = Runtime::GetRuntime(isolate); - auto runtimeLoop = runtime->RuntimeLoop(); - void* weakSelf = (__bridge void*)self; - auto gcProtect = ^() { - auto innerCache = isolateWrapper.GetCache(); - auto it = innerCache->Instances.find((id)weakSelf); - if (it != innerCache->Instances.end()) { - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - Local value = it->second->Get(isolate); - BaseDataWrapper* wrapper = tns::GetValue(isolate, value); - if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { - ObjCDataWrapper* objcWrapper = static_cast(wrapper); - objcWrapper->GcProtect(); - } - } - }; - if (CFRunLoopGetCurrent() != runtimeLoop) { - tns::ExecuteOnRunLoop(runtimeLoop, gcProtect); - } else { - gcProtect(); - } - } - - return retain(self, @selector(retain)); - }); - class_addMethod(extendedClass, @selector(retain), newRetain, "@@:"); - - void (*release)(id, SEL) = - (void (*)(id, SEL))FindNotOverridenMethod(extendedClass, @selector(release)); - IMP newRelease = imp_implementationWithBlock(^(id self) { - if (!isolateWrapper.IsValid()) { - release(self, @selector(release)); - return; - } - - if ([self retainCount] == 2) { - void* weakSelf = (__bridge void*)self; - auto gcUnprotect = ^() { - auto innerCache = isolateWrapper.GetCache(); - auto it = innerCache->Instances.find((id)weakSelf); - if (it != innerCache->Instances.end()) { - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - if (it->second != nullptr) { - Local value = it->second->Get(isolate); - BaseDataWrapper* wrapper = tns::GetValue(isolate, value); - if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { - ObjCDataWrapper* objcWrapper = static_cast(wrapper); - objcWrapper->GcUnprotect(); - } - } - } - }; - auto runtime = Runtime::GetRuntime(isolate); - auto runtimeLoop = runtime->RuntimeLoop(); - if (CFRunLoopGetCurrent() != runtimeLoop) { - tns::ExecuteOnRunLoop(runtimeLoop, gcUnprotect); - } else { - auto innerCache = isolateWrapper.GetCache(); - auto it = innerCache->Instances.find(self); - if (it != innerCache->Instances.end()) { - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - if (it->second != nullptr) { - Local value = it->second->Get(isolate); - BaseDataWrapper* wrapper = tns::GetValue(isolate, value); - if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { - ObjCDataWrapper* objcWrapper = static_cast(wrapper); - objcWrapper->GcUnprotect(); - } - } - } - } - } - - release(self, @selector(release)); - }); - class_addMethod(extendedClass, @selector(release), newRelease, "v@:"); + ClassBuilder::SwizzleRetainRelease(isolate, extendedClass); info.GetReturnValue().SetUndefined(); }).ToLocalChecked(); @@ -404,6 +307,247 @@ tns::Assert(success, isolate); } +void ClassBuilder::SwizzleRetainRelease(Isolate* isolate, Class extendedClass) { + IsolateWrapper isolateWrapper(isolate); + + /// We swizzle the retain and release methods for the following reason: + /// When we instantiate a native class via a JavaScript call we add it to the object + /// instances map thus incrementing the retainCount by 1. Then, when the native object is + /// referenced somewhere else its count will become more than 1. Since we want to keep the + /// corresponding JavaScript object alive even if it is not used anywhere, we call GcProtect + /// on it. Whenever the native object is released so that its retainCount is 1 (the object + /// instances map), we unprotect the corresponding JavaScript object in order to make both + /// of them destroyable/GC-able. When the JavaScript object is GC-ed we release the native + /// counterpart as well. + void (*retain)(id, SEL) = + (void (*)(id, SEL))FindNotOverridenMethod(extendedClass, @selector(retain)); + IMP newRetain = imp_implementationWithBlock(^(id self) { + if (!isolateWrapper.IsValid()) { + return retain(self, @selector(retain)); + } + if ([self retainCount] == 1) { + auto runtime = Runtime::GetRuntime(isolate); + auto runtimeLoop = runtime->RuntimeLoop(); + void* weakSelf = (__bridge void*)self; + auto gcProtect = ^() { + auto innerCache = isolateWrapper.GetCache(); + auto it = innerCache->Instances.find((id)weakSelf); + if (it != innerCache->Instances.end()) { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + Local value = it->second->Get(isolate); + BaseDataWrapper* wrapper = tns::GetValue(isolate, value); + if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { + ObjCDataWrapper* objcWrapper = static_cast(wrapper); + objcWrapper->GcProtect(); + } + } + }; + if (CFRunLoopGetCurrent() != runtimeLoop) { + tns::ExecuteOnRunLoop(runtimeLoop, gcProtect); + } else { + gcProtect(); + } + } + + return retain(self, @selector(retain)); + }); + class_addMethod(extendedClass, @selector(retain), newRetain, "@@:"); + + void (*release)(id, SEL) = + (void (*)(id, SEL))FindNotOverridenMethod(extendedClass, @selector(release)); + IMP newRelease = imp_implementationWithBlock(^(id self) { + if (!isolateWrapper.IsValid()) { + release(self, @selector(release)); + return; + } + + if ([self retainCount] == 2) { + void* weakSelf = (__bridge void*)self; + auto gcUnprotect = ^() { + auto innerCache = isolateWrapper.GetCache(); + auto it = innerCache->Instances.find((id)weakSelf); + if (it != innerCache->Instances.end()) { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + if (it->second != nullptr) { + Local value = it->second->Get(isolate); + BaseDataWrapper* wrapper = tns::GetValue(isolate, value); + if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { + ObjCDataWrapper* objcWrapper = static_cast(wrapper); + objcWrapper->GcUnprotect(); + } + } + } + }; + auto runtime = Runtime::GetRuntime(isolate); + auto runtimeLoop = runtime->RuntimeLoop(); + if (CFRunLoopGetCurrent() != runtimeLoop) { + tns::ExecuteOnRunLoop(runtimeLoop, gcUnprotect); + } else { + auto innerCache = isolateWrapper.GetCache(); + auto it = innerCache->Instances.find(self); + if (it != innerCache->Instances.end()) { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + if (it->second != nullptr) { + Local value = it->second->Get(isolate); + BaseDataWrapper* wrapper = tns::GetValue(isolate, value); + if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCObject) { + ObjCDataWrapper* objcWrapper = static_cast(wrapper); + objcWrapper->GcUnprotect(); + } + } + } + } + } + + release(self, @selector(release)); + }); + class_addMethod(extendedClass, @selector(release), newRelease, "v@:"); +} + +Class ClassBuilder::EnsureExtendedClass(Local context, Local ctorFunc) { + Isolate* isolate = context->GetIsolate(); + + // Already registered (or a native/extended constructor that carries a class wrapper) + BaseDataWrapper* existingWrapper = tns::GetValue(isolate, ctorFunc); + if (existingWrapper != nullptr) { + if (existingWrapper->Type() == WrapperType::ObjCClass) { + return static_cast(existingWrapper)->Klass(); + } + return nil; + } + + // Walk the constructor prototype chain (mirrors the `class X extends Y` chain) and collect + // every plain (unregistered) ES constructor level until we reach a constructor holding an + // ObjCClassWrapper. ES-registered ancestors are flattened into this registration; legacy + // `.extend()`-created ancestors are not supported (mirrors the historic restriction) and + // make this function bail out so callers preserve their old behavior. + std::vector> chainCtors; + Local current = ctorFunc; + Class baseClass = nil; + while (true) { + chainCtors.push_back(current); + + Local parentValue = current->GetPrototype(); + if (parentValue.IsEmpty() || !parentValue->IsObject() || !parentValue->IsFunction()) { + return nil; + } + + Local parent = parentValue.As(); + BaseDataWrapper* parentWrapper = tns::GetValue(isolate, parent); + if (parentWrapper == nullptr) { + current = parent; + continue; + } + + if (parentWrapper->Type() != WrapperType::ObjCClass) { + return nil; + } + + ObjCClassWrapper* parentClassWrapper = static_cast(parentWrapper); + if (!parentClassWrapper->ExtendedClass()) { + baseClass = parentClassWrapper->Klass(); + break; + } + + if (parentClassWrapper->ESDerivedClass()) { + // Flatten: the parent's registered class sits directly under the pure native base, so + // keep walking (collecting the parent's prototype for scanning) until we reach it. + current = parent; + continue; + } + + // Legacy `.extend()`-created base - not supported for ES class chaining. + return nil; + } + + if (baseClass == nil) { + return nil; + } + + auto cache = Caches::Get(isolate); + auto isolateId = cache->getIsolateId(); + + std::string baseClassName = class_getName(baseClass); + std::string className = tns::ToString(isolate, ctorFunc->GetName()); + + Class extendedClass = ClassBuilder::GetExtendedClass(baseClassName, className, + std::to_string(isolateId) + "_"); + class_addProtocol(extendedClass, @protocol(TNSDerivedClass)); + class_addProtocol(object_getClass(extendedClass), @protocol(TNSDerivedClass)); + + // Expose members level by level, most-derived first, so JS shadowing semantics carry over to + // the installed Objective-C implementations. Statics like ObjCProtocols/ObjCExposedMethods are + // read through the constructor (inheriting through the static chain like class statics do). + std::unordered_set visitedNames; + for (Local levelCtor : chainCtors) { + Local prototypeValue; + bool success = levelCtor->Get(context, tns::ToV8String(isolate, "prototype")) + .ToLocal(&prototypeValue); + tns::Assert(success && !prototypeValue.IsEmpty() && prototypeValue->IsObject(), isolate); + Local implementationObject = prototypeValue.As(); + + Local exposedMethods; + success = levelCtor->Get(context, tns::ToV8String(isolate, "ObjCExposedMethods")) + .ToLocal(&exposedMethods); + tns::Assert(success, isolate); + + Local exposedProtocols; + success = levelCtor->Get(context, tns::ToV8String(isolate, "ObjCProtocols")) + .ToLocal(&exposedProtocols); + tns::Assert(success, isolate); + + ClassBuilder::ExposeDynamicMethods(context, extendedClass, exposedMethods, exposedProtocols, + implementationObject, &visitedNames); + } + + tns::SetValue(isolate, ctorFunc, new ObjCClassWrapper(extendedClass, true, true)); + + std::string extendedClassName = class_getName(extendedClass); + + auto extendedPersistent = std::make_unique>(isolate, ctorFunc); + extendedPersistent->SetWrapperClassId(Constants::ClassTypes::DataWrapper); + cache->CtorFuncs.emplace(extendedClassName, std::move(extendedPersistent)); + + Local ctorPrototypeValue; + bool success = ctorFunc->Get(context, tns::ToV8String(isolate, "prototype")) + .ToLocal(&ctorPrototypeValue); + tns::Assert(success && !ctorPrototypeValue.IsEmpty() && ctorPrototypeValue->IsObject(), isolate); + cache->ClassPrototypes.emplace(extendedClassName, + std::make_unique>( + isolate, ctorPrototypeValue.As())); + + ClassBuilder::SwizzleRetainRelease(isolate, extendedClass); + + return extendedClass; +} + +Class ClassBuilder::ResolveConstructedClass(Local context, Local newTarget, + Class fallback) { + if (newTarget.IsEmpty() || !newTarget->IsFunction()) { + return fallback; + } + + Isolate* isolate = context->GetIsolate(); + Local newTargetFunc = newTarget.As(); + + BaseDataWrapper* wrapper = tns::GetValue(isolate, newTargetFunc); + if (wrapper != nullptr) { + if (wrapper->Type() == WrapperType::ObjCClass) { + return static_cast(wrapper)->Klass(); + } + return fallback; + } + + Class ensured = ClassBuilder::EnsureExtendedClass(context, newTargetFunc); + return ensured != nil ? ensured : fallback; +} + void ClassBuilder::ExposeDynamicMembers(v8::Local context, Class extendedClass, Local implementationObject, Local nativeSignature) { @@ -578,7 +722,8 @@ void ClassBuilder::ExposeDynamicMethods(Local context, Class extendedClass, Local exposedMethods, Local exposedProtocols, - Local implementationObject) { + Local implementationObject, + std::unordered_set* visitedNames) { Isolate* isolate = context->GetIsolate(); std::vector protocols; if (!exposedProtocols.IsEmpty() && exposedProtocols->IsArray()) { @@ -613,6 +758,13 @@ bool success = methodNames->Get(context, i).ToLocal(&methodName); tns::Assert(success, isolate); + // When flattening a multi-level ES class chain, skip names already exposed by a more + // derived level (JS shadowing semantics) + if (visitedNames != nullptr && + !visitedNames->insert("exposed:" + tns::ToString(isolate, methodName)).second) { + continue; + } + Local methodSignature; success = exposedMethods.As()->Get(context, methodName).ToLocal(&methodSignature); tns::Assert(success && methodSignature->IsObject(), isolate); @@ -700,7 +852,8 @@ implementationObject->Get(context, Symbol::GetIterator(isolate)).ToLocal(&symbolIterator); tns::Assert(success, isolate); - if (!symbolIterator.IsEmpty() && symbolIterator->IsFunction()) { + if (!symbolIterator.IsEmpty() && symbolIterator->IsFunction() && + !class_conformsToProtocol(extendedClass, @protocol(NSFastEnumeration))) { Local symbolIteratorFunc = symbolIterator.As(); class_addProtocol(extendedClass, @protocol(NSFastEnumeration)); @@ -723,7 +876,14 @@ isolate); } - tns::Assert(implementationObject->GetOwnPropertyNames(context).ToLocal(&propertyNames), isolate); + // Use ALL_PROPERTIES so that non-enumerable members are picked up too - methods and accessors + // declared with ES class syntax are non-enumerable, unlike the plain object literals passed to + // the legacy `.extend()` API. + PropertyFilter propertyFilter = + static_cast(PropertyFilter::ALL_PROPERTIES | PropertyFilter::SKIP_SYMBOLS); + tns::Assert( + implementationObject->GetOwnPropertyNames(context, propertyFilter).ToLocal(&propertyNames), + isolate); for (uint32_t i = 0; i < propertyNames->Length(); i++) { Local key; bool success = propertyNames->Get(context, i).ToLocal(&key); @@ -734,6 +894,16 @@ std::string methodName = tns::ToString(isolate, key); + if (methodName == "constructor") { + continue; + } + + // When flattening a multi-level ES class chain, skip names already handled by a more derived + // level (JS shadowing semantics) + if (visitedNames != nullptr && !visitedNames->insert(methodName).second) { + continue; + } + Local propertyDescriptor; tns::Assert(implementationObject->GetOwnPropertyDescriptor(context, key.As()) .ToLocal(&propertyDescriptor), diff --git a/NativeScript/runtime/DataWrapper.h b/NativeScript/runtime/DataWrapper.h index 2625acd0..68d7c9f1 100644 --- a/NativeScript/runtime/DataWrapper.h +++ b/NativeScript/runtime/DataWrapper.h @@ -368,8 +368,8 @@ class ObjCDataWrapper : public BaseDataWrapper { class ObjCClassWrapper : public BaseDataWrapper { public: - ObjCClassWrapper(Class klazz, bool extendedClass = false) - : klass_(klazz), extendedClass_(extendedClass) {} + ObjCClassWrapper(Class klazz, bool extendedClass = false, bool esDerivedClass = false) + : klass_(klazz), extendedClass_(extendedClass), esDerivedClass_(esDerivedClass) {} const WrapperType Type() { return WrapperType::ObjCClass; } @@ -377,9 +377,14 @@ class ObjCClassWrapper : public BaseDataWrapper { bool ExtendedClass() { return this->extendedClass_; } + // true when the class was registered lazily from a plain ES `class X extends NativeBase {}` + // constructor (see ClassBuilder::EnsureExtendedClass) + bool ESDerivedClass() { return this->esDerivedClass_; } + private: Class klass_; bool extendedClass_; + bool esDerivedClass_; }; class ObjCProtocolWrapper : public BaseDataWrapper { diff --git a/NativeScript/runtime/InlineFunctions.cpp b/NativeScript/runtime/InlineFunctions.cpp index 0e748296..ee6bc9ee 100644 --- a/NativeScript/runtime/InlineFunctions.cpp +++ b/NativeScript/runtime/InlineFunctions.cpp @@ -43,6 +43,18 @@ void InlineFunctions::Init(Local context) { " }" " }" " }," + " NativeClass(arg) {" + " if (typeof arg === 'function') {" + " return arg;" + " }" + " var options = arg || {};" + " return function (target) {" + " if (options.protocols && options.protocols.length > 0) {" + " target.ObjCProtocols = (target.ObjCProtocols && target.ObjCProtocols instanceof Array ? target.ObjCProtocols.concat(options.protocols) : options.protocols.slice());" + " }" + " return target;" + " }" + " }," " ObjCMethod() {" " var name = arguments[0];" " var hasName = (name !== undefined && typeof name === \"string\");" @@ -134,6 +146,7 @@ bool InlineFunctions::IsGlobalFunction(std::string name) { name == "NSMakeRange" || name == "__decorate" || name == "__param" || + name == "NativeClass" || name == "ObjCClass" || name == "ObjCMethod" || name == "ObjC" || diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 0097ef5b..8ba62dcc 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -4,6 +4,7 @@ #include "ArgConverter.h" #include "ArrayAdapter.h" #include "Caches.h" +#include "ClassBuilder.h" #include "Constants.h" #include "DictionaryAdapter.h" #include "ExtVector.h" @@ -576,6 +577,15 @@ inline bool isBool() { } else if (argHelper.isObject() && typeEncoding->type == BinaryTypeEncodingType::ClassEncoding) { Local obj = arg.As(); BaseDataWrapper* wrapper = tns::GetValue(isolate, obj); + if (wrapper == nullptr && obj->IsFunction()) { + // A plain ES subclass of a native type passed where a Class is expected + // (e.g. `tableView.registerClassForCellReuseIdentifier(JSClass, ...)`) - lazily + // register its derived class first. + Class ensured = ClassBuilder::EnsureExtendedClass(context, obj.As()); + if (ensured != nil) { + wrapper = tns::GetValue(isolate, obj); + } + } tns::Assert(wrapper != nullptr && wrapper->Type() == WrapperType::ObjCClass, isolate); ObjCClassWrapper* classWrapper = static_cast(wrapper); Class clazz = classWrapper->Klass(); @@ -728,6 +738,14 @@ inline bool isBool() { } } else { Local obj = arg.As(); + if (obj->IsFunction()) { + // A plain ES subclass of a native type passed where an `id` is expected - lazily + // register its derived class and marshal the Objective-C Class object. + Class ensured = ClassBuilder::EnsureExtendedClass(context, obj.As()); + if (ensured != nil) { + return ensured; + } + } DictionaryAdapter* adapter = [[DictionaryAdapter alloc] initWithJSObject:obj isolate:isolate]; // CFAutorelease(adapter); return adapter; diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index b7c296fd..3f46e26f 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -620,6 +620,10 @@ CacheItem* item = static_cast*>(info.Data().As()->Value()); Class klass = objc_getClass(item->meta_->name()); + // Plain ES `class X extends NativeType {}` subclasses reach this callback through + // super(); use new.target to lazily register (and construct) the derived class. + klass = ClassBuilder::ResolveConstructedClass(context, info.NewTarget(), klass); + const InterfaceMeta* interfaceMeta = static_cast(item->meta_); ArgConverter::ConstructObject(context, info, klass, interfaceMeta); } catch (NativeScriptException& ex) { @@ -633,19 +637,24 @@ try { Local thiz = info.This(); - Class klass; + Local context = isolate->GetCurrentContext(); + Class klass = nil; BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz); if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCClass) { ObjCClassWrapper* classWrapper = static_cast(wrapper); klass = classWrapper->Klass(); - } else { + } else if (wrapper == nullptr && thiz->IsFunction()) { + // `JSClass.alloc()` where JSClass is a plain ES subclass of a native type that has + // not been constructed yet - lazily register its derived class first. + klass = ClassBuilder::EnsureExtendedClass(context, thiz.As()); + } + + if (klass == nil) { CacheItem* item = static_cast*>(info.Data().As()->Value()); const InterfaceMeta* meta = item->meta_; klass = objc_getClass(meta->name()); } - - Local context = isolate->GetCurrentContext(); ObjCAllocDataWrapper* allocWrapper = new ObjCAllocDataWrapper(klass); Local result = ArgConverter::CreateJsWrapper(context, allocWrapper, Local()); info.GetReturnValue().Set(result); @@ -663,15 +672,24 @@ std::string className = item->className_; + Local context = isolate->GetCurrentContext(); Local thiz = info.This(); if (thiz->IsFunction()) { - if (BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz)) { + BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz); + if (wrapper != nullptr) { ObjCClassWrapper* classWrapper = static_cast(wrapper); className = class_getName(classWrapper->Klass()); + } else { + // Static call through a plain ES subclass of a native type (inherited static), + // e.g. `JSClass.new()` - lazily register the derived class so the invocation + // dispatches to it. + Class ensured = ClassBuilder::EnsureExtendedClass(context, thiz.As()); + if (ensured != nil) { + className = class_getName(ensured); + } } } - Local context = isolate->GetCurrentContext(); Local result = instanceMethod ? MetadataBuilder::InvokeMethod(context, item->meta_, info.This(), args, className, true) : MetadataBuilder::InvokeMethod(context, item->meta_, Local(), args, className, true); @@ -720,6 +738,31 @@ MetadataBuilder::InvokeMethod(context, item->meta_->setter(), receiver, args, item->className_, true); } +// Resolves the class name a static member access should dispatch to, honoring plain ES +// subclasses of native types used as receivers (e.g. `JSClass.someStaticProperty`). +static std::string ResolveStaticReceiverClassName(Local context, Local receiver, + const std::string& fallback) { + if (receiver.IsEmpty() || !receiver->IsFunction()) { + return fallback; + } + + Isolate* isolate = context->GetIsolate(); + BaseDataWrapper* wrapper = tns::GetValue(isolate, receiver); + if (wrapper != nullptr) { + if (wrapper->Type() == WrapperType::ObjCClass) { + return class_getName(static_cast(wrapper)->Klass()); + } + return fallback; + } + + Class ensured = ClassBuilder::EnsureExtendedClass(context, receiver.As()); + if (ensured != nil) { + return class_getName(ensured); + } + + return fallback; +} + void MetadataBuilder::PropertyNameGetterCallback(Local name, const PropertyCallbackInfo &info) { Isolate* isolate = info.GetIsolate(); CacheItem* item = static_cast*>(info.Data().As()->Value()); @@ -731,7 +774,8 @@ V8EmptyValueArgs args; Local context = isolate->GetCurrentContext(); - Local result = MetadataBuilder::InvokeMethod(context, item->meta_->getter(), Local(), args, item->className_, false); + std::string className = ResolveStaticReceiverClassName(context, info.This(), item->className_); + Local result = MetadataBuilder::InvokeMethod(context, item->meta_->getter(), Local(), args, className, false); if (!result.IsEmpty()) { info.GetReturnValue().Set(result); } @@ -748,7 +792,8 @@ V8SimpleValueArgs args(value); Local context = isolate->GetCurrentContext(); - MetadataBuilder::InvokeMethod(context, item->meta_->setter(), Local(), args, item->className_, false); + std::string className = ResolveStaticReceiverClassName(context, info.This(), item->className_); + MetadataBuilder::InvokeMethod(context, item->meta_->setter(), Local(), args, className, false); } void MetadataBuilder::StructPropertyGetterCallback(Local property, const PropertyCallbackInfo& info) { diff --git a/TestRunner/app/tests/Inheritance/ESClassTests.js b/TestRunner/app/tests/Inheritance/ESClassTests.js new file mode 100644 index 00000000..f0ced74c --- /dev/null +++ b/TestRunner/app/tests/Inheritance/ESClassTests.js @@ -0,0 +1,354 @@ +// Tests for native ES class support: plain `class X extends NativeType {}` without the +// @NativeClass decorator or ES5 downleveling. The Objective-C class is registered lazily by +// the runtime on first use (construction, alloc/new, static dispatch or Class marshalling). +describe(module.id, function () { + afterEach(function () { + TNSClearOutput(); + }); + + it('ESClassLazyRegistration', function () { + class ESLazyObject extends NSObject { + } + + // Defining the class must not register anything with the Objective-C runtime + expect(NSClassFromString('ESLazyObject')).toBeNull(); + + var instance = new ESLazyObject(); + expect(instance instanceof ESLazyObject).toBe(true); + + // First construction registers the class under the ES class name + expect(NSClassFromString('ESLazyObject')).toBe(ESLazyObject); + }); + + it('ESClassSimpleInheritance', function () { + class ESSimpleObject extends TNSDerivedInterface { + } + + var object = new ESSimpleObject(); + expect(object.constructor).toBe(ESSimpleObject); + expect(object instanceof ESSimpleObject).toBe(true); + expect(object instanceof TNSDerivedInterface).toBe(true); + expect(object instanceof NSObject).toBe(true); + expect(object.class()).toBe(ESSimpleObject); + expect(object.superclass).toBe(TNSDerivedInterface); + expect(ESSimpleObject.class()).toBe(ESSimpleObject); + expect(ESSimpleObject.superclass()).toBe(TNSDerivedInterface); + expect(NSStringFromClass(ESSimpleObject)).toBe('ESSimpleObject'); + }); + + it('ESClassConstructorLogicAndFields', function () { + class ESConstructorObject extends NSObject { + field = 42; + + constructor() { + super(); + this.initialized = true; + } + } + + var object = new ESConstructorObject(); + // The receiver created by super() must be the native-backed instance, with class + // fields and constructor logic applied to it + expect(object.field).toBe(42); + expect(object.initialized).toBe(true); + expect(object instanceof ESConstructorObject).toBe(true); + expect(object instanceof NSObject).toBe(true); + expect(NSStringFromClass(object.class())).toBe('ESConstructorObject'); + }); + + it('ESClassSuperArgsSelectInitializer', function () { + class ESCtorArgsObject extends TNSCInterface { + constructor(name, x) { + // Arguments passed to super(...) drive native initializer resolution; + // arguments passed to `new` are only seen by the JS constructor. + super(x); + this.name = name; + } + } + + var object = new ESCtorArgsObject('first', 7); + expect(object instanceof ESCtorArgsObject).toBe(true); + expect(object.name).toBe('first'); + expect(TNSGetOutput()).toBe('initWithPrimitive:7 called'); + TNSClearOutput(); + + class ESCtorTwoArgsObject extends TNSCInterface { + constructor(a, b) { + super(a, b); + } + } + + var object2 = new ESCtorTwoArgsObject(5, 10); + expect(object2 instanceof ESCtorTwoArgsObject).toBe(true); + expect(TNSGetOutput()).toBe('initWithInt:andInt: 5 10 called'); + TNSClearOutput(); + + // super() with no arguments falls back to plain [[Class alloc] init] + class ESCtorNoArgsObject extends TNSCInterface { + constructor() { + super(); + } + } + + var object3 = new ESCtorNoArgsObject(); + expect(object3 instanceof ESCtorNoArgsObject).toBe(true); + expect(TNSGetOutput()).toBe('init called'); + }); + + it('ESClassInstanceMethodsAndSuper', function () { + class ESMethodsObject extends TNSDerivedInterface { + baseMethod() { + TNSLog('js baseMethod called'); + super.baseMethod(); + } + derivedMethod() { + TNSLog('js derivedMethod called'); + super.derivedMethod(); + } + } + + var object = new ESMethodsObject(); + object.baseMethod(); + object.derivedMethod(); + expect(TNSGetOutput()).toBe('js baseMethod called' + + 'instance baseMethod called' + + 'js derivedMethod called' + + 'instance derivedMethod called'); + }); + + it('ESClassPropertyAccessorsAndSuper', function () { + class ESPropertyObject extends TNSDerivedInterface { + get baseProperty() { + TNSLog('js getBaseProperty called'); + return super.baseProperty; + } + set baseProperty(x) { + TNSLog('js setBaseProperty called'); + super.baseProperty = x; + } + } + + var object = new ESPropertyObject(); + object.baseProperty = 0; + UNUSED(object.baseProperty); + expect(TNSGetOutput()).toBe('js setBaseProperty called' + + 'instance setBaseProperty: called' + + 'js getBaseProperty called' + + 'instance baseProperty called'); + }); + + it('ESClassAllocInitBeforeConstruction', function () { + class ESAllocObject extends NSObject { + getAnswer() { + return 42; + } + } + + // alloc().init() without ever calling `new` must register and use the derived class + var object = ESAllocObject.alloc().init(); + expect(object instanceof ESAllocObject).toBe(true); + expect(object.getAnswer()).toBe(42); + expect(NSStringFromClass(object.class())).toBe('ESAllocObject'); + }); + + it('ESClassAllocInitDoesNotRunJsConstructor', function () { + var constructorRuns = 0; + + class ESAllocNoCtorObject extends NSObject { + field = 42; + + constructor() { + super(); + constructorRuns++; + this.initializedFromJs = true; + } + } + + // alloc().init() is purely native initialization: the JS constructor body and + // class field initializers only run through `new`, never through alloc/init. + var allocated = ESAllocNoCtorObject.alloc().init(); + expect(constructorRuns).toBe(0); + expect(allocated.field).toBe(undefined); + expect(allocated.initializedFromJs).toBe(undefined); + expect(allocated instanceof ESAllocNoCtorObject).toBe(true); + expect(NSStringFromClass(allocated.class())).toBe('ESAllocNoCtorObject'); + + var constructed = new ESAllocNoCtorObject(); + expect(constructorRuns).toBe(1); + expect(constructed.field).toBe(42); + expect(constructed.initializedFromJs).toBe(true); + }); + + it('ESClassNewBeforeConstruction', function () { + class ESNewObject extends NSObject { + } + + var object = ESNewObject.new(); + expect(object instanceof ESNewObject).toBe(true); + expect(NSStringFromClass(object.class())).toBe('ESNewObject'); + }); + + it('ESClassStaticMethodDispatch', function () { + class ESStaticMethodObject extends TNSDerivedInterface { + } + + ESStaticMethodObject.baseMethod(); + ESStaticMethodObject.derivedMethod(); + expect(TNSGetOutput()).toBe('static baseMethod called' + + 'static derivedMethod called'); + }); + + it('ESClassStaticPropertyDispatch', function () { + class ESStaticPropertyObject extends TNSDerivedInterface { + } + + ESStaticPropertyObject.baseProperty = 1; + UNUSED(ESStaticPropertyObject.baseProperty); + expect(TNSGetOutput()).toBe('static setBaseProperty: called' + + 'static baseProperty called'); + }); + + it('ESClassPassedAsClassArgument', function () { + class ESClassArgObject extends NSObject { + } + + // Passing the class to a native API before any instance exists must register it + expect(NSStringFromClass(ESClassArgObject)).toBe('ESClassArgObject'); + + var object = new ESClassArgObject(); + expect(object.isKindOfClass(ESClassArgObject)).toBe(true); + expect(object.isMemberOfClass(ESClassArgObject)).toBe(true); + expect(object.isKindOfClass(NSObject)).toBe(true); + }); + + it('ESClassProtocolImplementation', function () { + class ESProtocolObject extends NSObject { + static ObjCProtocols = [TNSBaseProtocol2]; + + baseProtocolMethod1() { + TNSLog('baseProtocolMethod1 called'); + } + baseProtocolMethod2() { + TNSLog('baseProtocolMethod2 called'); + } + } + + var object = ESProtocolObject.alloc().init(); + TNSTestNativeCallbacks.protocolImplementationProtocolInheritance(object); + expect(TNSGetOutput()).toBe('baseProtocolMethod1 called' + + 'baseProtocolMethod2 called'); + }); + + it('ESClassExposedMethods', function () { + class ESExposedObject extends NSObject { + static ObjCExposedMethods = { + 'voidSelector': { returns: interop.types.void }, + 'variadicSelector:x:': { returns: NSObject, params: [NSString, interop.types.int32] } + }; + + voidSelector() { + TNSLog('voidSelector called'); + } + ['variadicSelector:x:'](a, b) { + TNSLog('variadicSelector:' + a + ' x:' + b + ' called'); + return a; + } + } + + var object = new ESExposedObject(); + TNSTestNativeCallbacks.inheritanceVoidSelector(object); + expect(TNSTestNativeCallbacks.inheritanceVariadicSelector(object)).toBe('native'); + expect(TNSGetOutput()).toBe('voidSelector called' + + 'variadicSelector:native x:9 called'); + }); + + it('ESClassDescriptionOverrideFromNative', function () { + class ESDescriptionObject extends NSObject { + get description() { + return 'js description'; + } + } + + // Throws (native assert) if [object description] does not dispatch to the JS getter + TNSTestNativeCallbacks.apiDescriptionOverride(new ESDescriptionObject()); + }); + + it('ESClassMultiLevelInheritance', function () { + class ESLevelA extends TNSDerivedInterface { + baseMethod() { + TNSLog('A baseMethod called'); + super.baseMethod(); + } + derivedMethod() { + TNSLog('A derivedMethod called'); + super.derivedMethod(); + } + } + + class ESLevelB extends ESLevelA { + baseMethod() { + TNSLog('B baseMethod called'); + super.baseMethod(); + } + } + + var b = new ESLevelB(); + expect(b instanceof ESLevelB).toBe(true); + expect(b instanceof ESLevelA).toBe(true); + expect(b instanceof TNSDerivedInterface).toBe(true); + + b.baseMethod(); + b.derivedMethod(); + expect(TNSGetOutput()).toBe('B baseMethod called' + + 'A baseMethod called' + + 'instance baseMethod called' + + 'A derivedMethod called' + + 'instance derivedMethod called'); + TNSClearOutput(); + + // The intermediate class works standalone too, with its own registration + var a = new ESLevelA(); + expect(a instanceof ESLevelA).toBe(true); + expect(a instanceof ESLevelB).toBe(false); + a.baseMethod(); + expect(TNSGetOutput()).toBe('A baseMethod called' + + 'instance baseMethod called'); + }); + + it('ESClassPlainJsSubclassUnaffected', function () { + class PlainBase { + } + class PlainDerived extends PlainBase { + } + + // Classes with no native type in their prototype chain stay plain JS + var object = new PlainDerived(); + expect(object instanceof PlainDerived).toBe(true); + expect(object instanceof PlainBase).toBe(true); + }); + + it('NativeClassGlobalDecoratorNoop', function () { + expect(typeof global.NativeClass).toBe('function'); + + const ESDecoratedPlain = NativeClass(class ESDecoratedPlainObject extends NSObject { + }); + var instance = new ESDecoratedPlain(); + expect(instance instanceof ESDecoratedPlain).toBe(true); + + const ESDecoratedProtocols = NativeClass({ protocols: [TNSBaseProtocol2] })( + class ESDecoratedProtocolsObject extends NSObject { + baseProtocolMethod1() { + TNSLog('baseProtocolMethod1 called'); + } + baseProtocolMethod2() { + TNSLog('baseProtocolMethod2 called'); + } + } + ); + + var object = new ESDecoratedProtocols(); + TNSTestNativeCallbacks.protocolImplementationProtocolInheritance(object); + expect(TNSGetOutput()).toBe('baseProtocolMethod1 called' + + 'baseProtocolMethod2 called'); + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 98e030cb..76823d05 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -62,6 +62,7 @@ require("./Marshalling/ProtocolTests"); require("./Inheritance/InheritanceTests"); require("./Inheritance/ProtocolImplementationTests"); require("./Inheritance/TypeScriptTests"); +require("./Inheritance/ESClassTests"); // require("./MethodCallsTests"); //import "./FunctionsTests";