Rework @Throws
It now enables propagation only for instances of listed classes.
This commit is contained in:
committed by
SvyatoslavScherbina
parent
1d70b5d625
commit
85b9200bc2
+10
-3
@@ -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
|
||||
|
||||
+47
-4
@@ -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
|
||||
|
||||
|
||||
-16
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user