From 85b9200bc22bd67b4df83a4e97230c7c55e0c25e Mon Sep 17 00:00:00 2001 From: Svyatoslav Scherbina Date: Tue, 4 Feb 2020 13:20:52 +0300 Subject: [PATCH] Rework `@Throws` It now enables propagation only for instances of listed classes. --- OBJC_INTEROP.md | 13 +++-- .../objcexport/ObjCExportCodeGenerator.kt | 51 +++++++++++++++++-- .../objcexport/ObjCExportHeaderGenerator.kt | 16 ------ runtime/src/main/cpp/ObjCExportErrors.mm | 50 +++++++++--------- .../main/kotlin/kotlin/native/Annotations.kt | 13 +++-- .../kotlin/native/internal/ObjCExportUtils.kt | 4 -- 6 files changed, 92 insertions(+), 55 deletions(-) diff --git a/OBJC_INTEROP.md b/OBJC_INTEROP.md index dbf653d989c..ae482ce4cff 100644 --- a/OBJC_INTEROP.md +++ b/OBJC_INTEROP.md @@ -129,9 +129,16 @@ player.moveTo(UP, byInches = 42) Kotlin has no concept of checked exceptions, all Kotlin exceptions are unchecked. Swift has only checked errors. So if Swift or Objective-C code calls a Kotlin method which throws an exception to be handled, then the Kotlin method should be marked -with a `@Throws` annotation. In this case all Kotlin exceptions -(except for instances of `Error`, `RuntimeException` and subclasses) are translated into -a Swift error/`NSError`. +with a `@Throws` annotation specifying a list of "expected" exception classes. + +When compiling to Objective-C/Swift framework, functions having or inheriting +`@Throws` annotation are represented as `NSError*`-producing methods in Objective-C +and as `throws` methods in Swift. + +When Kotlin function called from Swift/Objective-C code throws an exception +which is an instance of one of the `@Throws`-specified classes or their subclasses, +it is propagated as `NSError`. Other Kotlin exceptions reaching Swift/Objective-C +are considered unhandled and cause program termination. Note that the opposite reversed translation is not implemented yet: Swift/Objective-C error-throwing methods aren't imported to Kotlin as diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt index a443b45e106..b6ec717e503 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt @@ -22,6 +22,8 @@ import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.konan.CompiledKlibModuleOrigin import org.jetbrains.kotlin.descriptors.konan.CurrentKlibModuleOrigin import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.IrClassReference +import org.jetbrains.kotlin.ir.expressions.IrVararg import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.* @@ -375,6 +377,9 @@ internal class ObjCExportCodeGenerator( internal val directMethodAdapters = mutableMapOf() + internal val exceptionTypeInfoArrays = mutableMapOf() + internal val typeInfoArrays = mutableMapOf, ConstPointer>() + inner class ObjCToKotlinMethodAdapter( selector: String, encoding: String, @@ -702,12 +707,17 @@ private fun ObjCExportCodeGenerator.generateAbstractObjCImp(methodBridge: Method private fun ObjCExportCodeGenerator.generateObjCImp( target: IrFunction?, + baseMethod: IrFunction, methodBridge: MethodBridge, isVirtual: Boolean = false ) = if (target == null) { generateAbstractObjCImp(methodBridge) } else { - generateObjCImp(methodBridge, isDirect = !isVirtual) { args, resultLifetime, exceptionHandler -> + generateObjCImp( + methodBridge, + isDirect = !isVirtual, + baseMethod = baseMethod + ) { args, resultLifetime, exceptionHandler -> val llvmTarget = if (!isVirtual) { codegen.llvmFunction(target) } else { @@ -721,6 +731,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp( private fun ObjCExportCodeGenerator.generateObjCImp( methodBridge: MethodBridge, isDirect: Boolean, + baseMethod: IrFunction? = null, callKotlin: FunctionGenerationContext.( args: List, resultLifetime: Lifetime, @@ -783,7 +794,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp( kotlinExceptionHandler { exception -> callFromBridge( context.llvm.Kotlin_ObjCExport_RethrowExceptionAsNSError, - listOf(exception, errorOutPtr!!) + listOf(exception, errorOutPtr!!, generateExceptionTypeInfoArray(baseMethod!!)) ) val returnValue = when (returnType) { @@ -830,6 +841,38 @@ private fun ObjCExportCodeGenerator.generateObjCImp( ret(genReturnValueOnSuccess(returnType)) } +private fun ObjCExportCodeGenerator.generateExceptionTypeInfoArray(baseMethod: IrFunction): LLVMValueRef = + exceptionTypeInfoArrays.getOrPut(baseMethod) { + val types = computesThrowsClasses(baseMethod) + generateTypeInfoArray(types.toSet()) + }.llvm + +private fun ObjCExportCodeGenerator.generateTypeInfoArray(types: Set): ConstPointer = + typeInfoArrays.getOrPut(types) { + val typeInfos = types.map { with(codegen) { it.typeInfoPtr } } + NullPointer(codegen.kTypeInfo) + codegen.staticData.placeGlobalConstArray("", codegen.kTypeInfoPtr, typeInfos) + } + +private fun computesThrowsClasses(method: IrFunction): List { + // Note: frontend ensures that all topmost overridden methods have equal @Throws annotations. + // However due to linking different versions of libraries IR could end up not meeting this condition. + // Handling this gracefully below. + + if (method is IrSimpleFunction && method.overriddenSymbols.isNotEmpty()) { + return computesThrowsClasses(method.overriddenSymbols.first().owner) + } + + val throwsVararg = method.annotations.findAnnotation(KonanFqNames.throws)?.getValueArgument(0) + ?: return emptyList() + + if (throwsVararg !is IrVararg) error(method.getContainingFile(), throwsVararg, "unexpected vararg") + + return throwsVararg.elements.map { + (it as? IrClassReference)?.symbol?.owner as? IrClass + ?: error(method.getContainingFile(), it, "unexpected @Throws argument") + } +} + private fun ObjCExportCodeGenerator.generateObjCImpForArrayConstructor( target: IrConstructor, methodBridge: MethodBridge @@ -1016,7 +1059,7 @@ private fun ObjCExportCodeGenerator.createMethodVirtualAdapter( val selector = namer.getSelector(baseMethod.descriptor) val methodBridge = mapper.bridgeMethod(baseMethod.descriptor) - val objCToKotlin = constPointer(generateObjCImp(baseMethod, methodBridge, isVirtual = true)) + val objCToKotlin = constPointer(generateObjCImp(baseMethod, baseMethod, methodBridge, isVirtual = true)) selectorsToDefine[selector] = methodBridge @@ -1042,7 +1085,7 @@ private fun ObjCExportCodeGenerator.createMethodAdapter( val selectorName = namer.getSelector(request.base.descriptor) val methodBridge = mapper.bridgeMethod(request.base.descriptor) val objCEncoding = getEncoding(methodBridge) - val objCToKotlin = constPointer(generateObjCImp(request.implementation, methodBridge)) + val objCToKotlin = constPointer(generateObjCImp(request.implementation, request.base, methodBridge)) selectorsToDefine[selectorName] = methodBridge diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt index e158d188c92..4e1e14d6dde 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt @@ -643,29 +643,13 @@ internal class ObjCExportTranslatorImpl( } } - private val uncheckedExceptionClasses = - listOf("kotlin.Error", "kotlin.RuntimeException").map { ClassId.topLevel(FqName(it)) } - private fun exportThrown(method: FunctionDescriptor) { if (!method.kind.isReal) return val throwsAnnotation = method.annotations.findAnnotation(KonanFqNames.throws) ?: return - if (!mapper.doesThrow(method)) { - warningCollector.reportWarning(method, - "@${KonanFqNames.throws.shortName()} annotation should also be added to a base method") - } - val arguments = (throwsAnnotation.allValueArguments.values.single() as ArrayValue).value for (argument in arguments) { val classDescriptor = TypeUtils.getClassDescriptor((argument as KClassValue).getArgumentType(method.module)) ?: continue - - classDescriptor.getAllSuperClassifiers().firstOrNull { it.classId in uncheckedExceptionClasses }?.let { - warningCollector.reportWarning(method, - "Method is declared to throw ${classDescriptor.fqNameSafe}, " + - "but instances of ${it.fqNameSafe} and its subclasses aren't propagated " + - "from Kotlin to Objective-C/Swift") - } - generator?.requireClassOrInterface(classDescriptor) } } diff --git a/runtime/src/main/cpp/ObjCExportErrors.mm b/runtime/src/main/cpp/ObjCExportErrors.mm index 1a9968cd8bc..489203dfe98 100644 --- a/runtime/src/main/cpp/ObjCExportErrors.mm +++ b/runtime/src/main/cpp/ObjCExportErrors.mm @@ -28,33 +28,44 @@ extern "C" OBJ_GETTER(Kotlin_Throwable_getMessage, KRef throwable); extern "C" OBJ_GETTER(Kotlin_ObjCExport_getWrappedError, KRef throwable); -extern "C" KBoolean Kotlin_ObjCExport_isUnchecked(KRef exception); static void printlnMessage(const char* message) { konan::consolePrintf("%s\n", message); } -static const char* uncheckedExceptionMessage = - "Instances of kotlin.Error, kotlin.RuntimeException and subclasses " - "aren't propagated from Kotlin to Objective-C/Swift."; - extern "C" RUNTIME_NORETURN void Kotlin_ObjCExport_trapOnUndeclaredException(KRef exception) { - if (Kotlin_ObjCExport_isUnchecked(exception)) { - printlnMessage(uncheckedExceptionMessage); - printlnMessage("Other exceptions can be propagated as NSError if method has or inherits @Throws annotation."); - } else { - printlnMessage("Exceptions are propagated from Kotlin to Objective-C/Swift as NSError " - "only if method has or inherits @Throws annotation"); - } + printlnMessage("Function doesn't have or inherit @Throws annotation and thus exception isn't propagated " + "from Kotlin to Objective-C/Swift as NSError.\n" + "It is considered unexpected and unhandled instead. Program will be terminated."); TerminateWithUnhandledException(exception); } static char kotlinExceptionOriginChar; -extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* outError) { - if (Kotlin_ObjCExport_isUnchecked(exception)) { - printlnMessage(uncheckedExceptionMessage); +static bool isExceptionOfType(KRef exception, const TypeInfo** types) { + for (int i = 0; types[i] != nullptr; ++i) { + // TODO: use fast instance check when possible. + if (IsInstance(exception, types[i])) return true; + } + + return false; +} + +extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* outError, const TypeInfo** types) { + ObjHolder errorHolder; + KRef error = Kotlin_ObjCExport_getWrappedError(exception, errorHolder.slot()); + if (error != nullptr) { + // Thrown originally by Swift/Objective-C. + // Not actually a Kotlin exception, so don't check if it matches [types]. + if (outError != nullptr) *outError = Kotlin_ObjCExport_refToObjC(error); + return; + } + + if (!isExceptionOfType(exception, types)) { + printlnMessage("Exception doesn't match @Throws-specified class list and thus isn't propagated " + "from Kotlin to Objective-C/Swift as NSError.\n" + "It is considered unexpected and unhandled instead. Program will be terminated."); TerminateWithUnhandledException(exception); } @@ -62,18 +73,11 @@ extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* return; } - ObjHolder errorHolder, messageHolder; - - KRef error = Kotlin_ObjCExport_getWrappedError(exception, errorHolder.slot()); - if (error != nullptr) { - *outError = Kotlin_ObjCExport_refToObjC(error); - return; - } - NSMutableDictionary* userInfo = [[NSMutableDictionary new] autorelease]; userInfo[@"KotlinException"] = Kotlin_ObjCExport_refToObjC(exception); userInfo[@"KotlinExceptionOrigin"] = @(&kotlinExceptionOriginChar); // Support for different Kotlin runtimes loaded. + ObjHolder messageHolder; KRef message = Kotlin_Throwable_getMessage(exception, messageHolder.slot()); NSString* description = Kotlin_Interop_CreateNSStringFromKString(message); if (description != nullptr) { diff --git a/runtime/src/main/kotlin/kotlin/native/Annotations.kt b/runtime/src/main/kotlin/kotlin/native/Annotations.kt index 92acf28dccb..77f4985e03c 100644 --- a/runtime/src/main/kotlin/kotlin/native/Annotations.kt +++ b/runtime/src/main/kotlin/kotlin/native/Annotations.kt @@ -36,11 +36,14 @@ public annotation class RetainForTarget(val target: String) /** * This annotation indicates what exceptions should be declared by a function when compiled to a platform method. * - * When compiling to Objective-C/Swift framework, methods having or inheriting this annotation are represented as - * `NSError*`-producing methods in Objective-C and as `throws` methods in Swift. - * When such a method called through framework API throws an exception, it is either propagated as - * `NSError` or considered unhandled (if exception `is` [kotlin.Error] or [kotlin.RuntimeException]). - * In any case exception is not checked to be instance of one of the [exceptionClasses]. + * When compiling to Objective-C/Swift framework, functions having or inheriting + * this annotation are represented as `NSError*`-producing methods in Objective-C + * and as `throws` methods in Swift. + * + * When Kotlin function called from Swift/Objective-C code throws an exception + * which is an instance of one of the [exceptionClasses] or their subclasses, + * it is propagated as `NSError`. Other Kotlin exceptions reaching Swift/Objective-C + * are considered unhandled and cause program termination. * * @property exceptionClasses the list of checked exception classes that may be thrown by the function. */ diff --git a/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt index f102045ef6f..6e58474f0e0 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt @@ -280,10 +280,6 @@ class ObjCErrorException( override fun toString(): String = "NSError-based exception: $message" } -@ExportForCppRuntime -private fun Kotlin_ObjCExport_isUnchecked(exception: Throwable): Boolean = - exception is kotlin.Error || exception is kotlin.RuntimeException - @PublishedApi @SymbolName("Kotlin_ObjCExport_trapOnUndeclaredException") @ExportForCppRuntime