Use ForeignException wrapper to handle native exception [KT-35056] (#4307)

* Use ForeignException wrapper to handle native exception [KT-35056]

* Add test

* Use Any? instead of NativePtr for automagic memory management

* Use clauseNSException specific clause, similar to `catch (NSException* e)

* cleanup + test fix

* Terminate on anything else but NSException

* Use ForeignException message for NSException details

* Code generated for AppleFamily only + move stuff to kotlinx.cinterop package

* Cleanup

* Fix platform-dependent code generation

* Fix test allocException

* wip: ForeignExceptionMode option: cinterop part

* ForeignExceptionMode support in compiler

* rework test for foreignExceptionMode option

* More tests on foreignExceptionMode option

* PR feedback: resolveFakeOverride and more

* Test reworked and extended

Co-authored-by: Vladimir Ivanov <vladimir.d.ivanov@jetbrains.com>
This commit is contained in:
Vladimir Ivanov
2020-09-14 13:50:32 +03:00
committed by GitHub
parent 0e6bfa062a
commit c4ce945262
24 changed files with 441 additions and 26 deletions
@@ -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<Any?>(payload))
@@ -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: <terminate|objc-wrap>")
}
class JSInteropArguments(argParser: ArgParser = ArgParser("jsinterop",
@@ -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) }
@@ -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 {
@@ -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<String>()
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<IrExpression?>
) = 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>): 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"
@@ -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
@@ -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")
@@ -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)
}
//-------------------------------------------------------------------------//
/**
@@ -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<IrE
private fun call(function: IrFunction, llvmFunction: LLVMValueRef, args: List<LLVMValueRef>,
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<String>("mode"))
functionGenerationContext.filteringExceptionHandler(currentCodeContext, foreignExceptionMode)
} ?: currentCodeContext.exceptionHandler
val result = call(llvmFunction, args, resultLifetime, exceptionHandler)
if (!function.isSuspend && function.returnType.isNothing()) {
@@ -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) }
+69
View File
@@ -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'
@@ -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)
}
}
@@ -0,0 +1,3 @@
language = Objective-C
headerFilter = **/objc_wrap.h
linkerOpts = -lobjcexception
@@ -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()
}
@@ -0,0 +1,4 @@
language = Objective-C
headerFilter = **/objc_wrap.h
linkerOpts = -lobjcexception
foreignExceptionMode = objc-wrap
@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
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
@@ -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
}
@@ -0,0 +1,21 @@
#include "objc_wrap.h"
#import <Foundation/Foundation.h>
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
+2
View File
@@ -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
@@ -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
@@ -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 <Foundation/NSException.h>
//! 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
@@ -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.
@@ -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")
}
}
@@ -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")
}
}
}