Rework @Throws

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