diff --git a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ForeignException.kt b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ForeignException.kt new file mode 100644 index 00000000000..9872a71a2b6 --- /dev/null +++ b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ForeignException.kt @@ -0,0 +1,21 @@ +/* + * 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 kotlinx.cinterop + +import kotlin.native.internal.ExportForCppRuntime + +public class ForeignException internal constructor(val nativeException: Any?): Exception() { + override val message: String = nativeException?.let { + kotlin_ObjCExport_ExceptionDetails(nativeException) + }?: "" + + // Current implementation expects NSException type only, which is ensured by CodeGenerator. + @SymbolName("Kotlin_ObjCExport_ExceptionDetails") + private external fun kotlin_ObjCExport_ExceptionDetails(nativeException: Any): String? +} + +@ExportForCppRuntime +internal fun CreateForeignException(payload: NativePtr): Throwable + = ForeignException(interpretObjCPointerOrNull(payload)) diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt index fe56513d669..e4a9a1bb23b 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/CommandLine.kt @@ -29,6 +29,7 @@ const val TEMP_DIR = "Xtemporary-files-dir" const val NOPACK = "nopack" const val COMPILE_SOURCES = "Xcompile-source" const val SHORT_MODULE_NAME = "Xshort-module-name" +const val FOREIGN_EXCEPTION_MODE = "Xforeign-exception-mode" // TODO: unify camel and snake cases. // Possible solution is to accept both cases @@ -119,6 +120,9 @@ open class CInteropArguments(argParser: ArgParser = fullName = "Xmodule-name", description = "A full name of the library used for dependency resolution" ) + + val foreignExceptionMode by argParser.option(ArgType.String, FOREIGN_EXCEPTION_MODE, + description = "Handle native exception in Kotlin: ") } class JSInteropArguments(argParser: ArgParser = ArgParser("jsinterop", diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt index 535ae78b6d0..7fdcf966774 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt @@ -27,6 +27,7 @@ import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.default import kotlinx.cli.required +import org.jetbrains.kotlin.konan.ForeignExceptionMode import org.jetbrains.kotlin.konan.library.* import org.jetbrains.kotlin.konan.target.CompilerOutputKind import org.jetbrains.kotlin.konan.target.Distribution @@ -200,7 +201,7 @@ private fun processCLib(flavorName: String, cinteropArguments: CInteropArguments val manifestAddend = additionalArgs.manifest?.let { File(it) } if (defFile == null && cinteropArguments.pkg == null) { - cinteropArguments.argParser.printError("-def or -pkg should provided!") + cinteropArguments.argParser.printError("-def or -pkg should be provided!") } val tool = prepareTool(cinteropArguments.target, flavor) @@ -327,6 +328,13 @@ private fun processCLib(flavorName: String, cinteropArguments: CInteropArguments } stubIrContext.addManifestProperties(def.manifestAddendProperties) + // cinterop command line option overrides def file property + val foreignExceptionMode = cinteropArguments.foreignExceptionMode?: def.config.foreignExceptionMode + foreignExceptionMode?.let { + def.manifestAddendProperties[ForeignExceptionMode.manifestKey] = + ForeignExceptionMode.byValue(it).value // may throw IllegalArgumentException + } + manifestAddend?.parentFile?.mkdirs() manifestAddend?.let { def.manifestAddendProperties.storeProperties(it) } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt index d95eb6b7ef3..1c2a88efdb8 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CAdapterGenerator.kt @@ -137,7 +137,7 @@ private fun isExportedClass(descriptor: ClassDescriptor): Boolean { return true } -private fun AnnotationDescriptor.properValue(key: String) = +internal fun AnnotationDescriptor.properValue(key: String) = this.argumentValue(key)?.toString()?.removeSurrounding("\"") private fun functionImplName(descriptor: DeclarationDescriptor, default: String, shortName: Boolean): String { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt index adc6634ea23..2d30f8ee317 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.common.lower.irNot import org.jetbrains.kotlin.backend.konan.KonanFqNames import org.jetbrains.kotlin.backend.konan.PrimitiveBinaryType import org.jetbrains.kotlin.backend.konan.RuntimeNames +import org.jetbrains.kotlin.backend.konan.descriptors.konanLibrary import org.jetbrains.kotlin.backend.konan.ir.* import org.jetbrains.kotlin.backend.konan.isObjCMetaClass import org.jetbrains.kotlin.builtins.KotlinBuiltIns @@ -45,6 +46,7 @@ import org.jetbrains.kotlin.backend.konan.getObjCMethodInfo import org.jetbrains.kotlin.backend.konan.lower.FunctionReferenceLowering import org.jetbrains.kotlin.builtins.StandardNames import org.jetbrains.kotlin.ir.descriptors.* +import org.jetbrains.kotlin.konan.ForeignExceptionMode internal interface KotlinStubs { val irBuiltIns: IrBuiltIns @@ -62,7 +64,8 @@ internal interface KotlinStubs { private class KotlinToCCallBuilder( val irBuilder: IrBuilderWithScope, val stubs: KotlinStubs, - val isObjCMethod: Boolean + val isObjCMethod: Boolean, + foreignExceptionMode: ForeignExceptionMode.Mode ) { val cBridgeName = stubs.getUniqueCName("knbridge") @@ -70,7 +73,7 @@ private class KotlinToCCallBuilder( val symbols: KonanSymbols get() = stubs.symbols val bridgeCallBuilder = KotlinCallBuilder(irBuilder, symbols) - val bridgeBuilder = KotlinCBridgeBuilder(irBuilder.startOffset, irBuilder.endOffset, cBridgeName, stubs, isKotlinToC = true) + val bridgeBuilder = KotlinCBridgeBuilder(irBuilder.startOffset, irBuilder.endOffset, cBridgeName, stubs, isKotlinToC = true, foreignExceptionMode) val cBridgeBodyLines = mutableListOf() val cCallBuilder = CCallBuilder() val cFunctionBuilder = CFunctionBuilder() @@ -110,10 +113,11 @@ private fun KotlinToCCallBuilder.buildKotlinBridgeCall(transformCall: (IrMemberA transformCall ) -internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWithScope, isInvoke: Boolean): IrExpression { +internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWithScope, isInvoke: Boolean, + foreignExceptionMode: ForeignExceptionMode.Mode = ForeignExceptionMode.default): IrExpression { require(expression.dispatchReceiver == null) - val callBuilder = KotlinToCCallBuilder(builder, this, isObjCMethod = false) + val callBuilder = KotlinToCCallBuilder(builder, this, isObjCMethod = false, foreignExceptionMode) val callee = expression.symbol.owner @@ -307,7 +311,13 @@ internal fun KotlinStubs.generateObjCCall( receiver: ObjCCallReceiver, arguments: List ) = builder.irBlock { - val callBuilder = KotlinToCCallBuilder(builder, this@generateObjCCall, isObjCMethod = true) + val resolved = method.resolveFakeOverride(allowAbstract = true)?: method + val exceptionMode = ForeignExceptionMode.byValue( + resolved.module.konanLibrary?.manifestProperties + ?.getProperty(ForeignExceptionMode.manifestKey) + ) + + val callBuilder = KotlinToCCallBuilder(builder, this@generateObjCCall, isObjCMethod = true, exceptionMode) val superClass = irTemporaryVar( superQualifier?.let { getObjCClass(symbols, it) } ?: irNullNativePtr(symbols) @@ -1327,7 +1337,7 @@ private class ObjCBlockPointerValuePassing( } private fun IrBuilderWithScope.callBlock(blockPtr: IrExpression, arguments: List): IrExpression { - val callBuilder = KotlinToCCallBuilder(this, stubs, isObjCMethod = false) + val callBuilder = KotlinToCCallBuilder(this, stubs, isObjCMethod = false, ForeignExceptionMode.default) val rawBlockPointerParameter = callBuilder.passThroughBridge(blockPtr, blockPtr.type, CTypes.id) val blockVariableName = "block" diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGenUtils.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGenUtils.kt index a48d0f086bb..dee663372ef 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGenUtils.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGenUtils.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.ir.types.impl.IrUninitializedType import org.jetbrains.kotlin.ir.util.constructors import org.jetbrains.kotlin.ir.util.irBuilder import org.jetbrains.kotlin.ir.util.irCatch +import org.jetbrains.kotlin.konan.ForeignExceptionMode import org.jetbrains.kotlin.name.Name internal class CFunctionBuilder { @@ -71,10 +72,11 @@ internal class KotlinBridgeBuilder( endOffset: Int, cName: String, stubs: KotlinStubs, - isExternal: Boolean + isExternal: Boolean, + foreignExceptionMode: ForeignExceptionMode.Mode ) { private var counter = 0 - private val bridge: IrFunction = createKotlinBridge(startOffset, endOffset, cName, stubs, isExternal) + private val bridge: IrFunction = createKotlinBridge(startOffset, endOffset, cName, stubs, isExternal, foreignExceptionMode) val irBuilder: IrBuilderWithScope = irBuilder(stubs.irBuiltIns, bridge.symbol).at(startOffset, endOffset) fun addParameter(type: IrType): IrValueParameter { @@ -105,7 +107,8 @@ private fun createKotlinBridge( endOffset: Int, cBridgeName: String, stubs: KotlinStubs, - isExternal: Boolean + isExternal: Boolean, + foreignExceptionMode: ForeignExceptionMode.Mode ): IrFunction { val bridgeDescriptor = WrappedSimpleFunctionDescriptor() val bridge = IrFunctionImpl( @@ -131,7 +134,8 @@ private fun createKotlinBridge( bridge.annotations += buildSimpleAnnotation(stubs.irBuiltIns, startOffset, endOffset, stubs.symbols.symbolName.owner, cBridgeName) bridge.annotations += buildSimpleAnnotation(stubs.irBuiltIns, startOffset, endOffset, - stubs.symbols.filterExceptions.owner) + stubs.symbols.filterExceptions.owner, + foreignExceptionMode.value) } else { bridge.annotations += buildSimpleAnnotation(stubs.irBuiltIns, startOffset, endOffset, stubs.symbols.exportForCppRuntime.owner, cBridgeName) @@ -144,9 +148,10 @@ internal class KotlinCBridgeBuilder( endOffset: Int, cName: String, stubs: KotlinStubs, - isKotlinToC: Boolean + isKotlinToC: Boolean, + foreignExceptionMode: ForeignExceptionMode.Mode = ForeignExceptionMode.default ) { - private val kotlinBridgeBuilder = KotlinBridgeBuilder(startOffset, endOffset, cName, stubs, isExternal = isKotlinToC) + private val kotlinBridgeBuilder = KotlinBridgeBuilder(startOffset, endOffset, cName, stubs, isExternal = isKotlinToC, foreignExceptionMode) private val cBridgeBuilder = CFunctionBuilder() val kotlinIrBuilder: IrBuilderWithScope get() = kotlinBridgeBuilder.irBuilder 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 420a658e2ce..fbe731a24b0 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 @@ -195,6 +195,8 @@ internal class KonanSymbols( val interopCreateNSStringFromKString = symbolTable.referenceSimpleFunction(context.interopBuiltIns.CreateNSStringFromKString) + val createForeignException = interopFunction("CreateForeignException") + val interopObjCGetSelector = interopFunction("objCGetSelector") val interopCEnumVar = interopClass("CEnumVar") diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt index 630e1e6454d..a3ec4aa2be8 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.descriptors.konan.CompiledKlibModuleOrigin import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall import org.jetbrains.kotlin.ir.expressions.IrGetObjectValue import org.jetbrains.kotlin.ir.expressions.IrReturn +import org.jetbrains.kotlin.konan.ForeignExceptionMode private fun IrConstructor.isAnyConstructorDelegation(context: Context): Boolean { val statements = this.body?.statements ?: return false @@ -703,23 +704,49 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, return LLVMBuildExtractElement(builder, vector, index, name)!! } - fun filteringExceptionHandler(codeContext: CodeContext): ExceptionHandler { + fun filteringExceptionHandler(codeContext: CodeContext, foreignExceptionMode: ForeignExceptionMode.Mode): ExceptionHandler { val lpBlock = basicBlockInFunction("filteringExceptionHandler", position()?.start) + val wrapExceptionMode = context.config.target.family.isAppleFamily && + foreignExceptionMode == ForeignExceptionMode.Mode.OBJC_WRAP + appendingTo(lpBlock) { val landingpad = gxxLandingpad(2) LLVMAddClause(landingpad, kotlinExceptionRtti.llvm) + if (wrapExceptionMode) { + LLVMAddClause(landingpad, objcNSExceptionRtti.llvm) + } LLVMAddClause(landingpad, LLVMConstNull(kInt8Ptr)) val fatalForeignExceptionBlock = basicBlock("fatalForeignException", position()?.start) val forwardKotlinExceptionBlock = basicBlock("forwardKotlinException", position()?.start) + val typeId = extractValue(landingpad, 1) val isKotlinException = icmpEq( - extractValue(landingpad, 1), + typeId, call(context.llvm.llvmEhTypeidFor, listOf(kotlinExceptionRtti.llvm)) ) - condBr(isKotlinException, forwardKotlinExceptionBlock, fatalForeignExceptionBlock) + if (wrapExceptionMode) { + val foreignExceptionBlock = basicBlock("foreignException", position()?.start) + val forwardNativeExceptionBlock = basicBlock("forwardNativeException", position()?.start) + + condBr(isKotlinException, forwardKotlinExceptionBlock, foreignExceptionBlock) + appendingTo(foreignExceptionBlock) { + val isObjCException = icmpEq( + typeId, + call(context.llvm.llvmEhTypeidFor, listOf(objcNSExceptionRtti.llvm)) + ) + condBr(isObjCException, forwardNativeExceptionBlock, fatalForeignExceptionBlock) + + appendingTo(forwardNativeExceptionBlock) { + val exception = createForeignException(landingpad, codeContext.exceptionHandler) + codeContext.genThrow(exception) + } + } + } else { + condBr(isKotlinException, forwardKotlinExceptionBlock, fatalForeignExceptionBlock) + } appendingTo(forwardKotlinExceptionBlock) { // Rethrow Kotlin exception to real handler. @@ -731,6 +758,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, call(context.llvm.cxaBeginCatchFunction, listOf(exceptionRecord)) terminate() } + } return object : ExceptionHandler.Local() { @@ -770,7 +798,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, LLVMAddClause(landingpadResult, LLVMConstNull(kInt8Ptr)) - // FIXME: properly handle C++ exceptions: currently C++ exception can be thrown out from try-finally + // TODO: properly handle C++ exceptions: currently C++ exception can be thrown out from try-finally // bypassing the finally block. return extractKotlinException(landingpadResult) @@ -797,6 +825,21 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, return exceptionPtr } + private fun createForeignException(landingpadResult: LLVMValueRef, exceptionHandler: ExceptionHandler): LLVMValueRef { + val exceptionRecord = extractValue(landingpadResult, 0, "er") + + // __cxa_begin_catch returns pointer to C++ exception object. + val exceptionRawPtr = call(context.llvm.cxaBeginCatchFunction, listOf(exceptionRecord)) + + // This will take care of ARC - need to be done in the catching scope, i.e. before __cxa_end_catch + val exception = call(context.ir.symbols.createForeignException.owner.llvmFunction, + listOf(exceptionRawPtr), + Lifetime.GLOBAL, exceptionHandler) + + call(context.llvm.cxaEndCatchFunction, listOf()) + return exception + } + inline fun ifThenElse( condition: LLVMValueRef, thenValue: LLVMValueRef, @@ -1273,6 +1316,14 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, origin = context.stdlibModule.llvmSymbolOrigin )).bitcast(int8TypePtr) + private val objcNSExceptionRtti: ConstPointer by lazy { + constPointer(importGlobal( + "OBJC_EHTYPE_\$_NSException", // typeinfo for NSException* + int8TypePtr, + origin = context.stdlibModule.llvmSymbolOrigin + )).bitcast(int8TypePtr) + } + //-------------------------------------------------------------------------// /** diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index a9810c8c5ed..4ac56fd8e7a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid +import org.jetbrains.kotlin.konan.ForeignExceptionMode import org.jetbrains.kotlin.konan.target.CompilerOutputKind import org.jetbrains.kotlin.konan.target.Family import org.jetbrains.kotlin.name.FqName @@ -2311,11 +2312,11 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map, resultLifetime: Lifetime): LLVMValueRef { - val exceptionHandler = if (function.hasAnnotation(RuntimeNames.filterExceptions)) { - functionGenerationContext.filteringExceptionHandler(currentCodeContext) - } else { - currentCodeContext.exceptionHandler - } + + val exceptionHandler = function.annotations.findAnnotation(RuntimeNames.filterExceptions)?.let { + val foreignExceptionMode = ForeignExceptionMode.byValue(it.getAnnotationValueOrNull("mode")) + functionGenerationContext.filteringExceptionHandler(currentCodeContext, foreignExceptionMode) + } ?: currentCodeContext.exceptionHandler val result = call(llvmFunction, args, resultLifetime, exceptionHandler) if (!function.isSuspend && function.returnType.isNothing()) { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt index 18ee1357fdb..4fabcaace8e 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.cgen.* import org.jetbrains.kotlin.backend.konan.descriptors.allOverriddenFunctions import org.jetbrains.kotlin.backend.konan.descriptors.isFromInteropLibrary +import org.jetbrains.kotlin.backend.konan.descriptors.konanLibrary import org.jetbrains.kotlin.backend.konan.descriptors.synthesizedName import org.jetbrains.kotlin.backend.konan.ir.* import org.jetbrains.kotlin.backend.konan.ir.companionObject @@ -50,6 +51,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.konan.ForeignExceptionMode internal class InteropLowering(context: Context) : FileLoweringPass { // TODO: merge these lowerings. @@ -1006,7 +1008,10 @@ private class InteropTransformer(val context: Context, override val irFile: IrFi if (function.annotations.hasAnnotation(RuntimeNames.cCall)) { context.llvmImports.add(function.llvmSymbolOrigin) - return generateWithStubs { generateCCall(expression, builder, isInvoke = false) } + val exceptionMode = ForeignExceptionMode.byValue( + function.module.konanLibrary?.manifestProperties?.getProperty(ForeignExceptionMode.manifestKey) + ) + return generateWithStubs { generateCCall(expression, builder, isInvoke = false, exceptionMode) } } val failCompilation = { msg: String -> context.reportCompilationError(msg, irFile, expression) } diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 728e63401e1..aa08502b283 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3626,6 +3626,22 @@ if (PlatformInfo.isAppleTarget(project)) { it.headers "$projectDir/interop/objc/msg_send/messaging.h" } + createInterop("foreignException") { + it.defFile 'interop/objc/foreignException/objc_wrap.def' + it.headers "$projectDir/interop/objc/foreignException/objc_wrap.h" + } + + createInterop("foreignExceptionMode_default") { + it.defFile 'interop/objc/foreignException/objcExceptionMode.def' + it.headers "$projectDir/interop/objc/foreignException/objc_wrap.h" + } + + createInterop("foreignExceptionMode_wrap") { + it.defFile 'interop/objc/foreignException/objcExceptionMode.def' + it.headers "$projectDir/interop/objc/foreignException/objc_wrap.h" + it.extraOpts '-Xforeign-exception-mode', "objc-wrap" + } + createInterop("objcKt34467") { it.defFile 'interop/objc/kt34467/module_library.def' def moduleMap = file("interop/objc/kt34467/module_library.modulemap").absolutePath @@ -3995,6 +4011,59 @@ if (PlatformInfo.isAppleTarget(project)) { } } + interopTest("interop_objc_foreignException") { + source = "interop/objc/foreignException/objc_wrap.kt" + interop = 'foreignException' + + doBeforeBuild { + mkdir(buildDir) + execKonanClang(project.target) { + args "$projectDir/interop/objc/foreignException/objc_wrap.m" + args "-lobjc", '-fobjc-arc' + args '-fPIC', '-shared', '-o', "$buildDir/libobjcexception.dylib" + } + if (project.target instanceof KonanTarget.IOS_X64) { + UtilsKt.codesign(project, "$buildDir/libobjcexception.dylib") + } + } + } + + interopTest("interop_objc_foreignExceptionMode_wrap") { + source = "interop/objc/foreignException/objcExceptionMode.kt" + interop = 'foreignExceptionMode_wrap' + goldValue = "OK: ForeignException\n" + + doBeforeBuild { + mkdir(buildDir) + execKonanClang(project.target) { + args "$projectDir/interop/objc/foreignException/objc_wrap.m" + args "-lobjc", '-fobjc-arc' + args '-fPIC', '-shared', '-o', "$buildDir/libobjcexception.dylib" + } + if (project.target instanceof KonanTarget.IOS_X64) { + UtilsKt.codesign(project, "$buildDir/libobjcexception.dylib") + } + } + } + + interopTest("interop_objc_foreignExceptionMode_default") { + source = "interop/objc/foreignException/objcExceptionMode.kt" + interop = 'foreignExceptionMode_default' + goldValue = "OK: Ends with uncaught exception handler\n" + + doBeforeBuild { + mkdir(buildDir) + execKonanClang(project.target) { + args "$projectDir/interop/objc/foreignException/objc_wrap.m" + args "-lobjc", '-fobjc-arc' + args '-fPIC', '-shared', '-o', "$buildDir/libobjcexception.dylib" + } + if (project.target instanceof KonanTarget.IOS_X64) { + UtilsKt.codesign(project, "$buildDir/libobjcexception.dylib") + } + } + } + interopTest("interop_objc_kt34467") { source = "interop/objc/kt34467/foo.kt" interop = 'objcKt34467' diff --git a/backend.native/tests/interop/objc/allocException.kt b/backend.native/tests/interop/objc/allocException.kt index eedb5c9d0f5..1f028457ce5 100644 --- a/backend.native/tests/interop/objc/allocException.kt +++ b/backend.native/tests/interop/objc/allocException.kt @@ -18,7 +18,13 @@ fun exc_handler(x: Any?) : Unit { } fun main() { + // This does not work anymore, as NSException propagated as ForeignException + // so we got `Uncaught Kotlin exception` instead. Use normal try/catch. objc_setUncaughtExceptionHandler(staticCFunction(::exc_handler)) - println(NSJSONSerialization()) + try { + println(NSJSONSerialization()) + } catch (e: Exception) { // ForeignException expected + println(e) + } } diff --git a/backend.native/tests/interop/objc/foreignException/objcExceptionMode.def b/backend.native/tests/interop/objc/foreignException/objcExceptionMode.def new file mode 100644 index 00000000000..a4e99951650 --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objcExceptionMode.def @@ -0,0 +1,3 @@ +language = Objective-C +headerFilter = **/objc_wrap.h +linkerOpts = -lobjcexception diff --git a/backend.native/tests/interop/objc/foreignException/objcExceptionMode.kt b/backend.native/tests/interop/objc/foreignException/objcExceptionMode.kt new file mode 100644 index 00000000000..722b614772f --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objcExceptionMode.kt @@ -0,0 +1,47 @@ +/* + * Test different behavior depending on foreignExceptionMode option + */ + +import kotlin.test.* +import objcExceptionMode.* +import kotlinx.cinterop.* +import platform.objc.* +import kotlin.system.exitProcess + +@Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER") +@Test fun testKT35056() { + val name = "Some native exception" + val reason = "Illegal value" + var finallyBlockTest = "FAILED" + var catchBlockTest = "FAILED" + try { + raiseExc(name, reason) + assertNotEquals("FAILED", catchBlockTest) // shall not get here anyway + } catch (e: ForeignException) { + val ret = logExc(e.nativeException) // return NSException name + assertEquals(name, ret) + assertEquals("$name:: $reason", e.message) + println("OK: ForeignException") + catchBlockTest = "PASSED" + } finally { + finallyBlockTest = "PASSED" + } + assertEquals("PASSED", catchBlockTest) + assertEquals("PASSED", finallyBlockTest) +} + +@Suppress("UNUSED_PARAMETER") +fun abnormal_handler(x: Any?) : Unit { + println("OK: Ends with uncaught exception handler") + exitProcess(0) +} + +fun main() { + // Depending on the `foreignxceptionMode` option (def file or cinterop cli) this test should ends + // normally with `ForeignException` handled or abnormally with `abnormal_handler`. + // Test shall validate output (golden value) from `abnormal_handler`. + + objc_setUncaughtExceptionHandler(staticCFunction(::abnormal_handler)) + + testKT35056() +} \ No newline at end of file diff --git a/backend.native/tests/interop/objc/foreignException/objc_wrap.def b/backend.native/tests/interop/objc/foreignException/objc_wrap.def new file mode 100644 index 00000000000..48416e6d2d0 --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objc_wrap.def @@ -0,0 +1,4 @@ +language = Objective-C +headerFilter = **/objc_wrap.h +linkerOpts = -lobjcexception +foreignExceptionMode = objc-wrap diff --git a/backend.native/tests/interop/objc/foreignException/objc_wrap.h b/backend.native/tests/interop/objc/foreignException/objc_wrap.h new file mode 100644 index 00000000000..384a558bbaa --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objc_wrap.h @@ -0,0 +1,9 @@ +#import + +void raiseExc(id name, id reason); +id logExc(id exception); + +@interface Foo : NSObject +- (void)instanceMethodThrow:(id)name reason:(id)reason; ++ (void)classMethodThrow:(id)name reason:(id)reason; +@end diff --git a/backend.native/tests/interop/objc/foreignException/objc_wrap.kt b/backend.native/tests/interop/objc/foreignException/objc_wrap.kt new file mode 100644 index 00000000000..1d71721afb6 --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objc_wrap.kt @@ -0,0 +1,53 @@ +/* + * Test different types of callable with foreignExceptionMode=objc-wrap + */ + +import kotlin.test.* +//import objcTests.* +import objc_wrap.* +import kotlinx.cinterop.* + +fun testInner(name: String, reason: String) { + var finallyBlockTest = "FAILED" + var catchBlockTest = "NOT EXPECTED" + try { + raiseExc(name, reason) + } catch (e: RuntimeException) { + catchBlockTest = "This shouldn't happen" + } finally { + finallyBlockTest = "PASSED" + } + assertEquals("NOT EXPECTED", catchBlockTest) + assertEquals("PASSED", finallyBlockTest) +} + +typealias CallMe = (String, String) -> Unit + +@Test fun testExceptionWrap(raise: CallMe) { + val name = "Some native exception" + val reason = "Illegal value" + var finallyBlockTest = "FAILED" + var catchBlockTest = "FAILED" + try { + raise(name, reason) + } catch (e: ForeignException) { + val ret = logExc(e.nativeException) // return NSException name + assertEquals(name, ret) + assertEquals("$name:: $reason", e.message) + catchBlockTest = "PASSED" + } finally { + finallyBlockTest = "PASSED" + } + assertEquals("PASSED", catchBlockTest) + assertEquals("PASSED", finallyBlockTest) +} + +class Bar() : Foo() + +fun main() { + testExceptionWrap(::raiseExc) // simple + testExceptionWrap(::testInner) // nested try block + testExceptionWrap(Foo::classMethodThrow) // class method + testExceptionWrap(Foo()::instanceMethodThrow) // instance method + testExceptionWrap(Bar()::instanceMethodThrow) // fake override +} \ No newline at end of file diff --git a/backend.native/tests/interop/objc/foreignException/objc_wrap.m b/backend.native/tests/interop/objc/foreignException/objc_wrap.m new file mode 100644 index 00000000000..d77e6de0ab3 --- /dev/null +++ b/backend.native/tests/interop/objc/foreignException/objc_wrap.m @@ -0,0 +1,21 @@ +#include "objc_wrap.h" + +#import + +void raiseExc(id name, id reason) { + [NSException raise:name format:@"%@", reason]; +} + +id logExc(id exc) { + assert([exc isKindOfClass:[NSException class]]); + return ((NSException*)exc).name; +} + +@implementation Foo : NSObject +- (void) instanceMethodThrow:(id)name reason:(id)reason { + raiseExc(name, reason); +} ++ (void) classMethodThrow:(id)name reason:(id)reason { + raiseExc(name, reason); +} +@end diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index af7427bd477..af742d9977a 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -69,6 +69,8 @@ add_executable(runtime src/main/cpp/ObjCInteropUtils.mm src/main/cpp/ObjCExportCollectionUtils.mm src/main/cpp/ObjCExportErrors.mm + src/main/cpp/ObjCExportExceptionDetails.mm + src/objc/cpp/ObjCExportNumbers.mm src/objc/cpp/ObjCExportClasses.mm src/objc/cpp/ObjCExportCollections.mm diff --git a/runtime/src/main/cpp/ObjCExportExceptionDetails.h b/runtime/src/main/cpp/ObjCExportExceptionDetails.h new file mode 100644 index 00000000000..36cae5c2890 --- /dev/null +++ b/runtime/src/main/cpp/ObjCExportExceptionDetails.h @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Memory.h" +#import "Types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +OBJ_GETTER(Kotlin_ObjCExport_ExceptionDetails, KRef thiz, KRef exceptionHolder); + +#ifdef __cplusplus +} +#endif diff --git a/runtime/src/main/cpp/ObjCExportExceptionDetails.mm b/runtime/src/main/cpp/ObjCExportExceptionDetails.mm new file mode 100644 index 00000000000..dcb5795dea7 --- /dev/null +++ b/runtime/src/main/cpp/ObjCExportExceptionDetails.mm @@ -0,0 +1,42 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Memory.h" +#import "ObjCExportExceptionDetails.h" +#import "ObjCExport.h" + +#if KONAN_OBJC_INTEROP + +#import + +//! TODO: Use not_null signature. +OBJ_GETTER(Kotlin_ObjCExport_ExceptionDetails, KRef /*thiz*/, KRef exceptionHolder) { + if (NSException* exception = (NSException*)Kotlin_ObjCExport_refToObjC(exceptionHolder)) { + RuntimeAssert([exception isKindOfClass:[NSException class]], "Illegal type: NSException expected"); + NSString* ret = [NSString stringWithFormat: @"%@:: %@", exception.name, exception.reason]; + RETURN_RESULT_OF(Kotlin_Interop_CreateKStringFromNSString, ret); + } + + RETURN_OBJ(nullptr); +} + +#else // KONAN_OBJC_INTEROP + +OBJ_GETTER(Kotlin_ObjCExport_ExceptionDetails, KRef /*thiz*/, KRef /*exceptionHolder*/) { + RETURN_OBJ(nullptr); +} + +#endif diff --git a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt index 7483192b71a..432e416597f 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt @@ -116,7 +116,7 @@ annotation class Independent */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) -@PublishedApi internal annotation class FilterExceptions +@PublishedApi internal annotation class FilterExceptions(val mode: String = "terminate") /** * Marks a class whose instances to be added to the list of leak detector candidates. diff --git a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/Exceptions.kt b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/Exceptions.kt index ae5b7c217a5..9cf9728e547 100644 --- a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/Exceptions.kt +++ b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/Exceptions.kt @@ -31,3 +31,22 @@ class KonanExternalToolFailure(message: String, val toolName: String, cause: Thr */ class MissingXcodeException(message: String, cause: Throwable? = null) : KonanException(message, cause) +/** + * Native exception handling in Kotlin: terminate, wrap, etc. + * Foreign exceptionMode mode is per library option: controlled by cinterop command-line option or def file property + * than stored in klib manifest and used by compiler to generate appropriate handler. + */ +class ForeignExceptionMode { + companion object { + val manifestKey = "foreignExceptionMode" + val default = Mode.TERMINATE + fun byValue(value: String?): Mode = value?.let { + Mode.values().find { it.value == value } + ?: throw IllegalArgumentException("Illegal ForeignExceptionMode $value") + } ?: default + } + enum class Mode(val value: String) { + TERMINATE("terminate"), + OBJC_WRAP("objc-wrap") + } +} diff --git a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/util/DefFile.kt b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/util/DefFile.kt index f98f0a8dc9e..b0a76b3f8f3 100644 --- a/shared/src/main/kotlin/org/jetbrains/kotlin/konan/util/DefFile.kt +++ b/shared/src/main/kotlin/org/jetbrains/kotlin/konan/util/DefFile.kt @@ -113,6 +113,11 @@ class DefFile(val file:File?, val config:DefFileConfig, val manifestAddendProper val disableDesignatedInitializerChecks by lazy { properties.getProperty("disableDesignatedInitializerChecks")?.toBoolean() ?: false } + + val foreignExceptionMode by lazy { + properties.getProperty("foreignExceptionMode") + } + } }