From d531df1643a5ce9fe7ae205b4b2cbcab7524319d Mon Sep 17 00:00:00 2001 From: Svyatoslav Scherbina Date: Thu, 15 Apr 2021 08:21:30 +0000 Subject: [PATCH] Native: add KotlinThrowable.asError() to the generated Obj-C framework This method can be useful when overriding a throwing Kotlin method in Swift or Obj-C. In this case, a user can call asError to wrap Kotlin exception to (NS)Error and then throw it to Kotlin, which will unwrap it back. ^KT-45127 Fixed --- .../kotlin/builtins/KotlinBuiltIns.java | 4 ++ .../kotlin/backend/konan/llvm/ContextUtils.kt | 1 + .../objcexport/ObjCExportCodeGenerator.kt | 19 +++++++ .../konan/objcexport/ObjCExportCodeSpec.kt | 7 +++ .../objcexport/ObjCExportHeaderGenerator.kt | 16 ++++++ .../konan/objcexport/ObjCExportNamer.kt | 4 ++ .../tests/objcexport/expectedLazy.h | 38 +++++++++++++ .../tests/objcexport/expectedLazyNoGenerics.h | 38 +++++++++++++ .../tests/objcexport/throwableAsError.kt | 38 +++++++++++++ .../tests/objcexport/throwableAsError.swift | 54 +++++++++++++++++++ .../runtime/src/main/cpp/ObjCExportErrors.h | 1 + .../runtime/src/main/cpp/ObjCExportErrors.mm | 4 ++ 12 files changed, 224 insertions(+) create mode 100644 kotlin-native/backend.native/tests/objcexport/throwableAsError.kt create mode 100644 kotlin-native/backend.native/tests/objcexport/throwableAsError.swift diff --git a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java index 03b0b070a70..938e4110703 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java +++ b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java @@ -961,6 +961,10 @@ public abstract class KotlinBuiltIns { return isConstructedFromGivenClass(type, FqNames.throwable); } + public static boolean isThrowable(@NotNull ClassDescriptor descriptor) { + return classFqNameEquals(descriptor, FqNames.throwable.toUnsafe()); + } + public static boolean isKClass(@NotNull ClassDescriptor descriptor) { return classFqNameEquals(descriptor, FqNames.kClass); } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt index 80e54052050..c43cfc95434 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt @@ -497,6 +497,7 @@ internal class Llvm(val context: Context, val llvmModule: LLVMModuleRef) { val Kotlin_ObjCExport_GetAssociatedObject by lazyRtFunction val Kotlin_ObjCExport_AbstractMethodCalled by lazyRtFunction val Kotlin_ObjCExport_RethrowExceptionAsNSError by lazyRtFunction + val Kotlin_ObjCExport_WrapExceptionToNSError by lazyRtFunction val Kotlin_ObjCExport_RethrowNSErrorAsException by lazyRtFunction val Kotlin_ObjCExport_AllocInstanceWithAssociatedObject by lazyRtFunction val Kotlin_ObjCExport_createContinuationArgument by lazyRtFunction diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt index 55a808b2074..9417a4d45c5 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt @@ -1291,6 +1291,9 @@ private fun ObjCExportCodeGenerator.createTypeAdapter( createObjectInstanceAdapter(irClass, it.selector) } } + ObjCKotlinThrowableAsErrorMethod -> { + adapters += createThrowableAsErrorAdapter() + } is ObjCMethodForKotlinMethod -> {} // Handled below. }.let {} // Force exhaustive. } @@ -1576,6 +1579,22 @@ private fun ObjCExportCodeGenerator.createEnumValuesAdapter( return objCToKotlinMethodAdapter(selector, methodBridge, imp) } +private fun ObjCExportCodeGenerator.createThrowableAsErrorAdapter(): ObjCExportCodeGenerator.ObjCToKotlinMethodAdapter { + val methodBridge = MethodBridge( + returnBridge = MethodBridge.ReturnValue.Mapped(ReferenceBridge), + receiver = MethodBridgeReceiver.Instance, + valueParameters = emptyList() + ) + + val imp = generateObjCImpBy(methodBridge) { + val exception = objCReferenceToKotlin(param(0), Lifetime.ARGUMENT) + ret(callFromBridge(context.llvm.Kotlin_ObjCExport_WrapExceptionToNSError, listOf(exception))) + } + + val selector = ObjCExportNamer.kotlinThrowableAsErrorMethodName + return objCToKotlinMethodAdapter(selector, methodBridge, imp) +} + private fun objCFunctionType(context: Context, methodBridge: MethodBridge): LLVMTypeRef { val paramTypes = methodBridge.paramBridges.map { it.objCType } diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt index cd7da97f839..f10ed75390d 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods import org.jetbrains.kotlin.backend.konan.descriptors.enumEntries import org.jetbrains.kotlin.backend.konan.descriptors.isArray import org.jetbrains.kotlin.backend.konan.descriptors.isInterface +import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.ir.symbols.* import org.jetbrains.kotlin.ir.util.SymbolTable @@ -88,6 +89,10 @@ internal fun ObjCExportedInterface.createCodeSpec(symbolTable: SymbolTable): Obj } } + if (KotlinBuiltIns.isThrowable(descriptor)) { + methods += ObjCKotlinThrowableAsErrorMethod + } + val categoryMethods = categoryMembers[descriptor].orEmpty().toObjCMethods() val superClassNotAny = descriptor.getSuperClassNotAny() @@ -149,6 +154,8 @@ internal class ObjCClassMethodForKotlinEnumValues( internal class ObjCGetterForObjectInstance(val selector: String) : ObjCMethodSpec() +internal object ObjCKotlinThrowableAsErrorMethod : ObjCMethodSpec() + internal sealed class ObjCTypeSpec(val binaryName: String) internal sealed class ObjCTypeForKotlinType( diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt index 2bb9181669d..5bdff481b25 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt @@ -386,6 +386,10 @@ internal class ObjCExportTranslatorImpl( } translateClassMembers(descriptor, genericExportScope) + + if (KotlinBuiltIns.isThrowable(descriptor)) { + add { buildThrowableAsErrorMethod() } + } } val attributes = if (descriptor.isFinalOrEnum) listOf(OBJC_SUBCLASSING_RESTRICTED) else emptyList() @@ -406,6 +410,18 @@ internal class ObjCExportTranslatorImpl( ) } + private fun buildThrowableAsErrorMethod(): ObjCMethod { + val asError = ObjCExportNamer.kotlinThrowableAsErrorMethodName + return ObjCMethod( + descriptor = null, + isInstanceMethod = true, + returnType = ObjCClassType("NSError"), + selectors = listOf(asError), + parameters = emptyList(), + attributes = listOf(swiftNameAttribute("$asError()")) + ) + } + private fun mapTypeConstructorParameters(descriptor: ClassDescriptor): List { if (objcGenerics) { return descriptor.typeConstructor.parameters.map { diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt index 03c6ca80def..a4e7772474b 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt @@ -66,6 +66,10 @@ interface ObjCExportNamer { val mutableSetName: ClassOrProtocolName val mutableMapName: ClassOrProtocolName val kotlinNumberName: ClassOrProtocolName + + companion object { + internal val kotlinThrowableAsErrorMethodName: String = "asError" + } } fun createNamer(moduleDescriptor: ModuleDescriptor, diff --git a/kotlin-native/backend.native/tests/objcexport/expectedLazy.h b/kotlin-native/backend.native/tests/objcexport/expectedLazy.h index 0de2329837a..a00c0ec344a 100644 --- a/kotlin-native/backend.native/tests/objcexport/expectedLazy.h +++ b/kotlin-native/backend.native/tests/objcexport/expectedLazy.h @@ -805,6 +805,44 @@ __attribute__((swift_name("OverrideMethodsOfAnyKt"))) + (BOOL)testObj:(id)obj other:(id)other swift:(BOOL)swift error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("test(obj:other:swift:)"))); @end; +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ThrowableAsError"))) +@interface KtThrowableAsError : KtKotlinThrowable +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (instancetype)initWithMessage:(NSString * _Nullable)message __attribute__((swift_name("init(message:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +- (instancetype)initWithCause:(KtKotlinThrowable * _Nullable)cause __attribute__((swift_name("init(cause:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +- (instancetype)initWithMessage:(NSString * _Nullable)message cause:(KtKotlinThrowable * _Nullable)cause __attribute__((swift_name("init(message:cause:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +@end; + +__attribute__((swift_name("ThrowsThrowableAsError"))) +@protocol KtThrowsThrowableAsError +@required + +/** + @note This method converts all Kotlin exceptions to errors. +*/ +- (BOOL)throwErrorAndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("throwError()"))); +@end; + +__attribute__((swift_name("ThrowsThrowableAsErrorSuspend"))) +@protocol KtThrowsThrowableAsErrorSuspend +@required + +/** + @note This method converts instances of CancellationException to errors. + Other uncaught Kotlin exceptions are fatal. +*/ +- (void)throwErrorWithCompletionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("throwError(completionHandler:)"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ThrowableAsErrorKt"))) +@interface KtThrowableAsErrorKt : KtBase ++ (KtThrowableAsError * _Nullable)callAndCatchThrowableAsErrorThrowsThrowableAsError:(id)throwsThrowableAsError __attribute__((swift_name("callAndCatchThrowableAsError(throwsThrowableAsError:)"))); ++ (KtThrowableAsError * _Nullable)callAndCatchThrowableAsErrorSuspendThrowsThrowableAsErrorSuspend:(id)throwsThrowableAsErrorSuspend __attribute__((swift_name("callAndCatchThrowableAsErrorSuspend(throwsThrowableAsErrorSuspend:)"))); +@end; + __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("ThrowsEmptyKt"))) @interface KtThrowsEmptyKt : KtBase diff --git a/kotlin-native/backend.native/tests/objcexport/expectedLazyNoGenerics.h b/kotlin-native/backend.native/tests/objcexport/expectedLazyNoGenerics.h index 007c0a12108..a30a2e3d830 100644 --- a/kotlin-native/backend.native/tests/objcexport/expectedLazyNoGenerics.h +++ b/kotlin-native/backend.native/tests/objcexport/expectedLazyNoGenerics.h @@ -778,6 +778,44 @@ __attribute__((swift_name("OverrideMethodsOfAnyKt"))) + (BOOL)testObj:(id)obj other:(id)other swift:(BOOL)swift error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("test(obj:other:swift:)"))); @end; +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ThrowableAsError"))) +@interface KtThrowableAsError : KtKotlinThrowable +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (instancetype)initWithMessage:(NSString * _Nullable)message __attribute__((swift_name("init(message:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +- (instancetype)initWithCause:(KtKotlinThrowable * _Nullable)cause __attribute__((swift_name("init(cause:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +- (instancetype)initWithMessage:(NSString * _Nullable)message cause:(KtKotlinThrowable * _Nullable)cause __attribute__((swift_name("init(message:cause:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); +@end; + +__attribute__((swift_name("ThrowsThrowableAsError"))) +@protocol KtThrowsThrowableAsError +@required + +/** + @note This method converts all Kotlin exceptions to errors. +*/ +- (BOOL)throwErrorAndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("throwError()"))); +@end; + +__attribute__((swift_name("ThrowsThrowableAsErrorSuspend"))) +@protocol KtThrowsThrowableAsErrorSuspend +@required + +/** + @note This method converts instances of CancellationException to errors. + Other uncaught Kotlin exceptions are fatal. +*/ +- (void)throwErrorWithCompletionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("throwError(completionHandler:)"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ThrowableAsErrorKt"))) +@interface KtThrowableAsErrorKt : KtBase ++ (KtThrowableAsError * _Nullable)callAndCatchThrowableAsErrorThrowsThrowableAsError:(id)throwsThrowableAsError __attribute__((swift_name("callAndCatchThrowableAsError(throwsThrowableAsError:)"))); ++ (KtThrowableAsError * _Nullable)callAndCatchThrowableAsErrorSuspendThrowsThrowableAsErrorSuspend:(id)throwsThrowableAsErrorSuspend __attribute__((swift_name("callAndCatchThrowableAsErrorSuspend(throwsThrowableAsErrorSuspend:)"))); +@end; + __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("ThrowsEmptyKt"))) @interface KtThrowsEmptyKt : KtBase diff --git a/kotlin-native/backend.native/tests/objcexport/throwableAsError.kt b/kotlin-native/backend.native/tests/objcexport/throwableAsError.kt new file mode 100644 index 00000000000..beddb8164b1 --- /dev/null +++ b/kotlin-native/backend.native/tests/objcexport/throwableAsError.kt @@ -0,0 +1,38 @@ +package throwableAsError + +import kotlin.coroutines.* + +class ThrowableAsError : Throwable() + +interface ThrowsThrowableAsError { + @Throws(Throwable::class) + fun throwError() +} + +fun callAndCatchThrowableAsError(throwsThrowableAsError: ThrowsThrowableAsError): ThrowableAsError? { + try { + throwsThrowableAsError.throwError() + } catch (e: ThrowableAsError) { + return e + } + + return null +} + +interface ThrowsThrowableAsErrorSuspend { + suspend fun throwError() +} + +fun callAndCatchThrowableAsErrorSuspend(throwsThrowableAsErrorSuspend: ThrowsThrowableAsErrorSuspend): ThrowableAsError? { + var throwable: ThrowableAsError? = null + suspend { + throwsThrowableAsErrorSuspend.throwError() + }.startCoroutine(object : Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) { + throwable = result.exceptionOrNull() as? ThrowableAsError + } + }) + + return throwable +} diff --git a/kotlin-native/backend.native/tests/objcexport/throwableAsError.swift b/kotlin-native/backend.native/tests/objcexport/throwableAsError.swift new file mode 100644 index 00000000000..3536b611d41 --- /dev/null +++ b/kotlin-native/backend.native/tests/objcexport/throwableAsError.swift @@ -0,0 +1,54 @@ +import Kt + +private func testUnwrapsToSame() throws { + let throwable = ThrowableAsError() + try assertSame(actual: throwable.asError().kotlinException as AnyObject, expected: throwable) +} + +private func testCanThrowAndCatch() throws { + let throwable = ThrowableAsError() + let impl = ThrowsThrowableAsErrorImpl(throwable: throwable) + let caught = ThrowableAsErrorKt.callAndCatchThrowableAsError(throwsThrowableAsError: impl) + try assertSame(actual: caught, expected: throwable) +} + +private class ThrowsThrowableAsErrorImpl : ThrowsThrowableAsError { + var throwable: KotlinThrowable + + init(throwable: KotlinThrowable) { + self.throwable = throwable + } + + func throwError() throws { + throw throwable.asError() + } +} + +private func testCanThrowAndCatchSuspend() throws { + let throwable = ThrowableAsError() + let impl = ThrowsThrowableAsErrorSuspendImpl(throwable: throwable) + let caught = ThrowableAsErrorKt.callAndCatchThrowableAsErrorSuspend(throwsThrowableAsErrorSuspend: impl) + try assertSame(actual: caught, expected: throwable) +} + +private class ThrowsThrowableAsErrorSuspendImpl : ThrowsThrowableAsErrorSuspend { + var throwable: KotlinThrowable + + init(throwable: KotlinThrowable) { + self.throwable = throwable + } + + func throwError(completionHandler: @escaping (KotlinUnit?, Error?) -> Void) { + completionHandler(nil, throwable.asError()) + } +} + +class ThrowableAsErrorTests : SimpleTestProvider { + override init() { + super.init() + + test("TestUnwrapsToSame", testUnwrapsToSame) + test("TestCanThrowAndCatch", testCanThrowAndCatch) + test("TestCanThrowAndCatchSuspend", testCanThrowAndCatchSuspend) + } +} diff --git a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.h b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.h index 6c9a0af42cd..85762623439 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.h +++ b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.h @@ -6,4 +6,5 @@ #import "Types.h" extern "C" id Kotlin_ObjCExport_ExceptionAsNSError(KRef exception, const TypeInfo** types); +extern "C" id Kotlin_ObjCExport_WrapExceptionToNSError(KRef exception); extern "C" OBJ_GETTER(Kotlin_ObjCExport_NSErrorAsException, id error); diff --git a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm index b41d4453c57..2d7c364d66b 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm +++ b/kotlin-native/runtime/src/main/cpp/ObjCExportErrors.mm @@ -70,6 +70,10 @@ extern "C" id Kotlin_ObjCExport_ExceptionAsNSError(KRef exception, const TypeInf TerminateWithUnhandledException(exception); } + return Kotlin_ObjCExport_WrapExceptionToNSError(exception); +} + +extern "C" id Kotlin_ObjCExport_WrapExceptionToNSError(KRef exception) { NSMutableDictionary* userInfo = [[NSMutableDictionary new] autorelease]; userInfo[@"KotlinException"] = Kotlin_ObjCExport_refToObjC(exception); userInfo[@"KotlinExceptionOrigin"] = @(&kotlinExceptionOriginChar); // Support for different Kotlin runtimes loaded.