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 8228e420776..7c273aa0b2a 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 @@ -207,6 +207,11 @@ internal class KonanSymbols( NoLookupLocation.FROM_BACKEND ).single()) + val objCExportResumeContinuation = internalFunction("resumeContinuation") + val objCExportResumeContinuationWithException = internalFunction("resumeContinuationWithException") + val objCExportGetCoroutineSuspended = internalFunction("getCoroutineSuspended") + val objCExportInterceptedContinuation = internalFunction("interceptedContinuation") + val getNativeNullPtr = symbolTable.referenceSimpleFunction(context.getNativeNullPtr) val boxCachePredicates = BoxCache.values().associate { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt index 51ab0f1c023..4f7f587a300 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/ContextUtils.kt @@ -488,6 +488,8 @@ internal class Llvm(val context: Context, val llvmModule: LLVMModuleRef) { val Kotlin_ObjCExport_RethrowExceptionAsNSError by lazyRtFunction val Kotlin_ObjCExport_RethrowNSErrorAsException by lazyRtFunction val Kotlin_ObjCExport_AllocInstanceWithAssociatedObject by lazyRtFunction + val Kotlin_ObjCExport_createContinuationArgument by lazyRtFunction + val Kotlin_ObjCExport_resumeContinuation by lazyRtFunction val tlsMode by lazy { when (target) { 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 d7070acab8d..09d9ca52924 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 @@ -86,7 +86,7 @@ internal open class ObjCExportCodeGeneratorBase(codegen: CodeGenerator) : ObjCCo generateBlockToKotlinFunctionConverter(bridge) } - private val blockGenerator = BlockGenerator(this.codegen) + protected val blockGenerator = BlockGenerator(this.codegen) private val functionToBlockConverterCache = mutableMapOf() internal fun kotlinFunctionToBlockConverter(bridge: BlockPointerBridge): LLVMValueRef = @@ -123,6 +123,10 @@ internal class ObjCExportCodeGenerator( val externalGlobalInitializers = mutableMapOf() + internal val continuationToCompletionConverter: LLVMValueRef by lazy { + generateContinuationToCompletionConverter(blockGenerator) + } + fun FunctionGenerationContext.genSendMessage( returnType: LLVMTypeRef, receiver: LLVMValueRef, @@ -584,6 +588,20 @@ private fun ObjCExportCodeGenerator.emitBoxConverter( setObjCExportTypeInfo(boxClass, constPointer(converter)) } +private fun ObjCExportCodeGenerator.generateContinuationToCompletionConverter( + blockGenerator: BlockGenerator +): LLVMValueRef = with(blockGenerator) { + generateWrapKotlinObjectToBlock( + BlockType(numberOfParameters = 2, returnsVoid = true), + convertName = "convertContinuation", + invokeName = "invokeCompletion" + ) { continuation, arguments -> + check(arguments.size == 2) + callFromBridge(context.llvm.Kotlin_ObjCExport_resumeContinuation, listOf(continuation) + arguments) + ret(null) + } +} + private const val maxConvertorsInCache = 33 private fun ObjCExportBlockCodeGenerator.emitFunctionConverters() { @@ -760,6 +778,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp( } var errorOutPtr: LLVMValueRef? = null + var continuation: LLVMValueRef? = null val kotlinArgs = methodBridge.paramBridges.mapIndexedNotNull { index, paramBridge -> val parameter = param(index) @@ -777,17 +796,22 @@ private fun ObjCExportCodeGenerator.generateObjCImp( errorOutPtr = parameter null } + + MethodBridgeValueParameter.SuspendCompletion -> { + callFromBridge( + context.llvm.Kotlin_ObjCExport_createContinuationArgument, + listOf(parameter, generateExceptionTypeInfoArray(baseMethod!!)), + Lifetime.ARGUMENT + ).also { + continuation = it + } + } } } // TODO: consider merging this handler with function cleanup. - val exceptionHandler = if (errorOutPtr == null) { - kotlinExceptionHandler { exception -> - callFromBridge(symbols.objCExportTrapOnUndeclaredException.owner.llvmFunction, listOf(exception)) - unreachable() - } - } else { - kotlinExceptionHandler { exception -> + val exceptionHandler = when { + errorOutPtr != null -> kotlinExceptionHandler { exception -> callFromBridge( context.llvm.Kotlin_ObjCExport_RethrowExceptionAsNSError, listOf(exception, errorOutPtr!!, generateExceptionTypeInfoArray(baseMethod!!)) @@ -810,6 +834,22 @@ private fun ObjCExportCodeGenerator.generateObjCImp( ret(returnValue) } + + continuation != null -> kotlinExceptionHandler { exception -> + // Callee haven't suspended, so it isn't going to call the completion. Call it here: + callFromBridge( + context.ir.symbols.objCExportResumeContinuationWithException.owner.llvmFunction, + listOf(continuation!!, exception) + ) + // Note: completion block could be called directly instead, but this implementation is + // simpler and avoids duplication. + ret(null) + } + + else -> kotlinExceptionHandler { exception -> + callFromBridge(symbols.objCExportTrapOnUndeclaredException.owner.llvmFunction, listOf(exception)) + unreachable() + } } val targetResult = callKotlin(kotlinArgs, Lifetime.ARGUMENT, exceptionHandler) @@ -825,6 +865,23 @@ private fun ObjCExportCodeGenerator.generateObjCImp( is MethodBridge.ReturnValue.WithError.ZeroForError -> genReturnValueOnSuccess(returnBridge.successBridge) MethodBridge.ReturnValue.Instance.InitResult -> param(0) MethodBridge.ReturnValue.Instance.FactoryResult -> kotlinReferenceToObjC(targetResult!!) // provided by [callKotlin] + MethodBridge.ReturnValue.Suspend -> { + val coroutineSuspended = callFromBridge( + codegen.llvmFunction(context.ir.symbols.objCExportGetCoroutineSuspended.owner), + emptyList(), + Lifetime.LOCAL + ) + ifThen(icmpNe(targetResult!!, coroutineSuspended)) { + // Callee haven't suspended, so it isn't going to call the completion. Call it here: + callFromBridge( + context.ir.symbols.objCExportResumeContinuation.owner.llvmFunction, + listOf(continuation!!, targetResult) + ) + // Note: completion block could be called directly instead, but this implementation is + // simpler and avoids duplication. + } + null + } } ret(genReturnValueOnSuccess(returnType)) @@ -928,6 +985,17 @@ private fun ObjCExportCodeGenerator.generateKotlinToObjCBridge( store(kNullInt8Ptr, it) errorOutPtr = it } + + MethodBridgeValueParameter.SuspendCompletion -> { + val continuation = param(irFunction.allParameters.size) // The last argument. + // TODO: consider placing interception into the converter to reduce code size. + val intercepted = callFromBridge( + context.ir.symbols.objCExportInterceptedContinuation.owner.llvmFunction, + listOf(continuation), + Lifetime.ARGUMENT + ) + callFromBridge(continuationToCompletionConverter, listOf(intercepted)) + } } } @@ -985,12 +1053,25 @@ private fun ObjCExportCodeGenerator.generateKotlinToObjCBridge( MethodBridge.ReturnValue.Instance.InitResult, MethodBridge.ReturnValue.Instance.FactoryResult -> error("init or factory method can't have bridge for overriding: $baseMethod") + + MethodBridge.ReturnValue.Suspend -> { + // Objective-C implementation of Kotlin suspend function is always responsible + // for calling the completion, so in Kotlin coroutines machinery terms it suspends, + // which is indicated by the return value: + callFromBridge( + context.ir.symbols.objCExportGetCoroutineSuspended.owner.llvmFunction, + emptyList(), + Lifetime.RETURN_VALUE + ) + } } val baseReturnType = baseIrFunction.returnType val actualReturnType = irFunction.returnType val retVal = when { + baseIrFunction.isSuspend -> genKotlinBaseMethodResult(Lifetime.RETURN_VALUE, methodBridge.returnBridge) + actualReturnType.isUnit() || actualReturnType.isNothing() -> { genKotlinBaseMethodResult(Lifetime.ARGUMENT, methodBridge.returnBridge) null @@ -1433,10 +1514,12 @@ private val MethodBridgeParameter.objCType: LLVMTypeRef get() = when (this) { is MethodBridgeReceiver -> ReferenceBridge.objCType MethodBridgeSelector -> int8TypePtr MethodBridgeValueParameter.ErrorOutParameter -> pointerType(ReferenceBridge.objCType) + MethodBridgeValueParameter.SuspendCompletion -> int8TypePtr } private fun MethodBridge.ReturnValue.objCType(context: Context): LLVMTypeRef { return when (this) { + MethodBridge.ReturnValue.Suspend, MethodBridge.ReturnValue.Void -> voidType MethodBridge.ReturnValue.HashCode -> if (context.is64BitNSInteger()) int64Type else int32Type is MethodBridge.ReturnValue.Mapped -> this.bridge.objCType @@ -1483,6 +1566,7 @@ private val Family.nsUIntegerEncoding: String get() = when (this) { } private fun MethodBridge.ReturnValue.getObjCEncoding(targetFamily: Family): String = when (this) { + MethodBridge.ReturnValue.Suspend, MethodBridge.ReturnValue.Void -> "v" MethodBridge.ReturnValue.HashCode -> targetFamily.nsUIntegerEncoding is MethodBridge.ReturnValue.Mapped -> this.bridge.objCEncoding @@ -1498,6 +1582,7 @@ private val MethodBridgeParameter.objCEncoding: String get() = when (this) { is MethodBridgeReceiver -> ReferenceBridge.objCEncoding MethodBridgeSelector -> ":" MethodBridgeValueParameter.ErrorOutParameter -> "^${ReferenceBridge.objCEncoding}" + MethodBridgeValueParameter.SuspendCompletion -> "@" } private val TypeBridge.objCEncoding: String get() = when (this) { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt index 4bf3f513a3b..cf7b36de49a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt @@ -35,6 +35,7 @@ internal object MethodBridgeSelector : MethodBridgeParameter() internal sealed class MethodBridgeValueParameter : MethodBridgeParameter() { data class Mapped(val bridge: TypeBridge) : MethodBridgeValueParameter() object ErrorOutParameter : MethodBridgeValueParameter() + object SuspendCompletion : MethodBridgeValueParameter() } internal data class MethodBridge( @@ -56,6 +57,8 @@ internal data class MethodBridge( object Success : WithError() data class ZeroForError(val successBridge: ReturnValue, val successMayBeZero: Boolean) : WithError() } + + object Suspend : ReturnValue() } val paramBridges: List = @@ -86,6 +89,7 @@ internal fun MethodBridge.valueParametersAssociated( when (it) { is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next() + MethodBridgeValueParameter.SuspendCompletion, is MethodBridgeValueParameter.ErrorOutParameter -> it to null } }.also { assert(!kotlinParameters.hasNext()) } @@ -101,6 +105,7 @@ internal fun MethodBridge.parametersAssociated( is MethodBridgeValueParameter.Mapped, MethodBridgeReceiver.Instance -> it to kotlinParameters.next() + MethodBridgeValueParameter.SuspendCompletion, MethodBridgeReceiver.Static, MethodBridgeSelector, MethodBridgeValueParameter.ErrorOutParameter -> it to null 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 acef339a6d8..0134b7ffb11 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 @@ -581,6 +581,7 @@ internal class ObjCExportTranslatorImpl( } } MethodBridgeValueParameter.ErrorOutParameter -> "error" + MethodBridgeValueParameter.SuspendCompletion -> "completionHandler" } val uniqueName = unifyName(candidateName, usedNames) @@ -590,6 +591,16 @@ internal class ObjCExportTranslatorImpl( is MethodBridgeValueParameter.Mapped -> mapType(p!!.type, bridge.bridge, objCExportScope) MethodBridgeValueParameter.ErrorOutParameter -> ObjCPointerType(ObjCNullableReferenceType(ObjCClassType("NSError")), nullable = true) + + MethodBridgeValueParameter.SuspendCompletion -> { + ObjCBlockPointerType( + returnType = ObjCVoidType, + parameterTypes = listOf( + mapReferenceType(method.returnType!!, objCExportScope).makeNullable(), + ObjCNullableReferenceType(ObjCClassType("NSError")) + ) + ) + } } parameters += ObjCParameter(uniqueName, p, type) @@ -652,6 +663,7 @@ internal class ObjCExportTranslatorImpl( } private fun mapReturnType(returnBridge: MethodBridge.ReturnValue, method: FunctionDescriptor, objCExportScope: ObjCExportScope): ObjCType = when (returnBridge) { + MethodBridge.ReturnValue.Suspend, MethodBridge.ReturnValue.Void -> ObjCVoidType MethodBridge.ReturnValue.HashCode -> ObjCPrimitiveType.NSUInteger is MethodBridge.ReturnValue.Mapped -> mapType(method.returnType!!, returnBridge.bridge, objCExportScope) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt index 4f3d47f4dcd..e8c9a0405f3 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt @@ -160,7 +160,7 @@ internal class ObjCExportLazyImpl( file.children.filterIsInstance().forEach { // Supposed to be similar to ObjCExportMapper.shouldBeVisible. - if ((it is KtFunction || it is KtProperty) && it.isPublic && !it.isSuspend && !it.hasExpectModifier()) { + if ((it is KtFunction || it is KtProperty) && it.isPublic && !it.hasExpectModifier()) { val classDescriptor = getClassIfExtension(it) if (classDescriptor != null) { extensions.getOrPut(classDescriptor, { mutableListOf() }) += it @@ -438,9 +438,6 @@ private fun ModuleDescriptor.isCommonStdlib() = private val KtModifierListOwner.isPublic: Boolean get() = this.visibilityModifierTypeOrDefault() == KtTokens.PUBLIC_KEYWORD -private val KtCallableDeclaration.isSuspend: Boolean - get() = this.hasModifier(KtTokens.SUSPEND_KEYWORD) - internal val KtPureClassOrObject.isInterface: Boolean get() = this is KtClass && this.isInterface() diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt index 9ac925a0fb1..a7594d3252a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt @@ -72,7 +72,7 @@ internal fun ObjCExportMapper.getClassIfCategory(extensionReceiverType: KotlinTy // Note: partially duplicated in ObjCExportLazyImpl.translateTopLevels. internal fun ObjCExportMapper.shouldBeExposed(descriptor: CallableMemberDescriptor): Boolean = - descriptor.isEffectivelyPublicApi && !descriptor.isSuspend && !descriptor.isExpect && + descriptor.isEffectivelyPublicApi && !descriptor.isExpect && !isHiddenByDeprecation(descriptor) internal fun ObjCExportMapper.shouldBeExposed(descriptor: ClassDescriptor): Boolean = @@ -241,6 +241,8 @@ private fun ObjCExportMapper.bridgeReturnType( ): MethodBridge.ReturnValue { val returnType = descriptor.returnType!! return when { + descriptor.isSuspend -> MethodBridge.ReturnValue.Suspend + descriptor is ConstructorDescriptor -> if (descriptor.constructedClass.isArray) { MethodBridge.ReturnValue.Instance.FactoryResult } else { @@ -315,7 +317,10 @@ private fun ObjCExportMapper.bridgeMethodImpl(descriptor: FunctionDescriptor): M } val returnBridge = bridgeReturnType(descriptor, convertExceptionsToErrors) - if (convertExceptionsToErrors) { + + if (descriptor.isSuspend) { + valueParameters += MethodBridgeValueParameter.SuspendCompletion + } else if (convertExceptionsToErrors) { // Add error out parameter before tail block parameters. The convention allows this. // Placing it after would trigger https://bugs.swift.org/browse/SR-12201 // (see also https://github.com/JetBrains/kotlin-native/issues/3825). @@ -332,6 +337,7 @@ private fun MethodBridgeValueParameter.isBlockPointer(): Boolean = when (this) { is BlockPointerBridge -> true } MethodBridgeValueParameter.ErrorOutParameter -> false + MethodBridgeValueParameter.SuspendCompletion -> true } internal fun ObjCExportMapper.bridgePropertyType(descriptor: PropertyDescriptor): TypeBridge { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt index b254d1898ff..1d2fe5b287b 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt @@ -453,13 +453,17 @@ internal class ObjCExportNamerImpl( else -> it!!.name.asString().toIdentifier() } MethodBridgeValueParameter.ErrorOutParameter -> "error" + MethodBridgeValueParameter.SuspendCompletion -> "completionHandler" } if (index == 0) { append(when { bridge is MethodBridgeValueParameter.ErrorOutParameter -> "AndReturn" + bridge is MethodBridgeValueParameter.SuspendCompletion -> "With" + method is ConstructorDescriptor -> "With" + else -> "" }) append(name.capitalize()) @@ -502,6 +506,7 @@ internal class ObjCExportNamerImpl( else -> it!!.name.asString().toIdentifier() } MethodBridgeValueParameter.ErrorOutParameter -> continue@parameters + MethodBridgeValueParameter.SuspendCompletion -> "completionHandler" } append(label) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt index dd83df5c208..ef06ee6ecdc 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt @@ -163,3 +163,8 @@ internal fun ObjCType.makeNullableIfReferenceOrPointer(): ObjCType = when (this) is ObjCNullableReferenceType, is ObjCRawType, is ObjCPrimitiveType, ObjCVoidType -> this } + +internal fun ObjCReferenceType.makeNullable(): ObjCNullableReferenceType = when (this) { + is ObjCNonNullReferenceType -> ObjCNullableReferenceType(this) + is ObjCNullableReferenceType -> this +} diff --git a/backend.native/tests/framework/main.swift b/backend.native/tests/framework/main.swift index 7f379d48166..7db9f6948a8 100644 --- a/backend.native/tests/framework/main.swift +++ b/backend.native/tests/framework/main.swift @@ -37,6 +37,15 @@ func assertEquals(actual: T, expected: T, } } +func assertSame(actual: AnyObject?, expected: AnyObject?, + _ message: String = "Assertion failed:", + file: String = #file, line: Int = #line) throws { + if (actual !== expected) { + try throwAssertFailed(message: message + " Expected value: \(expected), but got: \(actual)", + file: file, line: line) + } +} + func assertEquals(actual: [T], expected: [T], _ message: String = "Assertion failed: arrays not equal", file: String = #file, line: Int = #line) throws { diff --git a/backend.native/tests/objcexport/coroutines.kt b/backend.native/tests/objcexport/coroutines.kt new file mode 100644 index 00000000000..e05db7a657e --- /dev/null +++ b/backend.native/tests/objcexport/coroutines.kt @@ -0,0 +1,136 @@ +package coroutines + +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlin.native.concurrent.isFrozen +import kotlin.native.internal.ObjCErrorException +import kotlin.test.* + +class CoroutineException : Throwable() + +suspend fun suspendFun() = 42 + +@Throws(CoroutineException::class) +suspend fun suspendFun(result: Any?, doSuspend: Boolean, doThrow: Boolean): Any? { + if (doSuspend) { + suspendCoroutineUninterceptedOrReturn { + it.resume(Unit) + COROUTINE_SUSPENDED + } + } + + if (doThrow) throw CoroutineException() + + return result +} + +class ContinuationHolder { + internal lateinit var continuation: Continuation + + fun resume(value: T) { + continuation.resume(value) + } + + fun resumeWithException(exception: Throwable) { + continuation.resumeWithException(exception) + } +} + +@Throws(CoroutineException::class) +suspend fun suspendFunAsync(result: Any?, continuationHolder: ContinuationHolder): Any? = + suspendCoroutineUninterceptedOrReturn { + continuationHolder.continuation = it + COROUTINE_SUSPENDED + } ?: result + +@Throws(CoroutineException::class) +fun throwException(exception: Throwable) { + throw exception +} + +interface SuspendFun { + @Throws(CoroutineException::class) + suspend fun suspendFun(doYield: Boolean, doThrow: Boolean): Int +} + +class ResultHolder { + var completed: Int = 0 + var result: T? = null + var exception: Throwable? = null + + internal fun complete(result: Result) { + this.result = result.getOrNull() + this.exception = result.exceptionOrNull() + this.completed += 1 + } +} + +private class ResultHolderCompletion(val resultHolder: ResultHolder) : Continuation { + override val context: CoroutineContext + get() = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + resultHolder.complete(result) + } +} + +fun callSuspendFun(suspendFun: SuspendFun, doYield: Boolean, doThrow: Boolean, resultHolder: ResultHolder) { + suspend { suspendFun.suspendFun(doYield = doYield, doThrow = doThrow) } + .startCoroutine(ResultHolderCompletion(resultHolder)) +} + +@Throws +suspend fun callSuspendFun2(suspendFun: SuspendFun, doYield: Boolean, doThrow: Boolean): Int { + return suspendFun.suspendFun(doYield = doYield, doThrow = doThrow) +} + +interface SuspendBridge { + suspend fun int(value: T): Int + suspend fun intAsAny(value: T): Any? + + suspend fun unit(value: T): Unit + suspend fun unitAsAny(value: T): Any? + + @Throws(CoroutineException::class) suspend fun nothing(value: T): Nothing + @Throws(CoroutineException::class) suspend fun nothingAsInt(value: T): Int + @Throws(CoroutineException::class) suspend fun nothingAsAny(value: T): Any? + @Throws(CoroutineException::class) suspend fun nothingAsUnit(value: T): Unit +} + +abstract class AbstractSuspendBridge : SuspendBridge { + override suspend fun intAsAny(value: Int): Int = TODO() + + override suspend fun unitAsAny(value: Int): Unit = TODO() + + override suspend fun nothingAsInt(value: Int): Nothing = TODO() + override suspend fun nothingAsAny(value: Int): Nothing = TODO() + override suspend fun nothingAsUnit(value: Int): Nothing = TODO() +} + +private suspend fun callSuspendBridgeImpl(bridge: SuspendBridge) { + assertEquals(1, bridge.intAsAny(1)) + + assertSame(Unit, bridge.unitAsAny(2)) + + assertFailsWith { bridge.nothingAsInt(3) } + assertFailsWith { bridge.nothingAsAny(4) } + assertFailsWith { bridge.nothingAsUnit(5) } +} + +private suspend fun callAbstractSuspendBridgeImpl(bridge: AbstractSuspendBridge) { + assertEquals(6, bridge.intAsAny(6)) + + assertSame(Unit, bridge.unitAsAny(7)) + + assertFailsWith { bridge.nothingAsInt(8) } + assertFailsWith { bridge.nothingAsAny(9) } + assertFailsWith { bridge.nothingAsUnit(10) } +} + +@Throws +fun callSuspendBridge(bridge: AbstractSuspendBridge, resultHolder: ResultHolder) { + suspend { + callSuspendBridgeImpl(bridge) + callAbstractSuspendBridgeImpl(bridge) + }.startCoroutine(ResultHolderCompletion(resultHolder)) +} \ No newline at end of file diff --git a/backend.native/tests/objcexport/coroutines.swift b/backend.native/tests/objcexport/coroutines.swift new file mode 100644 index 00000000000..3cb587d7a21 --- /dev/null +++ b/backend.native/tests/objcexport/coroutines.swift @@ -0,0 +1,228 @@ +import Kt + +private func testCallSimple() throws { + var result: KotlinInt? = nil + var error: Error? = nil + var completionCalled = 0 + + CoroutinesKt.suspendFun { _result, _error in + completionCalled += 1 + result = _result + error = _error + } + + try assertEquals(actual: completionCalled, expected: 1) + try assertEquals(actual: result, expected: 42) + try assertNil(error) +} + +private func testCallSuspendFun(doSuspend: Bool, doThrow: Bool) throws { + class C {} + let expectedResult = C() + + var completionCalled = 0 + var result: AnyObject? = nil + var error: Error? = nil + + CoroutinesKt.suspendFun(result: expectedResult, doSuspend: doSuspend, doThrow: doThrow) { _result, _error in + completionCalled += 1 + result = _result as AnyObject? + error = _error + } + + try assertEquals(actual: completionCalled, expected: 1) + + if doThrow { + try assertNil(result) + try assertTrue(error?.kotlinException is CoroutineException) + } else { + try assertSame(actual: result, expected: expectedResult) + try assertNil(error) + } +} + +private func testSuspendFuncAsync(doThrow: Bool) throws { + var completionCalled = 0 + var result: AnyObject? = nil + var error: Error? = nil + + let continuationHolder = ContinuationHolder() + + CoroutinesKt.suspendFunAsync(result: nil, continuationHolder: continuationHolder) { _result, _error in + completionCalled += 1 + result = _result as AnyObject? + error = _error + } + + try assertEquals(actual: completionCalled, expected: 0) + + if doThrow { + let exception = CoroutineException() + continuationHolder.resumeWithException(exception: exception) + + try assertEquals(actual: completionCalled, expected: 1) + + try assertNil(result) + try assertSame(actual: error?.kotlinException as AnyObject?, expected: exception) + } else { + class C {} + let expectedResult = C() + continuationHolder.resume(value: expectedResult) + + try assertEquals(actual: completionCalled, expected: 1) + + try assertSame(actual: result, expected: expectedResult) + try assertNil(error) + } +} + +private func testCall() throws { + try testCallSuspendFun(doSuspend: true, doThrow: false) + try testCallSuspendFun(doSuspend: false, doThrow: false) + try testCallSuspendFun(doSuspend: true, doThrow: true) + try testCallSuspendFun(doSuspend: false, doThrow: true) + + try testSuspendFuncAsync(doThrow: false) + try testSuspendFuncAsync(doThrow: true) +} + +private class SuspendFunImpl : SuspendFun { + class E : Error {} + + var completion: (() -> Void)? = nil + + func suspendFun(doYield: Bool, doThrow: Bool, completionHandler: @escaping (KotlinInt?, Error?) -> Void) { + func callCompletion() { + if doThrow { + completionHandler(nil, E()) + } else { + completionHandler(17, nil) + } + } + + if doYield { + self.completion = callCompletion + } else { + callCompletion() + } + } +} + +private func testSuspendFunImpl(doYield: Bool, doThrow: Bool) throws { + let resultHolder = ResultHolder() + + let impl = SuspendFunImpl() + + CoroutinesKt.callSuspendFun( + suspendFun: impl, + doYield: doYield, + doThrow: doThrow, + resultHolder: resultHolder + ) + + if doYield { + try assertEquals(actual: resultHolder.completed, expected: 0) + guard let completion = impl.completion else { try fail() } + completion() + } + + try assertEquals(actual: resultHolder.completed, expected: 1) + + if doThrow { + try assertNil(resultHolder.result) + if let e = resultHolder.exception { + try assertFailsWith(SuspendFunImpl.E.self) { try CoroutinesKt.throwException(exception: e) } + } else { + try fail() + } + } else { + try assertEquals(actual: resultHolder.result, expected: 17) + try assertNil(resultHolder.exception) + } +} + +private func testSuspendFunImpl2(doYield: Bool, doThrow: Bool) throws { + let impl = SuspendFunImpl() + + var completionCalled = 0 + var result: KotlinInt? = nil + var error: Error? = nil + + CoroutinesKt.callSuspendFun2(suspendFun: impl, doYield: doYield, doThrow: doThrow) { _result, _error in + completionCalled += 1 + result = _result + error = _error + } + + if doYield { + try assertEquals(actual: completionCalled, expected: 0) + guard let completion = impl.completion else { try fail() } + completion() + } + + try assertEquals(actual: completionCalled, expected: 1) + + if doThrow { + try assertNil(result) + try assertTrue(error is SuspendFunImpl.E) + } else { + try assertEquals(actual: result, expected: 17) + try assertNil(error) + } +} + +private func testOverride() throws { + try testSuspendFunImpl(doYield: false, doThrow: false) + try testSuspendFunImpl(doYield: false, doThrow: true) + try testSuspendFunImpl(doYield: true, doThrow: false) + try testSuspendFunImpl(doYield: true, doThrow: true) + + try testSuspendFunImpl2(doYield: false, doThrow: false) + try testSuspendFunImpl2(doYield: false, doThrow: true) + try testSuspendFunImpl2(doYield: true, doThrow: false) + try testSuspendFunImpl2(doYield: true, doThrow: true) +} + +private class SwiftSuspendBridge : AbstractSuspendBridge { + class E : Error {} + + override func intAsAny(value: KotlinInt, completionHandler: @escaping (KotlinInt?, Error?) -> Void) { + completionHandler(value, nil) + } + + override func unitAsAny(value: KotlinInt, completionHandler: @escaping (KotlinUnit?, Error?) -> Void) { + completionHandler(KotlinUnit(), nil) + } + + override func nothingAsInt(value: KotlinInt, completionHandler: @escaping (KotlinNothing?, Error?) -> Void) { + completionHandler(nil, E()) + } + + override func nothingAsAny(value: KotlinInt, completionHandler: @escaping (KotlinNothing?, Error?) -> Void) { + completionHandler(nil, E()) + } + + override func nothingAsUnit(value: KotlinInt, completionHandler: @escaping (KotlinNothing?, Error?) -> Void) { + completionHandler(nil, E()) + } +} + +private func testBridges() throws { + let resultHolder = ResultHolder() + try CoroutinesKt.callSuspendBridge(bridge: SwiftSuspendBridge(), resultHolder: resultHolder) + + try assertEquals(actual: resultHolder.completed, expected: 1) + try assertNil(resultHolder.exception) + try assertSame(actual: resultHolder.result, expected: KotlinUnit()) +} + +class CoroutinesTests : SimpleTestProvider { + override init() { + super.init() + + test("TestCallSimple", testCallSimple) + test("TestCall", testCall) + test("TestOverride", testOverride) + test("TestBridges", testBridges) + } +} \ No newline at end of file diff --git a/backend.native/tests/objcexport/expectedLazy.h b/backend.native/tests/objcexport/expectedLazy.h index e17b3a46b1b..7c3eee7c21d 100644 --- a/backend.native/tests/objcexport/expectedLazy.h +++ b/backend.native/tests/objcexport/expectedLazy.h @@ -120,6 +120,77 @@ __attribute__((swift_name("KotlinBoolean"))) + (instancetype)numberWithBool:(BOOL)value; @end; +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("CoroutineException"))) +@interface KtCoroutineException : 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__((objc_subclassing_restricted)) +__attribute__((swift_name("ContinuationHolder"))) +@interface KtContinuationHolder : KtBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (void)resumeValue:(T _Nullable)value __attribute__((swift_name("resume(value:)"))); +- (void)resumeWithExceptionException:(KtKotlinThrowable *)exception __attribute__((swift_name("resumeWithException(exception:)"))); +@end; + +__attribute__((swift_name("SuspendFun"))) +@protocol KtSuspendFun +@required +- (void)suspendFunDoYield:(BOOL)doYield doThrow:(BOOL)doThrow completionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("suspendFun(doYield:doThrow:completionHandler:)"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("ResultHolder"))) +@interface KtResultHolder : KtBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +@property int32_t completed __attribute__((swift_name("completed"))); +@property T _Nullable result __attribute__((swift_name("result"))); +@property KtKotlinThrowable * _Nullable exception __attribute__((swift_name("exception"))); +@end; + +__attribute__((swift_name("SuspendBridge"))) +@protocol KtSuspendBridge +@required +- (void)intValue:(id _Nullable)value completionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("int(value:completionHandler:)"))); +- (void)intAsAnyValue:(id _Nullable)value completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("intAsAny(value:completionHandler:)"))); +- (void)unitValue:(id _Nullable)value completionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("unit(value:completionHandler:)"))); +- (void)unitAsAnyValue:(id _Nullable)value completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("unitAsAny(value:completionHandler:)"))); +- (void)nothingValue:(id _Nullable)value completionHandler:(void (^)(KtKotlinNothing * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothing(value:completionHandler:)"))); +- (void)nothingAsIntValue:(id _Nullable)value completionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsInt(value:completionHandler:)"))); +- (void)nothingAsAnyValue:(id _Nullable)value completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsAny(value:completionHandler:)"))); +- (void)nothingAsUnitValue:(id _Nullable)value completionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsUnit(value:completionHandler:)"))); +@end; + +__attribute__((swift_name("AbstractSuspendBridge"))) +@interface KtAbstractSuspendBridge : KtBase +- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); ++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); +- (void)intAsAnyValue:(KtInt *)value completionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("intAsAny(value:completionHandler:)"))); +- (void)unitAsAnyValue:(KtInt *)value completionHandler:(void (^)(KtKotlinUnit * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("unitAsAny(value:completionHandler:)"))); +- (void)nothingAsIntValue:(KtInt *)value completionHandler:(void (^)(KtKotlinNothing * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsInt(value:completionHandler:)"))); +- (void)nothingAsAnyValue:(KtInt *)value completionHandler:(void (^)(KtKotlinNothing * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsAny(value:completionHandler:)"))); +- (void)nothingAsUnitValue:(KtInt *)value completionHandler:(void (^)(KtKotlinNothing * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("nothingAsUnit(value:completionHandler:)"))); +@end; + +__attribute__((objc_subclassing_restricted)) +__attribute__((swift_name("CoroutinesKt"))) +@interface KtCoroutinesKt : KtBase ++ (void)suspendFunWithCompletionHandler:(void (^)(KtInt * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("suspendFun(completionHandler:)"))); ++ (void)suspendFunResult:(id _Nullable)result doSuspend:(BOOL)doSuspend doThrow:(BOOL)doThrow completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("suspendFun(result:doSuspend:doThrow:completionHandler:)"))); ++ (void)suspendFunAsyncResult:(id _Nullable)result continuationHolder:(KtContinuationHolder *)continuationHolder completionHandler:(void (^)(id _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("suspendFunAsync(result:continuationHolder:completionHandler:)"))); ++ (BOOL)throwExceptionException:(KtKotlinThrowable *)exception error:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("throwException(exception:)"))); ++ (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:)"))); +@end; + __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("DelegateClass"))) @interface KtDelegateClass : KtBase diff --git a/runtime/src/main/cpp/ObjCExport.h b/runtime/src/main/cpp/ObjCExport.h index d3281e66230..baec65cf0b9 100644 --- a/runtime/src/main/cpp/ObjCExport.h +++ b/runtime/src/main/cpp/ObjCExport.h @@ -9,6 +9,7 @@ #import "Memory.h" extern "C" id objc_retain(id self); +extern "C" id objc_retainBlock(id self); extern "C" void objc_release(id self); inline static id GetAssociatedObject(ObjHeader* obj) { diff --git a/runtime/src/main/cpp/ObjCExport.mm b/runtime/src/main/cpp/ObjCExport.mm index 446965d8116..c888eee295f 100644 --- a/runtime/src/main/cpp/ObjCExport.mm +++ b/runtime/src/main/cpp/ObjCExport.mm @@ -444,8 +444,6 @@ static const char* getBlockEncoding(id block) { extern "C" convertReferenceFromObjC* Kotlin_ObjCExport_blockToFunctionConverters; extern "C" int Kotlin_ObjCExport_blockToFunctionConverters_size; -extern "C" id objc_retainBlock(id self); - static OBJ_GETTER(blockToKotlinImp, id block, SEL cmd) { const char* encoding = getBlockEncoding(block); diff --git a/runtime/src/main/cpp/ObjCExportCoroutines.mm b/runtime/src/main/cpp/ObjCExportCoroutines.mm new file mode 100644 index 00000000000..d041ccf78d1 --- /dev/null +++ b/runtime/src/main/cpp/ObjCExportCoroutines.mm @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2020 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 +#import +#import + +#import "ObjCExport.h" +#import "ObjCExportErrors.h" + +typedef void (^Completion)(id _Nullable, NSError* _Nullable); + +extern "C" void Kotlin_ObjCExport_runCompletionSuccess(KRef completionHolder, KRef result) { + Completion completion = (Completion)GetAssociatedObject(completionHolder); + completion(Kotlin_ObjCExport_refToObjC(result), nullptr); +} + +extern "C" void Kotlin_ObjCExport_runCompletionFailure( + KRef completionHolder, + KRef exception, + const TypeInfo** exceptionTypes +) { + id error = Kotlin_ObjCExport_ExceptionAsNSError(exception, exceptionTypes); + Completion completion = (Completion)GetAssociatedObject(completionHolder); + completion(nullptr, error); +} + +extern "C" OBJ_GETTER(Kotlin_ObjCExport_createContinuationArgumentImpl, + KRef completionHolder, const TypeInfo** exceptionTypes); + +extern "C" OBJ_GETTER(Kotlin_ObjCExport_createContinuationArgument, id completion, const TypeInfo** exceptionTypes) { + if (pthread_main_np() != 1) { + [NSException raise:NSGenericException + format:@"Calling Kotlin suspend functions from Swift/Objective-C is currently supported only on main thread"]; + } + ObjHolder slot; + KRef completionHolder = AllocInstanceWithAssociatedObject(theForeignObjCObjectTypeInfo, + objc_retainBlock(completion), slot.slot()); + + RETURN_RESULT_OF(Kotlin_ObjCExport_createContinuationArgumentImpl, completionHolder, exceptionTypes); +} + +extern "C" void Kotlin_ObjCExport_resumeContinuationSuccess(KRef continuation, KRef result); +extern "C" void Kotlin_ObjCExport_resumeContinuationFailure(KRef continuation, KRef exception); + +extern "C" void Kotlin_ObjCExport_resumeContinuation(KRef continuation, id result, id error) { + ObjHolder holder; + + if (error != nullptr) { + if (result != nullptr) { + [NSException raise:NSGenericException + format:@"Kotlin completion handler is called with both result (%@) and error (%@) specified", + result, error]; + } + + KRef exception = Kotlin_ObjCExport_NSErrorAsException(error, holder.slot()); + Kotlin_ObjCExport_resumeContinuationFailure(continuation, exception); + } else { + KRef kotlinResult = Kotlin_ObjCExport_refFromObjC(result, holder.slot()); + Kotlin_ObjCExport_resumeContinuationSuccess(continuation, kotlinResult); + } +} + +#endif diff --git a/runtime/src/main/cpp/ObjCExportErrors.h b/runtime/src/main/cpp/ObjCExportErrors.h new file mode 100644 index 00000000000..6c9a0af42cd --- /dev/null +++ b/runtime/src/main/cpp/ObjCExportErrors.h @@ -0,0 +1,9 @@ +/* + * Copyright 2010-2020 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. + */ + +#import "Types.h" + +extern "C" id Kotlin_ObjCExport_ExceptionAsNSError(KRef exception, const TypeInfo** types); +extern "C" OBJ_GETTER(Kotlin_ObjCExport_NSErrorAsException, id error); diff --git a/runtime/src/main/cpp/ObjCExportErrors.mm b/runtime/src/main/cpp/ObjCExportErrors.mm index 489203dfe98..f85cc940f43 100644 --- a/runtime/src/main/cpp/ObjCExportErrors.mm +++ b/runtime/src/main/cpp/ObjCExportErrors.mm @@ -26,6 +26,8 @@ #import "Runtime.h" #import "Utils.h" +#import "ObjCExportErrors.h" + extern "C" OBJ_GETTER(Kotlin_Throwable_getMessage, KRef throwable); extern "C" OBJ_GETTER(Kotlin_ObjCExport_getWrappedError, KRef throwable); @@ -52,14 +54,13 @@ static bool isExceptionOfType(KRef exception, const TypeInfo** types) { return false; } -extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* outError, const TypeInfo** types) { +extern "C" id Kotlin_ObjCExport_ExceptionAsNSError(KRef exception, 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; + return Kotlin_ObjCExport_refToObjC(error); } if (!isExceptionOfType(exception, types)) { @@ -69,10 +70,6 @@ extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* TerminateWithUnhandledException(exception); } - if (outError == nullptr) { - return; - } - NSMutableDictionary* userInfo = [[NSMutableDictionary new] autorelease]; userInfo[@"KotlinException"] = Kotlin_ObjCExport_refToObjC(exception); userInfo[@"KotlinExceptionOrigin"] = @(&kotlinExceptionOriginChar); // Support for different Kotlin runtimes loaded. @@ -84,13 +81,17 @@ extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* userInfo[NSLocalizedDescriptionKey] = description; } - *outError = [NSError errorWithDomain:@"KotlinException" code:0 userInfo:userInfo]; - return; + return [NSError errorWithDomain:@"KotlinException" code:0 userInfo:userInfo]; } -extern "C" void Kotlin_ObjCExport_RethrowNSErrorAsExceptionImpl(KRef message, KRef error); +extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* outError, const TypeInfo** types) { + id error = Kotlin_ObjCExport_ExceptionAsNSError(exception, types); // Also traps on unexpected exception. + if (outError != nullptr) *outError = error; +} -extern "C" void Kotlin_ObjCExport_RethrowNSErrorAsException(id error) { +extern "C" OBJ_GETTER(Kotlin_ObjCExport_NSErrorAsExceptionImpl, KRef message, KRef error); + +extern "C" OBJ_GETTER(Kotlin_ObjCExport_NSErrorAsException, id error) { NSString* description; NSError* e = (NSError*) error; @@ -102,9 +103,7 @@ extern "C" void Kotlin_ObjCExport_RethrowNSErrorAsException(id error) { if (kotlinException != nullptr && kotlinExceptionOrigin != nullptr && [kotlinExceptionOrigin isEqual:@(&kotlinExceptionOriginChar)] ) { - ObjHolder kotlinExceptionHolder; - ThrowException(Kotlin_ObjCExport_refFromObjC(kotlinException, kotlinExceptionHolder.slot())); - return; + RETURN_RESULT_OF(Kotlin_ObjCExport_refFromObjC, kotlinException); } } description = e.localizedDescription; @@ -116,7 +115,13 @@ extern "C" void Kotlin_ObjCExport_RethrowNSErrorAsException(id error) { KRef message = Kotlin_Interop_CreateKStringFromNSString(description, messageHolder.slot()); KRef kotlinError = Kotlin_ObjCExport_refFromObjC(error, errorHolder.slot()); // TODO: a simple opaque wrapper would be enough. - Kotlin_ObjCExport_RethrowNSErrorAsExceptionImpl(message, kotlinError); + RETURN_RESULT_OF(Kotlin_ObjCExport_NSErrorAsExceptionImpl, message, kotlinError); +} + +extern "C" void Kotlin_ObjCExport_RethrowNSErrorAsException(id error) { + ObjHolder holder; + KRef exception = Kotlin_ObjCExport_NSErrorAsException(error, holder.slot()); + ThrowException(exception); } @interface NSError (NSErrorKotlinException) diff --git a/runtime/src/main/kotlin/kotlin/coroutines/intrinsics/IntrinsicsNative.kt b/runtime/src/main/kotlin/kotlin/coroutines/intrinsics/IntrinsicsNative.kt index dfdbac2469f..940af70a762 100644 --- a/runtime/src/main/kotlin/kotlin/coroutines/intrinsics/IntrinsicsNative.kt +++ b/runtime/src/main/kotlin/kotlin/coroutines/intrinsics/IntrinsicsNative.kt @@ -193,4 +193,35 @@ private inline fun createCoroutineFromSuspendFunction( else -> error("This coroutine had already completed") } } +} + +/** + * This function creates continuation suitable for passing as implicit argument to suspend functions. + * The continuation calls [callback] and then delegates to [completion]. + * + * The result is [ContinuationImpl] because that is an expectation of all coroutines machinery. + * + * It can be thought as a state machine of + * ``` + * suspend fun foo() { + * val result = runCatching { } + * callback(result) + * } + * ``` + */ +@Suppress("UNCHECKED_CAST") +internal inline fun createContinuationArgumentFromCallback( + completion: Continuation, + crossinline callback: (Result) -> Unit +): Continuation = object : ContinuationImpl(completion as Continuation) { + private var invoked = false + + override fun invokeSuspend(result: Result): Any? { + if (invoked) error("This coroutine had already completed") + invoked = true + + callback(result) + + return Unit // Not suspended. + } } \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt new file mode 100644 index 00000000000..c8a6188dc5f --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportCoroutines.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2018 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. + */ + +package kotlin.native.internal + +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlin.coroutines.native.internal.* +import kotlin.native.concurrent.* + +@ExportForCppRuntime +private fun Kotlin_ObjCExport_createContinuationArgumentImpl( + completionHolder: Any, + exceptionTypes: NativePtr +): Continuation = createContinuationArgumentFromCallback(EmptyCompletion) { result -> + result.fold( + onSuccess = { value -> + runCompletionSuccess(completionHolder, value) + }, + onFailure = { exception -> + runCompletionFailure(completionHolder, exception, exceptionTypes) + } + ) +} + +private object EmptyCompletion : Continuation { + override val context: CoroutineContext + get() = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + val exception = result.exceptionOrNull() ?: return + TerminateWithUnhandledException(exception) + // Throwing the exception from [resumeWith] is not generally expected. + // Also terminating is consistent with other pieces of ObjCExport machinery. + } +} + +@PublishedApi +@ExportForCppRuntime("Kotlin_ObjCExport_resumeContinuationSuccess") // Also makes it a data flow root. +internal fun resumeContinuation(continuation: Continuation, value: Any?) { + continuation.resume(value) +} + +@PublishedApi +@ExportForCppRuntime("Kotlin_ObjCExport_resumeContinuationFailure") // Also makes it a data flow root. +internal fun resumeContinuationWithException(continuation: Continuation, exception: Throwable) { + continuation.resumeWithException(exception) +} + +@PublishedApi +@ExportForCompiler // Mark as data flow root. +internal fun getCoroutineSuspended(): Any = COROUTINE_SUSPENDED + +@PublishedApi +@ExportForCompiler // Mark as data flow root. +internal fun interceptedContinuation(continuation: Continuation): Continuation = continuation.intercepted() + +@FilterExceptions +@SymbolName("Kotlin_ObjCExport_runCompletionSuccess") +private external fun runCompletionSuccess(completionHolder: Any, result: Any?) + +@FilterExceptions +@SymbolName("Kotlin_ObjCExport_runCompletionFailure") +private external fun runCompletionFailure(completionHolder: Any, exception: Throwable, exceptionTypes: NativePtr) \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt index 6e58474f0e0..1454a625c26 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/ObjCExportUtils.kt @@ -266,12 +266,10 @@ internal class NSEnumeratorAsKIterator : AbstractIterator() { @ExportForCppRuntime private fun Kotlin_NSSetAsKSet_create() = NSSetAsKSet() @ExportForCppRuntime private fun Kotlin_NSDictionaryAsKMap_create() = NSDictionaryAsKMap() -@ExportForCppRuntime private fun Kotlin_ObjCExport_RethrowNSErrorAsExceptionImpl( +@ExportForCppRuntime private fun Kotlin_ObjCExport_NSErrorAsExceptionImpl( message: String?, error: Any -) { - throw ObjCErrorException(message, error) -} +) = ObjCErrorException(message, error) class ObjCErrorException( message: String?, diff --git a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt index 3435328425e..2790ff08e76 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt @@ -110,6 +110,9 @@ internal fun ReportUnhandledException(throwable: Throwable) { throwable.printStackTrace() } +@SymbolName("TerminateWithUnhandledException") +internal external fun TerminateWithUnhandledException(throwable: Throwable) + @ExportForCppRuntime internal fun ExceptionReporterLaunchpad(reporter: (Throwable) -> Unit, throwable: Throwable) { try {