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.
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
with a `@Throws` annotation. In this case all Kotlin exceptions
(except for instances of `Error`, `RuntimeException` and subclasses) are translated into
a Swift error/`NSError`.
with a `@Throws` annotation specifying a list of "expected" exception classes.
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:
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.CurrentKlibModuleOrigin
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.types.*
import org.jetbrains.kotlin.ir.util.*
@@ -375,6 +377,9 @@ internal class ObjCExportCodeGenerator(
internal val directMethodAdapters = mutableMapOf<DirectAdapterRequest, ObjCToKotlinMethodAdapter>()
internal val exceptionTypeInfoArrays = mutableMapOf<IrFunction, ConstPointer>()
internal val typeInfoArrays = mutableMapOf<Set<IrClass>, ConstPointer>()
inner class ObjCToKotlinMethodAdapter(
selector: String,
encoding: String,
@@ -702,12 +707,17 @@ private fun ObjCExportCodeGenerator.generateAbstractObjCImp(methodBridge: Method
private fun ObjCExportCodeGenerator.generateObjCImp(
target: IrFunction?,
baseMethod: IrFunction,
methodBridge: MethodBridge,
isVirtual: Boolean = false
) = if (target == null) {
generateAbstractObjCImp(methodBridge)
} else {
generateObjCImp(methodBridge, isDirect = !isVirtual) { args, resultLifetime, exceptionHandler ->
generateObjCImp(
methodBridge,
isDirect = !isVirtual,
baseMethod = baseMethod
) { args, resultLifetime, exceptionHandler ->
val llvmTarget = if (!isVirtual) {
codegen.llvmFunction(target)
} else {
@@ -721,6 +731,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
private fun ObjCExportCodeGenerator.generateObjCImp(
methodBridge: MethodBridge,
isDirect: Boolean,
baseMethod: IrFunction? = null,
callKotlin: FunctionGenerationContext.(
args: List<LLVMValueRef>,
resultLifetime: Lifetime,
@@ -783,7 +794,7 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
kotlinExceptionHandler { exception ->
callFromBridge(
context.llvm.Kotlin_ObjCExport_RethrowExceptionAsNSError,
listOf(exception, errorOutPtr!!)
listOf(exception, errorOutPtr!!, generateExceptionTypeInfoArray(baseMethod!!))
)
val returnValue = when (returnType) {
@@ -830,6 +841,38 @@ private fun ObjCExportCodeGenerator.generateObjCImp(
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(
target: IrConstructor,
methodBridge: MethodBridge
@@ -1016,7 +1059,7 @@ private fun ObjCExportCodeGenerator.createMethodVirtualAdapter(
val selector = namer.getSelector(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
@@ -1042,7 +1085,7 @@ private fun ObjCExportCodeGenerator.createMethodAdapter(
val selectorName = namer.getSelector(request.base.descriptor)
val methodBridge = mapper.bridgeMethod(request.base.descriptor)
val objCEncoding = getEncoding(methodBridge)
val objCToKotlin = constPointer(generateObjCImp(request.implementation, methodBridge))
val objCToKotlin = constPointer(generateObjCImp(request.implementation, request.base, 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) {
if (!method.kind.isReal) 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
for (argument in arguments) {
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)
}
}
+27 -23
View File
@@ -28,33 +28,44 @@
extern "C" OBJ_GETTER(Kotlin_Throwable_getMessage, 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) {
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) {
if (Kotlin_ObjCExport_isUnchecked(exception)) {
printlnMessage(uncheckedExceptionMessage);
printlnMessage("Other exceptions can be propagated as NSError if method has or inherits @Throws annotation.");
} else {
printlnMessage("Exceptions are propagated from Kotlin to Objective-C/Swift as NSError "
"only if method has or inherits @Throws annotation");
}
printlnMessage("Function doesn't have or inherit @Throws annotation and thus exception 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);
}
static char kotlinExceptionOriginChar;
extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id* outError) {
if (Kotlin_ObjCExport_isUnchecked(exception)) {
printlnMessage(uncheckedExceptionMessage);
static bool isExceptionOfType(KRef exception, const TypeInfo** types) {
for (int i = 0; types[i] != nullptr; ++i) {
// 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);
}
@@ -62,18 +73,11 @@ extern "C" void Kotlin_ObjCExport_RethrowExceptionAsNSError(KRef exception, id*
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];
userInfo[@"KotlinException"] = Kotlin_ObjCExport_refToObjC(exception);
userInfo[@"KotlinExceptionOrigin"] = @(&kotlinExceptionOriginChar); // Support for different Kotlin runtimes loaded.
ObjHolder messageHolder;
KRef message = Kotlin_Throwable_getMessage(exception, messageHolder.slot());
NSString* description = Kotlin_Interop_CreateNSStringFromKString(message);
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.
*
* When compiling to Objective-C/Swift framework, methods having or inheriting this annotation are represented as
* `NSError*`-producing methods in Objective-C and as `throws` methods in Swift.
* When such a method called through framework API throws an exception, it is either propagated as
* `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 compiling to Objective-C/Swift framework, functions having or inheriting
* this 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 [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.
*/
@@ -280,10 +280,6 @@ class ObjCErrorException(
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
@SymbolName("Kotlin_ObjCExport_trapOnUndeclaredException")
@ExportForCppRuntime