From d5cbfd5420776329a3c096fabaf15b5198467e26 Mon Sep 17 00:00:00 2001 From: Svyatoslav Scherbina Date: Wed, 20 May 2020 16:54:41 +0300 Subject: [PATCH] Support implicit `@Throws(CancellationException::class)` in ObjCExport --- .../kotlin/backend/konan/KonanFqNames.kt | 1 + .../jetbrains/kotlin/backend/konan/ir/Ir.kt | 2 + .../objcexport/ObjCExportCodeGenerator.kt | 27 ++++++++------ .../objcexport/ObjCExportHeaderGenerator.kt | 37 +++++++++++++++---- backend.native/tests/objcexport/coroutines.kt | 26 ++++++++++++- .../tests/objcexport/coroutines.swift | 34 +++++++++++++++++ .../tests/objcexport/expectedLazy.h | 15 ++++++++ 7 files changed, 121 insertions(+), 21 deletions(-) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt index 038dfce4097..6f5c8f20f98 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt @@ -22,6 +22,7 @@ object KonanFqNames { val nonNullNativePtr = internalPackageName.child(Name.identifier(NON_NULL_NATIVE_PTR_NAME)).toUnsafe() val Vector128 = packageName.child(Name.identifier(VECTOR128)) val throws = FqName("kotlin.Throws") + val cancellationException = FqName("kotlin.coroutines.cancellation.CancellationException") val threadLocal = FqName("kotlin.native.concurrent.ThreadLocal") val sharedImmutable = FqName("kotlin.native.concurrent.SharedImmutable") val frozen = FqName("kotlin.native.internal.Frozen") diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt index ec5c3b61500..a90e21449ac 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt @@ -409,6 +409,8 @@ internal class KonanSymbols( .filterNot { it.isExpect }.single().getter!! ) + val cancellationException = topLevelClass(KonanFqNames.cancellationException) + val kotlinResult = topLevelClass("kotlin.Result") val kotlinResultGetOrThrow = symbolTable.referenceSimpleFunction( 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 d739e11ef1e..bfe0c88a02c 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 @@ -9,10 +9,7 @@ import llvm.* import org.jetbrains.kotlin.backend.common.ir.simpleFunctions import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.descriptors.* -import org.jetbrains.kotlin.backend.konan.ir.allParameters -import org.jetbrains.kotlin.backend.konan.ir.isOverridable -import org.jetbrains.kotlin.backend.konan.ir.isUnit -import org.jetbrains.kotlin.backend.konan.ir.llvmSymbolOrigin +import org.jetbrains.kotlin.backend.konan.ir.* import org.jetbrains.kotlin.backend.konan.llvm.* import org.jetbrains.kotlin.backend.konan.llvm.objc.ObjCCodeGenerator import org.jetbrains.kotlin.backend.konan.llvm.objc.ObjCDataGenerator @@ -890,7 +887,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp( private fun ObjCExportCodeGenerator.generateExceptionTypeInfoArray(baseMethod: IrFunction): LLVMValueRef = exceptionTypeInfoArrays.getOrPut(baseMethod) { - val types = computesThrowsClasses(baseMethod) + val types = effectiveThrowsClasses(baseMethod, symbols) generateTypeInfoArray(types.toSet()) }.llvm @@ -900,16 +897,22 @@ private fun ObjCExportCodeGenerator.generateTypeInfoArray(types: Set): 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. - +private fun effectiveThrowsClasses(method: IrFunction, symbols: KonanSymbols): List { if (method is IrSimpleFunction && method.overriddenSymbols.isNotEmpty()) { - return computesThrowsClasses(method.overriddenSymbols.first().owner) + return effectiveThrowsClasses(method.overriddenSymbols.first().owner, symbols) } - val throwsVararg = method.annotations.findAnnotation(KonanFqNames.throws)?.getValueArgument(0) + val throwsAnnotation = method.annotations.findAnnotation(KonanFqNames.throws) + ?: return if (method.isSuspend) { + listOf(symbols.cancellationException.owner) + } else { + // 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 missing annotation gracefully: + emptyList() + } + + val throwsVararg = throwsAnnotation.getValueArgument(0) ?: return emptyList() if (throwsVararg !is IrVararg) error(method.getContainingFile(), throwsVararg, "unexpected vararg") 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 450ab348685..7266d9c756b 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 @@ -652,16 +652,37 @@ internal class ObjCExportTranslatorImpl( } private fun exportThrown(method: FunctionDescriptor) { - if (!method.kind.isReal) return - val throwsAnnotation = method.annotations.findAnnotation(KonanFqNames.throws) ?: return - - val arguments = (throwsAnnotation.allValueArguments.values.single() as ArrayValue).value - for (argument in arguments) { - val classDescriptor = TypeUtils.getClassDescriptor((argument as KClassValue).getArgumentType(method.module)) ?: continue - generator?.requireClassOrInterface(classDescriptor) - } + getDefinedThrows(method) + ?.mapNotNull { method.module.findClassAcrossModuleDependencies(it) } + ?.forEach { generator?.requireClassOrInterface(it) } } + private fun getDefinedThrows(method: FunctionDescriptor): Sequence? { + if (!method.kind.isReal) return null + + val throwsAnnotation = method.annotations.findAnnotation(KonanFqNames.throws) + + if (throwsAnnotation != null) { + val argumentsArrayValue = throwsAnnotation.firstArgument() as? ArrayValue + return argumentsArrayValue?.value?.asSequence().orEmpty() + .filterIsInstance() + .mapNotNull { + when (val value = it.value) { + is KClassValue.Value.NormalClass -> value.classId + is KClassValue.Value.LocalClass -> null + } + } + } + + if (method.isSuspend && method.overriddenDescriptors.isEmpty()) { + return implicitSuspendThrows + } + + return null + } + + private val implicitSuspendThrows = sequenceOf(ClassId.topLevel(KonanFqNames.cancellationException)) + private fun mapReturnType(returnBridge: MethodBridge.ReturnValue, method: FunctionDescriptor, objCExportScope: ObjCExportScope): ObjCType = when (returnBridge) { MethodBridge.ReturnValue.Suspend, MethodBridge.ReturnValue.Void -> ObjCVoidType diff --git a/backend.native/tests/objcexport/coroutines.kt b/backend.native/tests/objcexport/coroutines.kt index 0e7b291c23a..ca20f682b02 100644 --- a/backend.native/tests/objcexport/coroutines.kt +++ b/backend.native/tests/objcexport/coroutines.kt @@ -139,4 +139,28 @@ fun callSuspendBridge(bridge: AbstractSuspendBridge, resultHolder: ResultHolder< callSuspendBridgeImpl(bridge) callAbstractSuspendBridgeImpl(bridge) }.startCoroutine(ResultHolderCompletion(resultHolder)) -} \ No newline at end of file +} + +suspend fun throwCancellationException(): Unit { + val exception = CancellationException("coroutine is cancelled") + + // Note: frontend checker hardcodes fq names of CancellationException super classes (see NativeThrowsChecker). + // This is our best effort to keep that list in sync with actual stdlib code: + assertTrue(exception is kotlin.Throwable) + assertTrue(exception is kotlin.Exception) + assertTrue(exception is kotlin.RuntimeException) + assertTrue(exception is kotlin.IllegalStateException) + assertTrue(exception is kotlin.coroutines.cancellation.CancellationException) + + throw exception +} + +abstract class ThrowCancellationException { + internal abstract suspend fun throwCancellationException() +} + +class ThrowCancellationExceptionImpl : ThrowCancellationException() { + public override suspend fun throwCancellationException() { + throw CancellationException() + } +} diff --git a/backend.native/tests/objcexport/coroutines.swift b/backend.native/tests/objcexport/coroutines.swift index 41762f677b7..8ca42a3eecb 100644 --- a/backend.native/tests/objcexport/coroutines.swift +++ b/backend.native/tests/objcexport/coroutines.swift @@ -221,6 +221,38 @@ private func testBridges() throws { try assertSame(actual: resultHolder.result, expected: KotlinUnit()) } +private func testImplicitThrows1() throws { + var result: KotlinUnit? = nil + var error: Error? = nil + var completionCalled = 0 + + CoroutinesKt.throwCancellationException { _result, _error in + completionCalled += 1 + result = _result + error = _error + } + + try assertEquals(actual: completionCalled, expected: 1) + try assertNil(result) + try assertTrue(error?.kotlinException is KotlinCancellationException) +} + +private func testImplicitThrows2() throws { + var result: KotlinUnit? = nil + var error: Error? = nil + var completionCalled = 0 + + ThrowCancellationExceptionImpl().throwCancellationException { _result, _error in + completionCalled += 1 + result = _result + error = _error + } + + try assertEquals(actual: completionCalled, expected: 1) + try assertNil(result) + try assertTrue(error?.kotlinException is KotlinCancellationException) +} + class CoroutinesTests : SimpleTestProvider { override init() { super.init() @@ -229,5 +261,7 @@ class CoroutinesTests : SimpleTestProvider { test("TestCall", testCall) test("TestOverride", testOverride) test("TestBridges", testBridges) + test("TestImplicitThrows1", testImplicitThrows1) + test("TestImplicitThrows2", testImplicitThrows2) } } \ No newline at end of file diff --git a/backend.native/tests/objcexport/expectedLazy.h b/backend.native/tests/objcexport/expectedLazy.h index 5cbcfc4a04e..a9a3528029c 100644 --- a/backend.native/tests/objcexport/expectedLazy.h +++ b/backend.native/tests/objcexport/expectedLazy.h @@ -179,6 +179,20 @@ __attribute__((swift_name("AbstractSuspendBridge"))) - (void)nothingAsUnitValue:(KtInt *)value completionHandler:(void (^)(KtKotlinNothing * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsUnit(value:completionHandler:)"))); @end; +__attribute__((swift_name("ThrowCancellationException"))) +@interface KtThrowCancellationException : KtBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ThrowCancellationExceptionImpl"))) +@interface KtThrowCancellationExceptionImpl : KtThrowCancellationException +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (void)throwCancellationExceptionWithCompletionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("throwCancellationException(completionHandler:)"))); +@end; + __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("CoroutinesKt"))) @interface KtCoroutinesKt : KtBase @@ -189,6 +203,7 @@ __attribute__((swift_name("CoroutinesKt"))) + (void)callSuspendFunSuspendFun:(id)suspendFun doYield:(BOOL)doYield doThrow:(BOOL)doThrow resultHolder:(KtResultHolder *)resultHolder __attribute__((swift_name("callSuspendFun(suspendFun:doYield:doThrow:resultHolder:)"))); + (void)callSuspendFun2SuspendFun:(id)suspendFun doYield:(BOOL)doYield doThrow:(BOOL)doThrow completionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("callSuspendFun2(suspendFun:doYield:doThrow:completionHandler:)"))); + (BOOL)callSuspendBridgeBridge:(KtAbstractSuspendBridge *)bridge resultHolder:(KtResultHolder *)resultHolder error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("callSuspendBridge(bridge:resultHolder:)"))); ++ (void)throwCancellationExceptionWithCompletionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("throwCancellationException(completionHandler:)"))); @end; __attribute__((swift_name("GH4002ArgumentBase")))