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.
|
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
|
||||||
|
|||||||
+47
-4
@@ -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
|
||||||
|
|
||||||
|
|||||||
-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) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user