From 4d6614e6742a13286de5e6baf2c1ffef4c474edd Mon Sep 17 00:00:00 2001 From: SvyatoslavScherbina Date: Fri, 22 Feb 2019 13:51:08 +0300 Subject: [PATCH] Use compiler-generated C stubs when overriding some Objective-C methods (#2697) * Add tests to interop_objc_smoke * Use compiler-generated C stubs when overriding some Objective-C methods --- .../kotlin/kotlinx/cinterop/ObjectiveCImpl.kt | 2 +- .../kotlinx/cinterop/internal/Annotations.kt | 12 ++ .../kotlin/native/interop/gen/ObjCStubs.kt | 32 +++- .../kotlin/backend/konan/ObjCInterop.kt | 4 +- .../kotlin/backend/konan/cgen/CBridgeGen.kt | 168 ++++++++++++++---- .../jetbrains/kotlin/backend/konan/ir/Ir.kt | 20 +-- .../llvm/KotlinObjCClassInfoGenerator.kt | 31 ++-- .../backend/konan/lower/InteropLowering.kt | 28 ++- backend.native/tests/build.gradle | 2 +- backend.native/tests/interop/objc/smoke.h | 50 +++++- backend.native/tests/interop/objc/smoke.kt | 24 +++ backend.native/tests/interop/objc/smoke.m | 8 + 12 files changed, 311 insertions(+), 70 deletions(-) diff --git a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ObjectiveCImpl.kt b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ObjectiveCImpl.kt index 7a6e320721d..d343c6427b7 100644 --- a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ObjectiveCImpl.kt +++ b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ObjectiveCImpl.kt @@ -106,7 +106,7 @@ annotation class ObjCMethod(val selector: String, val bridge: String) @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) -annotation class ObjCBridge(val selector: String, val encoding: String, val imp: String) +annotation class ObjCBridge(val selector: String, val encoding: String, val imp: String = "") @Target(AnnotationTarget.CONSTRUCTOR) @Retention(AnnotationRetention.BINARY) diff --git a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Annotations.kt b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Annotations.kt index c613f4700cf..c21d5c01aea 100644 --- a/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Annotations.kt +++ b/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/internal/Annotations.kt @@ -14,4 +14,16 @@ public annotation class CCall(val id: String) { @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.BINARY) annotation class WCString + + @Target(AnnotationTarget.FUNCTION) + @Retention(AnnotationRetention.BINARY) + annotation class ReturnsRetained + + @Target(AnnotationTarget.FUNCTION) + @Retention(AnnotationRetention.BINARY) + annotation class ConsumesReceiver + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.BINARY) + annotation class Consumed } diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt index 04ebdefc8d9..353e7ff87ee 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt @@ -19,6 +19,7 @@ package org.jetbrains.kotlin.native.interop.gen import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.native.interop.gen.jvm.StubGenerator import org.jetbrains.kotlin.native.interop.indexer.* +import org.jetbrains.kotlin.utils.addIfNotNull private fun ObjCMethod.getKotlinParameterNames(forConstructorOrFactory: Boolean = false): List { val selectorParts = this.selector.split(":") @@ -74,6 +75,8 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, if (context.nativeBridges.isSupported(this)) { val result = mutableListOf() result.add("@ObjCMethod".applyToStrings(method.selector, bridgeName)) + if (method.nsConsumesSelf) result.add("@CCall.ConsumesReceiver") + if (method.nsReturnsRetained) result.add("@CCall.ReturnsRetained") result.add(header) if (method.isInit) { @@ -136,9 +139,16 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, } } + val objCBridge = "@ObjCBridge".applyToStrings( + *mutableListOf().apply { + add(method.selector) + add(method.encoding) + addIfNotNull(implementationTemplate) + }.toTypedArray() + ) + context.addTopLevelDeclaration( - listOf("@kotlin.native.internal.ExportForCompiler", - "@ObjCBridge".applyToStrings(method.selector, method.encoding, implementationTemplate)) + listOf("@kotlin.native.internal.ExportForCompiler", objCBridge) + block(bridgeHeader, bodyLines) ) @@ -154,7 +164,7 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, private val kotlinParameters: List private val kotlinReturnType: String private val header: String - private val implementationTemplate: String + private val implementationTemplate: String? internal val bridgeName: String private val bridgeHeader: String @@ -194,7 +204,8 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, val name = kotlinParameterNames[index] val kotlinType = stubGenerator.mirror(it.type).argType - kotlinParameters.add(KotlinParameter(name, kotlinType)) + val annotatedName = if (it.nsConsumed) "@CCall.Consumed $name" else name + kotlinParameters.add(KotlinParameter(annotatedName, kotlinType)) kotlinObjCBridgeParameters.add(KotlinParameter(name, kotlinType)) nativeBridgeArguments.add(TypedKotlinValue(it.type, name.asSimpleName())) @@ -233,7 +244,12 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, } bodyGenerator.returnResult(result) - this.implementationTemplate = genImplementationTemplate(stubGenerator) + this.implementationTemplate = if (needsImplementationTemplate()) { + genImplementationTemplate(stubGenerator) + } else { + null + } + this.bodyLines = bodyGenerator.build() bridgeName = "objcKniBridge${stubGenerator.nextUniqueId()}" @@ -277,6 +293,10 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, } } + private fun needsImplementationTemplate(): Boolean = + method.getReturnType(container.classOrProtocol).isBlockPointer() || + method.parameters.any { it.type.isBlockPointer() } + private fun genImplementationTemplate(stubGenerator: StubGenerator): String = when (container) { is ObjCClassOrProtocol -> { val codeBuilder = NativeCodeBuilder(stubGenerator.simpleBridgeGenerator.topLevelNativeScope) @@ -289,6 +309,8 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, } } +private fun Type.isBlockPointer() = this.unwrapTypedefs() is ObjCBlockPointer + private fun deprecatedInit(className: String, initParameterNames: List, factory: Boolean): String { val replacement = if (factory) "$className.create" else className val replacementKind = if (factory) "factory method" else "constructor" diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ObjCInterop.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ObjCInterop.kt index 9f59154b817..5ab21bfc5ad 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ObjCInterop.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ObjCInterop.kt @@ -95,7 +95,7 @@ fun IrClass.isKotlinObjCClass(): Boolean = this.isObjCClass() && !this.isExterna data class ObjCMethodInfo(val bridge: FunctionDescriptor, val selector: String, val encoding: String, - val imp: String) + val imp: String?) private fun CallableDescriptor.getBridgeAnnotation() = this.annotations.findAnnotation(objCBridgeFqName) @@ -121,7 +121,7 @@ private fun objCMethodInfoByBridge(packageView: PackageViewDescriptor, bridgeNam bridge = bridge, selector = bridgeAnnotation.getStringValue("selector"), encoding = bridgeAnnotation.getStringValue("encoding"), - imp = bridgeAnnotation.getStringValue("imp") + imp = bridgeAnnotation.getStringValueOrNull("imp") ) } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt index e7c2423d8b1..6460c2e48f5 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt @@ -93,7 +93,7 @@ internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWit val cFunctionBuilder = callBuilder.cFunctionBuilder - val callee = expression.symbol.owner + val callee = expression.symbol.owner as IrSimpleFunction // TODO: consider computing all arguments before converting. @@ -148,9 +148,9 @@ internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWit val returnValuePassing = if (isInvoke) { val returnType = expression.getTypeArgument(expression.typeArgumentsCount - 1)!! - mapReturnType(returnType, TypeLocation.FunctionCallResult(expression)) + mapReturnType(returnType, TypeLocation.FunctionCallResult(expression), signature = null) } else { - mapReturnType(callee.returnType, TypeLocation.FunctionCallResult(expression)) + mapReturnType(callee.returnType, TypeLocation.FunctionCallResult(expression), signature = callee) } val result = with(returnValuePassing) { @@ -176,7 +176,8 @@ internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWit private class CCallbackBuilder( val stubs: KotlinStubs, - val location: IrElement + val location: IrElement, + val isObjCMethod: Boolean ) { val irBuiltIns: IrBuiltIns get() = stubs.irBuiltIns @@ -205,18 +206,29 @@ private fun CCallbackBuilder.passThroughBridge( return bridgeBuilder.addParameter(kotlinBridgeParameterType, cBridgeParameterType).first } -private fun CCallbackBuilder.addParameter(it: IrValueParameter) { - val typeLocation = TypeLocation.FunctionPointerParameter(cFunctionBuilder.numberOfParameters, location) - val valuePassing = stubs.mapType(it.type, typeLocation) +private fun CCallbackBuilder.addParameter(it: IrValueParameter, functionParameter: IrValueParameter) { + val typeLocation = if (isObjCMethod) { + TypeLocation.ObjCMethodParameter(it.index, functionParameter) + } else { + TypeLocation.FunctionPointerParameter(cFunctionBuilder.numberOfParameters, location) + } + + val valuePassing = mapParameter(it, typeLocation) val kotlinArgument = with(valuePassing) { receiveValue() } kotlinCallBuilder.arguments += kotlinArgument } -private fun CCallbackBuilder.build(function: IrSimpleFunction, signature: IrFunction): String { +private fun CCallbackBuilder.build(function: IrSimpleFunction, signature: IrSimpleFunction): String { val kotlinCall = kotlinCallBuilder.build(function) - with(stubs.mapReturnType(signature.returnType, TypeLocation.FunctionPointerReturnValue(location))) { + val typeLocation = if (isObjCMethod) { + TypeLocation.ObjCMethodReturnValue(function) + } else { + TypeLocation.FunctionPointerReturnValue(location) + } + + with(stubs.mapReturnType(signature.returnType, typeLocation, signature)) { returnValue(kotlinCall) } @@ -246,18 +258,25 @@ private fun KotlinStubs.generateCFunction( isObjCMethod: Boolean, location: IrElement ): String { - val callbackBuilder = CCallbackBuilder(this, location) + val callbackBuilder = CCallbackBuilder(this, location, isObjCMethod) - signature.dispatchReceiverParameter?.let { callbackBuilder.addParameter(it) } if (isObjCMethod) { -// assert(mapType(signature.dispatchReceiverParameter!!.type) is ObjCReferenceValuePassing) + val receiver = signature.dispatchReceiverParameter!! + assert(isObjCReferenceType(receiver.type)) + val valuePassing = ObjCReferenceValuePassing(symbols, receiver.type, retained = signature.consumesReceiver()) + val kotlinArgument = with(valuePassing) { callbackBuilder.receiveValue() } + callbackBuilder.kotlinCallBuilder.arguments += kotlinArgument + // Selector is ignored: with(TrivialValuePassing(symbols.nativePtrType, CTypes.voidPtr)) { callbackBuilder.receiveValue() } + } else { + require(signature.dispatchReceiverParameter == null) } - signature.extensionReceiverParameter?.let { callbackBuilder.addParameter(it) } + + signature.extensionReceiverParameter?.let { callbackBuilder.addParameter(it, function.extensionReceiverParameter!!) } signature.valueParameters.forEach { - callbackBuilder.addParameter(it) + callbackBuilder.addParameter(it, function.valueParameters[it.index]) } return callbackBuilder.build(function, signature) @@ -266,11 +285,14 @@ private fun KotlinStubs.generateCFunction( internal fun KotlinStubs.generateCFunctionPointer( function: IrSimpleFunction, signature: IrSimpleFunction, - isObjCMethod: Boolean, expression: IrExpression ): IrExpression { - val cFunction = generateCFunction(function, signature, isObjCMethod, expression) - val fakeFunction = createFakeKotlinExternalFunction(signature, cFunction, isObjCMethod) + val fakeFunction = generateCFunctionAndFakeKotlinExternalFunction( + function, + signature, + isObjCMethod = false, + location = expression + ) addKotlin(fakeFunction) return IrFunctionReferenceImpl( @@ -283,6 +305,16 @@ internal fun KotlinStubs.generateCFunctionPointer( ) } +internal fun KotlinStubs.generateCFunctionAndFakeKotlinExternalFunction( + function: IrSimpleFunction, + signature: IrSimpleFunction, + isObjCMethod: Boolean, + location: IrElement +): IrSimpleFunction { + val cFunction = generateCFunction(function, signature, isObjCMethod, location) + return createFakeKotlinExternalFunction(signature, cFunction, isObjCMethod) +} + private fun KotlinStubs.createFakeKotlinExternalFunction( signature: IrSimpleFunction, cFunctionName: String, @@ -345,13 +377,20 @@ private fun IrType.isCEnumType(): Boolean { // TODO: get rid of consulting descriptors for annotations. // Make sure external stubs always get proper annotaions. -private fun IrValueParameter.isWCStringParameter() = - this.annotations.hasAnnotation(cCall.child(Name.identifier("WCString"))) || - this.descriptor.annotations.hasAnnotation(cCall.child(Name.identifier("WCString"))) +private fun IrDeclaration.hasCCallAnnotation(name: String): Boolean = + this.annotations.hasAnnotation(cCall.child(Name.identifier(name))) || + this.descriptor.annotations.hasAnnotation(cCall.child(Name.identifier(name))) -private fun IrValueParameter.isCStringParameter() = - this.annotations.hasAnnotation(cCall.child(Name.identifier("CString"))) || - this.descriptor.annotations.hasAnnotation(cCall.child(Name.identifier("CString"))) + +private fun IrValueParameter.isWCStringParameter() = hasCCallAnnotation("WCString") + +private fun IrValueParameter.isCStringParameter() = hasCCallAnnotation("CString") + +private fun IrValueParameter.isConsumed() = hasCCallAnnotation("Consumed") + +private fun IrSimpleFunction.consumesReceiver() = hasCCallAnnotation("ConsumesReceiver") + +private fun IrSimpleFunction.returnsRetained() = hasCCallAnnotation("ReturnsRetained") private fun getStructSpelling(kotlinClass: IrClass): String? = kotlinClass.getAnnotationArgumentValue(FqName("kotlinx.cinterop.internal.CStruct"), "spelling") @@ -393,17 +432,31 @@ private fun KotlinToCCallBuilder.mapParameter( } } +private fun CCallbackBuilder.mapParameter( + parameter: IrValueParameter, + typeLocation: TypeLocation +): ValuePassing = when { + stubs.isObjCReferenceType(parameter.type) -> + ObjCReferenceValuePassing(symbols, parameter.type, retained = parameter.isConsumed()) + + else -> stubs.mapType(parameter.type, typeLocation) +} + private sealed class TypeLocation(val element: IrElement) { class FunctionArgument(val argument: IrExpression) : TypeLocation(argument) class FunctionCallResult(val call: IrCall) : TypeLocation(call) class FunctionPointerParameter(val index: Int, element: IrElement) : TypeLocation(element) class FunctionPointerReturnValue(element: IrElement) : TypeLocation(element) + + class ObjCMethodParameter(val index: Int, element: IrElement) : TypeLocation(element) + class ObjCMethodReturnValue(element: IrElement) : TypeLocation(element) } -private fun KotlinStubs.mapReturnType(type: IrType, location: TypeLocation): ValueReturning = +private fun KotlinStubs.mapReturnType(type: IrType, location: TypeLocation, signature: IrSimpleFunction?): ValueReturning = when { type.isUnit() -> VoidReturning + isObjCReferenceType(type) -> ObjCReferenceValuePassing(symbols, type, signature?.returnsRetained() ?: false) else -> mapType(type, location) } @@ -448,7 +501,7 @@ private fun KotlinStubs.mapType(type: IrType, reportUnsupportedType: (String) -> } type.isFunction() -> reportUnsupportedType("") - isObjCReferenceType(type) -> ObjCReferenceValuePassing(symbols, type) + isObjCReferenceType(type) -> ObjCReferenceValuePassing(symbols, type, retained = false) else -> reportUnsupportedType("doesn't correspond to any C type") } @@ -456,16 +509,19 @@ private fun KotlinStubs.mapType(type: IrType, reportUnsupportedType: (String) -> private fun KotlinStubs.isObjCReferenceType(type: IrType): Boolean { if (target.family != Family.OSX && target.family != Family.IOS) return false + // Handle the same types as produced by [objCPointerMirror] in Interop/StubGenerator/.../Mappings.kt. + if (type.isObjCObjectType()) return true val descriptor = type.classifierOrNull?.descriptor ?: return false val builtIns = irBuiltIns.builtIns return when (descriptor) { + builtIns.any, builtIns.string, builtIns.list, builtIns.mutableList, - builtIns.set, builtIns.mutableSet, - builtIns.map, builtIns.mutableMap -> true + builtIns.set, + builtIns.map -> true else -> false } } @@ -489,7 +545,11 @@ private abstract class SimpleValuePassing : ValuePassing { abstract val kotlinBridgeType: IrType abstract val cBridgeType: CType abstract val cType: CType + abstract fun IrBuilderWithScope.kotlinToBridged(expression: IrExpression): IrExpression + open fun IrBuilderWithScope.kotlinCallbackResultToBridged(expression: IrExpression): IrExpression = + kotlinToBridged(expression) + abstract fun IrBuilderWithScope.bridgedToKotlin(expression: IrExpression, symbols: KonanSymbols): IrExpression abstract fun bridgedToC(expression: String): String abstract fun cToBridged(expression: String): String @@ -522,7 +582,7 @@ private abstract class SimpleValuePassing : ValuePassing { bridgeBuilder.setReturnType(kotlinBridgeType, cBridgeType) kotlinBridgeStatements += with(bridgeBuilder.kotlinIrBuilder) { - irReturn(kotlinToBridged(expression)) + irReturn(kotlinCallbackResultToBridged(expression)) } val cBridgeCall = buildCBridgeCall() cBodyLines += "return ${bridgedToC(cBridgeCall)};" @@ -689,7 +749,11 @@ private class CEnumValuePassing( override fun cToBridged(expression: String): String = with(baseValuePassing) { cToBridged(expression) } } -private class ObjCReferenceValuePassing(private val symbols: KonanSymbols, private val type: IrType) : SimpleValuePassing() { +private class ObjCReferenceValuePassing( + private val symbols: KonanSymbols, + private val type: IrType, + private val retained: Boolean +) : SimpleValuePassing() { override val kotlinBridgeType: IrType get() = symbols.nativePtrType override val cBridgeType: CType @@ -697,15 +761,47 @@ private class ObjCReferenceValuePassing(private val symbols: KonanSymbols, priva override val cType: CType get() = CTypes.voidPtr - override fun IrBuilderWithScope.kotlinToBridged(expression: IrExpression): IrExpression = - irCall(symbols.interopObjCObjectRawValueGetter.owner).apply { - extensionReceiver = expression + override fun IrBuilderWithScope.kotlinToBridged(expression: IrExpression): IrExpression { + val ptr = irCall(symbols.interopObjCObjectRawValueGetter.owner).apply { + extensionReceiver = expression + } + return if (retained) { + irCall(symbols.interopObjCRetain).apply { + putValueArgument(0, ptr) } + } else { + ptr + } + } - override fun IrBuilderWithScope.bridgedToKotlin(expression: IrExpression, symbols: KonanSymbols): IrExpression = - irCall(symbols.interopInterpretObjCPointerOrNull, listOf(type)).apply { - putValueArgument(0, expression) + override fun IrBuilderWithScope.kotlinCallbackResultToBridged(expression: IrExpression): IrExpression { + if (retained) return kotlinToBridged(expression) // Optimization. + // Kotlin code may loose the ownership on this pointer after returning from the bridge, + // so retain the pointer and autorelease it: + return irCall(symbols.interopObjcRetainAutoreleaseReturnValue.owner).apply { + putValueArgument(0, kotlinToBridged(expression)) + } + // TODO: optimize by using specialized Kotlin-to-ObjC converter. + } + + override fun IrBuilderWithScope.bridgedToKotlin(expression: IrExpression, symbols: KonanSymbols): IrExpression { + fun wrapObjCPtr(ptr: IrExpression) = irCall(symbols.interopInterpretObjCPointerOrNull, listOf(type)).apply { + putValueArgument(0, ptr) + } + + return if (retained) { + irBlock(startOffset, endOffset) { + val ptrVar = irTemporary(expression) + val resultVar = irTemporary(wrapObjCPtr(irGet(ptrVar))) + +irCall(symbols.interopObjCRelease.owner).apply { + putValueArgument(0, irGet(ptrVar)) + } + +irGet(resultVar) } + } else { + wrapObjCPtr(expression) + } + } override fun bridgedToC(expression: String): String = expression override fun cToBridged(expression: String): String = expression @@ -797,6 +893,8 @@ private fun KotlinStubs.reportUnsupportedType(reason: String, type: IrType, loca is TypeLocation.FunctionCallResult -> " of return value" is TypeLocation.FunctionPointerParameter -> " of callback parameter ${location.index + 1}" is TypeLocation.FunctionPointerReturnValue -> " of callback return value" + is TypeLocation.ObjCMethodParameter -> " of overridden Objective-C method parameter" + is TypeLocation.ObjCMethodReturnValue -> " of overridden Objective-C method return value" } reportError(location.element, "type ${type.toKotlinType()}$typeLocation is not supported here" + diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt index c5e194dab51..a66ccf040d0 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt @@ -217,17 +217,11 @@ internal class KonanSymbols(context: Context, val symbolTable: SymbolTable, val val interopAllocObjCObject = symbolTable.referenceSimpleFunction(context.interopBuiltIns.allocObjCObject) - val interopObjCRelease = symbolTable.referenceSimpleFunction( - context.interopBuiltIns.packageScope - .getContributedFunctions(Name.identifier("objc_release"), NoLookupLocation.FROM_BACKEND) - .single() - ) + val interopObjCRelease = interopFunction("objc_release") - val interopObjCRetain = symbolTable.referenceSimpleFunction( - context.interopBuiltIns.packageScope - .getContributedFunctions(Name.identifier("objc_retain"), NoLookupLocation.FROM_BACKEND) - .single() - ) + val interopObjCRetain = interopFunction("objc_retain") + + val interopObjcRetainAutoreleaseReturnValue = interopFunction("objc_retainAutoreleaseReturnValue") val interopGetObjCClass = symbolTable.referenceSimpleFunction(context.interopBuiltIns.getObjCClass) @@ -502,6 +496,12 @@ internal class KonanSymbols(context: Context, val symbolTable: SymbolTable, val Name.identifier(className), NoLookupLocation.FROM_BACKEND ) as ClassDescriptor) + private fun interopFunction(name: String) = symbolTable.referenceSimpleFunction( + context.interopBuiltIns.packageScope + .getContributedFunctions(Name.identifier(name), NoLookupLocation.FROM_BACKEND) + .single() + ) + val functions = (0 .. KONAN_FUNCTION_INTERFACES_MAX_PARAMETERS) .map { symbolTable.referenceClass(builtIns.getFunction(it)) } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt index 6e8c40b7215..5279717f0b0 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/KotlinObjCClassInfoGenerator.kt @@ -10,7 +10,6 @@ import llvm.LLVMValueRef import org.jetbrains.kotlin.backend.common.atMostOne import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.descriptors.getStringValue -import org.jetbrains.kotlin.backend.konan.descriptors.getStringValueOrNull import org.jetbrains.kotlin.backend.konan.irasdescriptors.* import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction @@ -27,7 +26,7 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con val instanceMethods = generateInstanceMethodDescs(irClass) val companionObject = irClass.declarations.filterIsInstance().atMostOne { it.isCompanion } - val classMethods = companionObject?.generateOverridingMethodDescs() ?: emptyList() + val classMethods = companionObject?.generateMethodDescs().orEmpty() val superclassName = irClass.getSuperClassNotAny()!!.let { context.llvm.imports.add(it.llvmSymbolOrigin) @@ -71,11 +70,13 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con objCLLvmDeclarations.bodyOffsetGlobal.setInitializer(Int32(0)) } + private fun IrClass.generateMethodDescs(): List = + this.generateOverridingMethodDescs() + this.generateImpMethodDescs() + private fun generateInstanceMethodDescs( irClass: IrClass ): List = mutableListOf().apply { - addAll(irClass.generateOverridingMethodDescs()) - addAll(irClass.generateImpMethodDescs()) + addAll(irClass.generateMethodDescs()) val allImplementedSelectors = this.map { it.selector }.toSet() assert(irClass.getSuperClassNotAny()!!.isExternalObjCClass()) @@ -112,19 +113,21 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con staticData.cStringLiteral(encoding) ) - private fun generateMethodDesc(info: ObjCMethodInfo) = ObjCMethodDesc( - info.selector, - info.encoding, - context.llvm.externalFunction( - info.imp, - functionType(voidType), - origin = info.bridge.llvmSymbolOrigin - ) - ) + private fun generateMethodDesc(info: ObjCMethodInfo) = info.imp?.let { imp -> + ObjCMethodDesc( + info.selector, + info.encoding, + context.llvm.externalFunction( + imp, + functionType(voidType), + origin = info.bridge.llvmSymbolOrigin + ) + ) + } private fun IrClass.generateOverridingMethodDescs(): List = this.simpleFunctions().filter { it.isReal } - .mapNotNull { it.getObjCMethodInfo() }.map { generateMethodDesc(it) } + .mapNotNull { it.getObjCMethodInfo() }.mapNotNull { generateMethodDesc(it) } private fun IrClass.generateImpMethodDescs(): List = this.declarations .filterIsInstance() diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt index 4b795202115..7d8eb358cb8 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.backend.common.push import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.cgen.KotlinStubs import org.jetbrains.kotlin.backend.konan.cgen.generateCCall +import org.jetbrains.kotlin.backend.konan.cgen.generateCFunctionAndFakeKotlinExternalFunction import org.jetbrains.kotlin.backend.konan.cgen.generateCFunctionPointer import org.jetbrains.kotlin.backend.konan.getInlinedClass import org.jetbrains.kotlin.backend.konan.descriptors.allOverriddenFunctions @@ -794,8 +795,33 @@ private class InteropTransformer(val context: Context, val irFile: IrFile) : IrB } } + override fun visitClass(declaration: IrClass): IrStatement { + super.visitClass(declaration) + if (declaration.isKotlinObjCClass()) { + val imps = declaration.simpleFunctions().filter { it.isReal }.flatMap { function -> + function.overriddenSymbols.mapNotNull { + val info = it.owner.getExternalObjCMethodInfo() + if (info == null || info.imp != null) { + null + } else { + generateWithStubs(it.owner) { + generateCFunctionAndFakeKotlinExternalFunction( + function, + it.owner, + isObjCMethod = true, + location = function + ) + } + } + } + } + declaration.addChildren(imps) + } + return declaration + } + private fun generateCFunctionPointer(function: IrSimpleFunction, expression: IrExpression): IrExpression = - generateWithStubs { generateCFunctionPointer(function, function, false, expression) } + generateWithStubs { generateCFunctionPointer(function, function, expression) } override fun visitCall(expression: IrCall): IrExpression { diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 9171c806ba3..b2b131ee885 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -3046,7 +3046,7 @@ task interop_objc_smoke(type: RunInteropKonanTest) { "5\n" + "String macro\n" + "CFString macro\n" + - "Deallocated\nDeallocated\n" + "Deallocated\nDeallocated\nDeallocated\n" if (isAppleTarget(project)) { source = "interop/objc/smoke.kt" diff --git a/backend.native/tests/interop/objc/smoke.h b/backend.native/tests/interop/objc/smoke.h index 6a826097866..999fd010c13 100644 --- a/backend.native/tests/interop/objc/smoke.h +++ b/backend.native/tests/interop/objc/smoke.h @@ -65,4 +65,52 @@ int formatStringLength(NSString* format, ...); typedef NS_ENUM(int32_t, ForwardDeclaredEnum); typedef NS_ENUM(int32_t, ForwardDeclaredEnum) { ZERO, ONE, TWO, -}; \ No newline at end of file +}; + +@protocol ObjectFactory +@required +-(id)create; +@end; + +id createObjectWithFactory(id factory) { + return [factory create]; +} + +@protocol BlockProvider +@required +-(int (^)(int)) block; +@end; + +int callProvidedBlock(id blockProvider, int argument) { + return [blockProvider block](argument); +} + +@protocol BlockConsumer +@required +-(int)callBlock:(int (^)(int))block argument:(int)argument; +@end; + +int callPlusOneBlock(id blockConsumer, int argument) { + return [blockConsumer callBlock:^int(int p) { return p + 1; } argument:argument]; +} + +@protocol CustomRetainMethods +@required +-(id)returnRetained:(id)obj __attribute__((ns_returns_retained)); +-(void)consume:(id) __attribute__((ns_consumed)) obj; +-(void)consumeSelf __attribute__((ns_consumes_self)); +@end; + +extern BOOL unexpectedDeallocation; + +@interface MustNotBeDeallocated : NSObject +@end; + +void useCustomRetainMethods(id p) { + MustNotBeDeallocated* obj = [MustNotBeDeallocated new]; + CFBridgingRetain(obj); // Over-retain to detect possible over-release. + [p returnRetained:obj]; + + [p consume:p]; + [p consumeSelf]; +} \ No newline at end of file diff --git a/backend.native/tests/interop/objc/smoke.kt b/backend.native/tests/interop/objc/smoke.kt index 40a4a65bce9..867f9342217 100644 --- a/backend.native/tests/interop/objc/smoke.kt +++ b/backend.native/tests/interop/objc/smoke.kt @@ -82,6 +82,30 @@ fun run() { println(STRING_MACRO) println(CFSTRING_MACRO) + + // Ensure that overriding method bridge has retain-autorelease sequence: + createObjectWithFactory(object : NSObject(), ObjectFactoryProtocol { + override fun create() = autoreleasepool { NSObject() } + }) + + assertEquals(222, callProvidedBlock(object : NSObject(), BlockProviderProtocol { + override fun block(): (Int) -> Int = { it * 2 } + }, 111)) + + assertEquals(322, callPlusOneBlock(object : NSObject(), BlockConsumerProtocol { + override fun callBlock(block: ((Int) -> Int)?, argument: Int) = block!!(argument) + }, 321)) + + autoreleasepool { + useCustomRetainMethods(object : Foo(), CustomRetainMethodsProtocol { + override fun returnRetained(obj: Any?) = obj + override fun consume(obj: Any?) {} + override fun consumeSelf() {} + }) + } + + assertFalse(unexpectedDeallocation) + } fun MutablePairProtocol.swap() { diff --git a/backend.native/tests/interop/objc/smoke.m b/backend.native/tests/interop/objc/smoke.m index 276fd513165..ce9d5509055 100644 --- a/backend.native/tests/interop/objc/smoke.m +++ b/backend.native/tests/interop/objc/smoke.m @@ -68,3 +68,11 @@ int formatStringLength(NSString* format, ...) { va_end(args); return result.length; } + +BOOL unexpectedDeallocation = NO; + +@implementation MustNotBeDeallocated +-(void)dealloc { + unexpectedDeallocation = YES; +} +@end;