| /* |
| * Copyright 2010-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license |
| * that can be found in the LICENSE file. |
| */ |
| |
| #if KONAN_OBJC_INTEROP |
| |
| #import <Foundation/Foundation.h> |
| #import <objc/objc-exception.h> |
| |
| #include <objc/objc.h> |
| #include <objc/runtime.h> |
| #include <objc/message.h> |
| #include <cstdio> |
| #include <cstdint> |
| #include <mutex> |
| #include <string> |
| |
| #include "Memory.h" |
| |
| #include "ManuallyScoped.hpp" |
| #include "Natives.h" |
| #include "ObjCBackRef.hpp" |
| #include "ObjCInterop.h" |
| #include "ObjCExportPrivate.h" |
| #include "ObjCMMAPI.h" |
| #include "StackTrace.hpp" |
| #include "Types.h" |
| #include "concurrent/Mutex.hpp" |
| |
| using namespace kotlin; |
| |
| // Replaced in ObjCExportCodeGenerator. |
| __attribute__((weak)) const char* Kotlin_ObjCInterop_uniquePrefix = nullptr; |
| |
| const char* Kotlin_ObjCInterop_getUniquePrefix() { |
| auto result = Kotlin_ObjCInterop_uniquePrefix; |
| RuntimeCheck(result != nullptr, "unique prefix is not initialized"); |
| return result; |
| } |
| |
| extern "C" id objc_msgSendSuper2(struct objc_super *super, SEL op, ...); |
| |
| // Acts only as container for the method, not actually applied to any class. |
| @protocol HasKotlinObjCClassData |
| @required |
| -(void*)_kotlinObjCClassData; |
| @end |
| |
| static inline struct KotlinObjCClassData* GetKotlinClassData(id objOrClass) { |
| void* ptr = [(id<HasKotlinObjCClassData>)objOrClass _kotlinObjCClassData]; |
| return static_cast<struct KotlinObjCClassData*>(ptr); |
| } |
| |
| RUNTIME_NOTHROW bool IsKotlinObjCClass(Class cls) { |
| // `CreateKotlinObjCClass` adds this selector to both class and meta-class using `AddKotlinClassData`. |
| return [cls instancesRespondToSelector:@selector(_kotlinObjCClassData)]; |
| } |
| |
| namespace { |
| |
| using BackRef = ManuallyScoped<mm::ObjCBackRef>; |
| |
| BackRef& getBackRef(id obj, KotlinObjCClassData* classData) { |
| void* body = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(obj) + classData->bodyOffset); |
| return *reinterpret_cast<ManuallyScoped<mm::ObjCBackRef>*>(body); |
| } |
| |
| BackRef& getBackRef(id obj) { |
| // TODO: suboptimal; consider specializing methods for each class. |
| auto* classData = GetKotlinClassData(obj); |
| return getBackRef(obj, classData); |
| } |
| |
| OBJ_GETTER(toKotlinImp, id self, SEL _cmd) { |
| RETURN_OBJ(getBackRef(self)->ref()); |
| } |
| |
| id allocWithZoneImp(Class self, SEL _cmd, void* zone) { |
| // [super allocWithZone:zone] |
| auto* classData = GetKotlinClassData(self); // TODO: suboptimal; consider specializing. |
| struct objc_super s = {(id)self, object_getClass(classData->objcClass)}; |
| auto messenger = reinterpret_cast<id (*) (struct objc_super*, SEL _cmd, void* zone)>(objc_msgSendSuper2); |
| id result = messenger(&s, _cmd, zone); |
| |
| auto* typeInfo = classData->typeInfo; |
| |
| kotlin::CalledFromNativeGuard guard; |
| ObjHolder holder; |
| auto kotlinObj = AllocInstanceWithAssociatedObject(typeInfo, result, holder.slot()); |
| |
| getBackRef(result, classData).construct(kotlinObj); |
| |
| return result; |
| } |
| |
| id retainImp(id self, SEL _cmd) { |
| getBackRef(self)->retain(); |
| return self; |
| } |
| |
| BOOL _tryRetainImp(id self, SEL _cmd) { |
| return getBackRef(self)->tryRetain(); |
| } |
| |
| void releaseImp(id self, SEL _cmd) { |
| getBackRef(self)->release(); |
| } |
| |
| void releaseAsAssociatedObjectImp(id self, SEL _cmd) { |
| // No need for any special handling. Weak reference handling machinery |
| // has already cleaned up the reference to Kotlin object. |
| |
| // [super release] |
| auto* classData = GetKotlinClassData(self); |
| Class clazz = classData->objcClass; |
| struct objc_super s = {self, clazz}; |
| objc_msgSendSuper2(&s, @selector(release)); // FIXME is this correct? |
| } |
| |
| void deallocImp(id self, SEL _cmd) { |
| getBackRef(self).destroy(); |
| |
| // [super dealloc] |
| auto* classData = GetKotlinClassData(self); |
| Class clazz = classData->objcClass; |
| struct objc_super s = {self, clazz}; |
| objc_msgSendSuper2(&s, @selector(dealloc)); // FIXME is this correct? |
| } |
| |
| } |
| |
| extern "C" { |
| |
| Class Kotlin_Interop_getObjCClass(const char* name) { |
| Class result = objc_lookUpClass(name); |
| RuntimeCheck(result != nil, "Objective-C class '%s' not found. Ensure that the containing framework or library was linked.", name); |
| return result; |
| } |
| |
| RUNTIME_NOTHROW const TypeInfo* GetObjCKotlinTypeInfo(ObjHeader* obj) { |
| void* objcPtr = obj->GetAssociatedObject(); |
| RuntimeAssert(objcPtr != nullptr, ""); |
| return GetKotlinClassData(reinterpret_cast<id>(objcPtr))->typeInfo; |
| } |
| |
| RUNTIME_NOTHROW bool IsInstanceOfKotlinClassImplementingObjCProtocol(ObjHeader* kotlinObj, id obj, const char* protocolName) { |
| // Same as `IsKotlinObjCClass`, but more efficient: |
| if ((kotlinObj->type_info()->flags_ & TF_KOTLIN_OBJC_CLASS) == 0) { |
| // Not an instance of an `IsKotlinObjCClass`-class. The contract requires returning false. |
| return false; |
| } |
| |
| /* |
| The implementation below intentionally avoids getting a `Protocol*` by name with `objc_getProtocol` |
| and compares the names instead. |
| |
| The reason: `CreateKotlinObjCClass`, when creating the obj's class, |
| looks up protocols by name and doesn't mark the class as adopting the protocol when can't find it. |
| But this can legitimately happen when the protocol has neither properly adopting Objective-C classes |
| nor @protocol references, because the Objective-C compiler creates `__OBJC_PROTOCOL` data on demand. |
| No data => no protocol by name at runtime => no adoption. |
| But this function makes the best effort to keep the type checking behaviour working properly: |
| if the Kotlin class is defined as implementing the protocol, the type check should return `true`. |
| |
| So, the implementation below checks `KotlinObjCClassInfo.protocolNames` which contains all the implemented protocols |
| as defined in the source code, regardless of whether they can be found with `objc_getProtocol` at runtime or not. |
| |
| Reminder: a Kotlin class can't implement an Objective-C protocol unless it subclasses an Objective-C class. |
| Also, Kotlin subclasses of Objective-C classes (= `IsKotlinObjCClass`) must be final. |
| */ |
| auto* classData = GetKotlinClassData(obj); |
| auto* info = classData->classInfo; |
| for (size_t i = 0;; ++i) { |
| const char* name = info->protocolNames[i]; |
| if (name == nullptr) break; |
| // Note: the check below might wrongly fail if the protocol has the `objc_runtime_name` attribute. See KT-82296. |
| if (strcmp(protocolName, name) == 0) return true; |
| } |
| return false; |
| } |
| |
| static void AddNSObjectOverride(bool isClassMethod, Class clazz, SEL selector, void* imp) { |
| Class nsObjectClass = Kotlin_Interop_getObjCClass("NSObject"); |
| |
| Method nsObjectMethod = class_getInstanceMethod( |
| isClassMethod ? object_getClass((id)nsObjectClass) : nsObjectClass, selector); |
| RuntimeCheck(nsObjectMethod != nullptr, "NSObject method not found"); |
| |
| const char* nsObjectMethodTypeEncoding = method_getTypeEncoding(nsObjectMethod); |
| RuntimeCheck(nsObjectMethodTypeEncoding != nullptr, "NSObject method has no encoding provided"); |
| |
| // TODO: something of the above can be cached. |
| |
| BOOL added = class_addMethod( |
| isClassMethod ? object_getClass((id)clazz) : clazz, selector, (IMP)imp, nsObjectMethodTypeEncoding); |
| RuntimeCheck(added, "Unable to add method to Objective-C class"); |
| } |
| |
| static void AddKotlinClassData(bool isClassMethod, Class clazz, void* imp) { |
| SEL selector = @selector(_kotlinObjCClassData); |
| |
| auto methodDescription = protocol_getMethodDescription( |
| @protocol(HasKotlinObjCClassData), |
| selector, |
| YES, YES |
| ); |
| |
| const char* typeEncoding = methodDescription.types; |
| |
| RuntimeCheck(typeEncoding != nullptr, "unable to find method in Objective-C protocol"); |
| |
| BOOL added = class_addMethod( |
| isClassMethod ? object_getClass((id)clazz) : clazz, selector, (IMP)imp, typeEncoding); |
| RuntimeCheck(added, "Unable to add method to Objective-C class"); |
| } |
| |
| static void AddMethods(Class clazz, const struct ObjCMethodDescription* methods, int32_t methodsNum) { |
| for (int32_t i = 0; i < methodsNum; ++i) { |
| const struct ObjCMethodDescription* method = &methods[i]; |
| BOOL added = class_addMethod(clazz, sel_registerName(method->selector), (IMP)method->imp, method->encoding); |
| RuntimeAssert(added == YES, "Unable to add method to Objective-C class"); |
| } |
| } |
| |
| static kotlin::ThreadStateAware<kotlin::SpinLock> classCreationMutex; |
| static int anonymousClassNextId = 0; |
| |
| NO_EXTERNAL_CALLS_CHECK static Class allocateClass(const KotlinObjCClassInfo* info) { |
| Class superclass = Kotlin_Interop_getObjCClass(info->superclassName); |
| |
| if (info->exported) { |
| RuntimeCheck(info->name != nullptr, "exported Objective-C class must have a name"); |
| Class result = objc_allocateClassPair(superclass, info->name, 0); |
| if (result != nullptr) return result; |
| // Similar to how Objective-C runtime handles this: |
| fprintf(stderr, "Class %s has multiple implementations. Which one will be used is undefined.\n", info->name); |
| } |
| |
| std::string className = Kotlin_ObjCInterop_getUniquePrefix(); |
| |
| if (info->name != nullptr) { |
| className += info->name; |
| } else { |
| className += "_kobjc"; |
| } |
| |
| int classId = anonymousClassNextId++; |
| className += std::to_string(classId); |
| |
| Class result = objc_allocateClassPair(superclass, className.c_str(), 0); |
| RuntimeCheck(result != nullptr, "Failed to allocate Objective-C class"); |
| return result; |
| } |
| |
| void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) { |
| std::lock_guard lockGuard(classCreationMutex); |
| |
| void* createdClass = *info->createdClass; |
| if (createdClass != nullptr) { |
| return createdClass; |
| } |
| |
| kotlin::NativeOrUnregisteredThreadGuard threadStateGuard(/* reentrant = */ true); |
| |
| Class newClass = allocateClass(info); |
| |
| RuntimeAssert(newClass != nullptr, "Failed to allocate Objective-C class"); |
| |
| Class newMetaclass = object_getClass(reinterpret_cast<id>(newClass)); |
| |
| for (size_t i = 0;; ++i) { |
| const char* protocolName = info->protocolNames[i]; |
| if (protocolName == nullptr) break; |
| Protocol* proto = objc_getProtocol(protocolName); |
| if (proto != nullptr) { |
| BOOL added = class_addProtocol(newClass, proto); |
| RuntimeAssert(added == YES, "Unable to add protocol to Objective-C class"); |
| added = class_addProtocol(newMetaclass, proto); |
| RuntimeAssert(added == YES, "Unable to add protocol to Objective-C metaclass"); |
| } |
| } |
| |
| AddNSObjectOverride(false, newClass, Kotlin_ObjCExport_toKotlinSelector, (void*)&toKotlinImp); |
| AddNSObjectOverride(true, newClass, @selector(allocWithZone:), (void*)&allocWithZoneImp); |
| AddNSObjectOverride(false, newClass, @selector(retain), (void*)&retainImp); |
| AddNSObjectOverride(false, newClass, @selector(_tryRetain), (void*)&_tryRetainImp); |
| AddNSObjectOverride(false, newClass, @selector(release), (void*)&releaseImp); |
| AddNSObjectOverride(false, newClass, Kotlin_ObjCExport_releaseAsAssociatedObjectSelector, |
| (void*)&releaseAsAssociatedObjectImp); |
| AddNSObjectOverride(false, newClass, @selector(dealloc), (void*)&deallocImp); |
| |
| AddMethods(newClass, info->instanceMethods, info->instanceMethodsNum); |
| AddMethods(newMetaclass, info->classMethods, info->classMethodsNum); |
| |
| // Adding both instance and class methods to make [GetKotlinClassData] work |
| // for instances as well as the class itself. |
| AddKotlinClassData(false, newClass, (void*)info->classDataImp); |
| AddKotlinClassData(true, newClass, (void*)info->classDataImp); |
| |
| int bodySize = sizeof(BackRef); |
| char bodyTypeEncoding[16]; |
| snprintf(bodyTypeEncoding, sizeof(bodyTypeEncoding), "[%dc]", bodySize); |
| BOOL added = class_addIvar(newClass, "kotlinBody", bodySize, /* log2(align) = */ 3, bodyTypeEncoding); |
| RuntimeAssert(added == YES, "Unable to add ivar to Objective-C class"); |
| |
| objc_registerClassPair(newClass); |
| |
| Ivar body = class_getInstanceVariable(newClass, "kotlinBody"); |
| RuntimeAssert(body != nullptr, "Unable to get ivar added to Objective-C class"); |
| int32_t offset = (int32_t)ivar_getOffset(body); |
| *info->bodyOffset = offset; |
| |
| // `GetKotlinClassData` below calls an Objective-C method on the generated class -- `_kotlinObjCClassData`. |
| // |
| // `Kotlin_ObjCExport_createTypeInfoWithKotlinFieldsFrom` calls a method as well -- `instancesRespondToSelector:`, |
| // as part of `IsKotlinObjCClass`. It also queries a lot of details from the class using Objective-C runtime APIs. |
| // |
| // Doing this to a dynamically created Objective-C class is not safe until it is registered with |
| // `objc_registerClassPair`. See e.g. KT-82669. |
| // |
| // That's why it is important to keep those two calls after `objc_registerClassPair`. |
| auto* classData = GetKotlinClassData(newClass); |
| const TypeInfo* actualTypeInfo = Kotlin_ObjCExport_createTypeInfoWithKotlinFieldsFrom(newClass, info->typeInfo); |
| |
| classData->typeInfo = actualTypeInfo; |
| classData->objcClass = newClass; |
| classData->bodyOffset = offset; |
| |
| *info->createdClass = newClass; |
| return newClass; |
| } |
| |
| void* objc_autoreleasePoolPush(); |
| void objc_autoreleasePoolPop(void* ptr); |
| id objc_allocWithZone(Class clazz); |
| id objc_retain(id ptr); |
| void objc_release(id ptr); |
| id objc_retainAutoreleaseReturnValue(id ptr); |
| |
| void* Kotlin_objc_autoreleasePoolPush() { |
| return objc_autoreleasePoolPush(); |
| } |
| |
| void Kotlin_objc_autoreleasePoolPop(void* ptr) { |
| kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); |
| objc_autoreleasePoolPop(ptr); |
| } |
| |
| id Kotlin_objc_allocWithZone(Class clazz) { |
| kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative); |
| return objc_allocWithZone(clazz); |
| } |
| |
| id Kotlin_objc_retain_inNative(id ptr) { |
| NativeOrUnregisteredThreadGuard guard(/*reentrant=*/ true); |
| return objc_retain(ptr); |
| } |
| |
| id Kotlin_objc_retainBlock_inNative(id ptr) { |
| NativeOrUnregisteredThreadGuard guard(/*reentrant=*/ true); |
| return objc_retainBlock(ptr); |
| } |
| |
| id Kotlin_objc_retainAutoreleaseReturnValue_inNative(id ptr) { |
| NativeOrUnregisteredThreadGuard guard(/*reentrant=*/ true); |
| return objc_retainAutoreleaseReturnValue(ptr); |
| } |
| |
| void Kotlin_objc_release_inNative(id ptr) { |
| NativeOrUnregisteredThreadGuard guard(/*reentrant=*/ true); |
| objc_release(ptr); |
| } |
| |
| void* Kotlin_Block_copy(void* blockPtr) { |
| return _Block_copy(blockPtr); |
| } |
| |
| void Kotlin_objc_detachObjCObject(KRef ref) { |
| id associatedObject = GetAssociatedObject(ref); |
| while (true) { |
| if (associatedObject == nullptr) break; |
| id actualAssociatedObject = AtomicCompareAndSwapAssociatedObject(ref, associatedObject, nullptr); |
| if (actualAssociatedObject == associatedObject) { |
| Kotlin_ObjCExport_releaseAssociatedObject(associatedObject); |
| break; |
| } |
| associatedObject = actualAssociatedObject; |
| } |
| } |
| |
| } // extern "C" |
| |
| #else // KONAN_OBJC_INTEROP |
| |
| #include "KAssert.h" |
| |
| extern "C" { |
| |
| void* Kotlin_objc_autoreleasePoolPush() { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void Kotlin_objc_autoreleasePoolPop(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| } |
| |
| void* Kotlin_objc_allocWithZone(void* clazz) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void* Kotlin_objc_retain_inNative(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void* Kotlin_objc_retainBlock_inNative(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void* Kotlin_objc_retainAutoreleaseReturnValue_inNative(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void Kotlin_objc_release_inNative(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| } |
| |
| void* Kotlin_Block_copy(void* ptr) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| return nullptr; |
| } |
| |
| void Kotlin_objc_detachObjCObject(void* ref) { |
| RuntimeAssert(false, "Objective-C interop is disabled"); |
| } |
| |
| } // extern "C" |
| |
| #endif // KONAN_OBJC_INTEROP |