Support exposing Kotlin methods as Objective-C actions and outlets

This commit is contained in:
Svyatoslav Scherbina
2017-08-28 12:46:00 +03:00
committed by SvyatoslavScherbina
parent 2f30c15b45
commit 5a7e3e20b4
9 changed files with 312 additions and 22 deletions
@@ -62,7 +62,11 @@ inline fun <T : ObjCObject?> interpretObjCPointerOrNull(rawPtr: NativePtr): T? =
null
}
inline fun <T : ObjCObject?> interpretObjCPointer(rawPtr: NativePtr): T? = interpretObjCPointerOrNull<T>(rawPtr)!!
inline fun <T : ObjCObject> interpretObjCPointer(rawPtr: NativePtr): T = if (rawPtr != nativeNullPtr) {
ObjCPointerHolder(rawPtr).uncheckedCast<T>()
} else {
throw NullPointerException()
}
inline val ObjCObject.rawPtr: NativePtr get() = (this.uncheckedCast<ObjCPointerHolder>()).rawPtr
inline val ObjCObject?.rawPtr: NativePtr get() = if (this != null) {
@@ -105,6 +109,10 @@ annotation class ObjCConstructor(val initSelector: String)
@Retention(AnnotationRetention.BINARY)
annotation class InteropStubs()
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
private annotation class ObjCMethodImp(val selector: String, val encoding: String)
@konan.internal.ExportForCompiler
private fun <T : ObjCObject> allocObjCObject(clazz: NativePtr): T {
val rawResult = objc_allocWithZone(clazz)
@@ -28,4 +28,20 @@ inline fun <R> autoreleasepool(block: () -> R): R {
// TODO: null checks
var <T : ObjCObject?> ObjCObjectVar<T>.value: T
get() = interpretObjCPointerOrNull<T>(nativeMemUtils.getNativePtr(this)).uncheckedCast<T>()
set(value) = nativeMemUtils.putNativePtr(this, value.rawPtr)
set(value) = nativeMemUtils.putNativePtr(this, value.rawPtr)
/**
* Makes Kotlin method in Objective-C class accessible through Objective-C dispatch
* to be used as action sent by control in UIKit or AppKit.
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class ObjCAction
/**
* Makes Kotlin property in Objective-C class settable through Objective-C dispatch
* to be used as IB outlet.
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
annotation class ObjCOutlet
@@ -152,6 +152,15 @@ internal class InteropBuiltIns(builtIns: KonanBuiltIns) {
val getObjCMessenger = packageScope.getContributedFunctions("getMessenger").single()
val getObjCMessengerLU = packageScope.getContributedFunctions("getMessengerLU").single()
val interpretObjCPointerOrNull = packageScope.getContributedFunctions("interpretObjCPointerOrNull").single()
val interpretObjCPointer = packageScope.getContributedFunctions("interpretObjCPointer").single()
val objCAction = packageScope.getContributedClassifier("ObjCAction") as ClassDescriptor
val objCOutlet = packageScope.getContributedClassifier("ObjCOutlet") as ClassDescriptor
val objCMethodImp = packageScope.getContributedClassifier("ObjCMethodImp") as ClassDescriptor
}
private fun MemberScope.getContributedVariables(name: String) =
@@ -22,6 +22,8 @@ import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.ExternalOverridabilityCondition
import org.jetbrains.kotlin.resolve.descriptorUtil.*
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
private val interopPackageName = InteropBuiltIns.FqNames.packageName
private val objCObjectFqName = interopPackageName.child(Name.identifier("ObjCObject"))
@@ -34,6 +36,12 @@ fun ClassDescriptor.isObjCClass(): Boolean =
this.getAllSuperClassifiers().any { it.fqNameSafe == objCObjectFqName } &&
this.containingDeclaration.fqNameSafe != interopPackageName
fun KotlinType.isObjCObjectType(): Boolean =
TypeUtils.getClassDescriptor(this)
?.getAllSuperClassifiers()
?.any { it.fqNameSafe == objCObjectFqName }
?: false
fun ClassDescriptor.isExternalObjCClass(): Boolean = this.isObjCClass() &&
this.parentsWithSelf.filterIsInstance<ClassDescriptor>().any {
it.annotations.findAnnotation(externalObjCClassFqName) != null
@@ -153,4 +161,17 @@ class ObjCOverridabilityCondition : ExternalOverridabilityCondition {
return true
}
}
fun inferObjCSelector(descriptor: FunctionDescriptor): String = if (descriptor.valueParameters.isEmpty()) {
descriptor.name.asString()
} else {
buildString {
append(descriptor.name)
append(':')
descriptor.valueParameters.drop(1).forEach {
append(it.name)
append(':')
}
}
}
@@ -65,6 +65,12 @@ internal class KonanSymbols(context: Context, val symbolTable: SymbolTable): Sym
symbolTable.referenceSimpleFunction(function)
}
val interopInterpretObjCPointer =
symbolTable.referenceSimpleFunction(context.interopBuiltIns.interpretObjCPointer)
val interopInterpretObjCPointerOrNull =
symbolTable.referenceSimpleFunction(context.interopBuiltIns.interpretObjCPointerOrNull)
val getNativeNullPtr = symbolTable.referenceSimpleFunction(context.builtIns.getNativeNullPtr)
val boxFunctions = ValueType.values().associate {
@@ -176,10 +176,10 @@ internal interface ContextUtils : RuntimeAware {
/**
* Address of entry point of [llvmFunction].
*/
val FunctionDescriptor.entryPointAddress: ConstValue
val FunctionDescriptor.entryPointAddress: ConstPointer
get() {
val result = LLVMConstBitCast(this.llvmFunction, int8TypePtr)!!
return constValue(result)
return constPointer(result)
}
val ClassDescriptor.typeInfoPtr: ConstPointer
@@ -176,7 +176,7 @@ internal class RTTIGeneratorVisitor(context: Context) : IrElementVisitorVoid {
generator.generate(descriptor)
if (descriptor.isKotlinObjCClass()) {
kotlinObjCClassInfoGenerator.generate(descriptor)
kotlinObjCClassInfoGenerator.generate(declaration)
}
}
@@ -20,24 +20,28 @@ import llvm.LLVMStoreSizeOfType
import org.jetbrains.kotlin.backend.konan.Context
import org.jetbrains.kotlin.backend.konan.ObjCMethodInfo
import org.jetbrains.kotlin.backend.konan.descriptors.contributedMethods
import org.jetbrains.kotlin.backend.konan.descriptors.getStringValue
import org.jetbrains.kotlin.backend.konan.getObjCMethodInfo
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.isFinalClass
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces
internal class KotlinObjCClassInfoGenerator(override val context: Context) : ContextUtils {
fun generate(descriptor: ClassDescriptor) {
fun generate(irClass: IrClass) {
val descriptor = irClass.descriptor
assert(descriptor.isFinalClass)
val objCLLvmDeclarations = context.llvmDeclarations.forClass(descriptor).objCDeclarations!!
val instanceMethods = descriptor.generateMethodDescs()
val instanceMethods = descriptor.generateOverridingMethodDescs() + irClass.generateImpMethodDescs()
val companionObjectDescriptor = descriptor.companionObjectDescriptor
val classMethods = companionObjectDescriptor?.generateMethodDescs() ?: emptyList()
val classMethods = companionObjectDescriptor?.generateOverridingMethodDescs() ?: emptyList()
val superclassName = descriptor.getSuperClassNotAny()!!.name.asString()
val protocolNames = descriptor.getSuperInterfaces().map { it.name.asString().removeSuffix("Protocol") }
@@ -79,16 +83,37 @@ internal class KotlinObjCClassInfoGenerator(override val context: Context) : Con
objCLLvmDeclarations.bodyOffsetGlobal.setInitializer(Int32(0))
}
private fun createMethodDesc(selector: String, encoding: String, imp: ConstPointer) = Struct(
runtime.objCMethodDescription,
staticData.cStringLiteral(selector),
staticData.cStringLiteral(encoding),
imp
)
private fun generateMethodDesc(info: ObjCMethodInfo): ConstValue {
return Struct(runtime.objCMethodDescription,
staticData.cStringLiteral(info.selector),
staticData.cStringLiteral(info.encoding),
return createMethodDesc(
info.selector,
info.encoding,
constPointer(context.llvm.externalFunction(info.imp, functionType(voidType))).bitcast(int8TypePtr)
)
}
private fun ClassDescriptor.generateMethodDescs(): List<ConstValue> =
private fun ClassDescriptor.generateOverridingMethodDescs(): List<ConstValue> =
this.unsubstitutedMemberScope.contributedMethods.filter {
it.kind.isReal && it !is ConstructorDescriptor
}.mapNotNull { it.getObjCMethodInfo() }.map { generateMethodDesc(it) }
private fun IrClass.generateImpMethodDescs(): List<ConstValue> = this.declarations
.filterIsInstance<IrSimpleFunction>()
.mapNotNull {
val annotation =
it.descriptor.annotations.findAnnotation(context.interopBuiltIns.objCMethodImp.fqNameSafe) ?:
return@mapNotNull null
createMethodDesc(
annotation.getStringValue("selector"),
annotation.getStringValue("encoding"),
it.descriptor.entryPointAddress
)
}
}
@@ -18,36 +18,41 @@ package org.jetbrains.kotlin.backend.konan.lower
import org.jetbrains.kotlin.backend.common.FileLoweringPass
import org.jetbrains.kotlin.backend.common.descriptors.allParameters
import org.jetbrains.kotlin.backend.common.lower.IrBuildingTransformer
import org.jetbrains.kotlin.backend.common.lower.at
import org.jetbrains.kotlin.backend.common.lower.irBlock
import org.jetbrains.kotlin.backend.common.lower.*
import org.jetbrains.kotlin.backend.common.peek
import org.jetbrains.kotlin.backend.common.pop
import org.jetbrains.kotlin.backend.common.push
import org.jetbrains.kotlin.backend.konan.*
import org.jetbrains.kotlin.backend.konan.descriptors.getStringValue
import org.jetbrains.kotlin.backend.konan.descriptors.isInterface
import org.jetbrains.kotlin.backend.konan.descriptors.synthesizedName
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptorImpl
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.annotations.AnnotationsImpl
import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl
import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.declarations.impl.IrFunctionImpl
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.getArguments
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.OverridingUtil
import org.jetbrains.kotlin.resolve.constants.StringValue
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
import org.jetbrains.kotlin.types.typeUtil.isUnit
internal class InteropLoweringPart1(val context: Context) : IrBuildingTransformer(context), FileLoweringPass {
@@ -87,7 +92,7 @@ internal class InteropLoweringPart1(val context: Context) : IrBuildingTransforme
override fun visitClass(declaration: IrClass): IrStatement {
if (declaration.descriptor.isKotlinObjCClass()) {
checkKotlinObjCClass(declaration)
lowerKotlinObjCClass(declaration)
}
outerClasses.push(declaration)
@@ -98,6 +103,204 @@ internal class InteropLoweringPart1(val context: Context) : IrBuildingTransforme
}
}
private fun lowerKotlinObjCClass(irClass: IrClass) {
checkKotlinObjCClass(irClass)
val interop = context.interopBuiltIns
irClass.declarations.mapNotNull {
when {
it is IrSimpleFunction && it.descriptor.annotations.hasAnnotation(interop.objCAction) ->
generateActionImp(it)
it is IrProperty && it.descriptor.annotations.hasAnnotation(interop.objCOutlet) ->
generateOutletSetterImp(it)
else -> null
}
}.let { irClass.declarations.addAll(it) }
}
private fun generateActionImp(function: IrSimpleFunction): IrSimpleFunction {
val action = "@${context.interopBuiltIns.objCAction.name}"
function.extensionReceiverParameter?.let {
context.reportCompilationError("$action method must not have extension receiver",
currentFile, it)
}
function.valueParameters.forEach {
val kotlinType = it.descriptor.type
if (!kotlinType.isObjCObjectType()) {
context.reportCompilationError("Unexpected $action method parameter type: $kotlinType\n" +
"Only Objective-C object types are supported here",
currentFile, it)
}
}
val returnType = function.descriptor.returnType!!
if (!returnType.isUnit()) {
context.reportCompilationError("Unexpected $action method return type: $returnType\n" +
"Only 'Unit' is supported here",
currentFile, function
)
}
return generateFunctionImp(inferObjCSelector(function.descriptor), function)
}
private fun generateOutletSetterImp(property: IrProperty): IrSimpleFunction {
val descriptor = property.descriptor
val outlet = "@${context.interopBuiltIns.objCOutlet.name}"
if (!descriptor.isVar) {
context.reportCompilationError("$outlet property must be var",
currentFile, property)
}
property.getter?.extensionReceiverParameter?.let {
context.reportCompilationError("$outlet must not have extension receiver",
currentFile, it)
}
val type = descriptor.type
if (!type.isObjCObjectType()) {
context.reportCompilationError("Unexpected $outlet type: $type\n" +
"Only Objective-C object types are supported here",
currentFile, property)
}
val name = descriptor.name.asString()
val selector = "set${name.capitalize()}:"
return generateFunctionImp(selector, property.setter!!)
}
private fun getMethodSignatureEncoding(function: IrFunction): String {
assert(function.extensionReceiverParameter == null)
assert(function.valueParameters.all { it.type.isObjCObjectType() })
assert(function.descriptor.returnType!!.isUnit())
// Note: these values are valid for x86_64 and arm64.
return when (function.valueParameters.size) {
0 -> "v16@0:8"
1 -> "v24@0:8@16"
2 -> "v32@0:8@16@24"
else -> context.reportCompilationError("Only 0, 1 or 2 parameters are supported here",
currentFile, function
)
}
}
private fun generateFunctionImp(selector: String, function: IrFunction): IrSimpleFunction {
val signatureEncoding = getMethodSignatureEncoding(function)
val returnType = function.descriptor.returnType!!
assert(returnType.isUnit())
val nativePtrType = context.builtIns.nativePtr.defaultType
val parameterTypes = mutableListOf(nativePtrType) // id self
parameterTypes.add(nativePtrType) // SEL _cmd
function.valueParameters.mapTo(parameterTypes) {
when {
it.descriptor.type.isObjCObjectType() -> nativePtrType
else -> TODO()
}
}
// Annotations to be detected in KotlinObjCClassInfoGenerator:
val annotations = createObjCMethodImpAnnotations(selector = selector, encoding = signatureEncoding)
val newDescriptor = SimpleFunctionDescriptorImpl.create(
function.descriptor.containingDeclaration,
annotations,
("imp:" + selector).synthesizedName,
CallableMemberDescriptor.Kind.SYNTHESIZED,
SourceElement.NO_SOURCE
)
val valueParameters = parameterTypes.mapIndexed { index, it ->
ValueParameterDescriptorImpl(
newDescriptor,
null,
index,
Annotations.EMPTY,
Name.identifier("p$index"),
it,
false,
false,
false,
null,
SourceElement.NO_SOURCE
)
}
newDescriptor.initialize(
null, null,
emptyList(),
valueParameters,
returnType,
Modality.FINAL,
Visibilities.PRIVATE
)
val newFunction = IrFunctionImpl(
function.startOffset, function.endOffset,
IrDeclarationOrigin.DEFINED,
newDescriptor
).apply { createParameterDeclarations() }
val builder = context.createIrBuilder(newFunction.symbol)
newFunction.body = builder.irBlockBody(newFunction) {
+irCall(function.symbol).apply {
dispatchReceiver = interpretObjCPointer(
irGet(newFunction.valueParameters[0].symbol),
function.dispatchReceiverParameter!!.type
)
function.valueParameters.forEachIndexed { index, parameter ->
putValueArgument(index,
interpretObjCPointer(
irGet(newFunction.valueParameters[index + 2].symbol),
parameter.type
)
)
}
}
}
return newFunction
}
private fun IrBuilderWithScope.interpretObjCPointer(expression: IrExpression, type: KotlinType): IrExpression {
val callee: IrFunctionSymbol = if (TypeUtils.isNullableType(type)) {
symbols.interopInterpretObjCPointerOrNull
} else {
symbols.interopInterpretObjCPointer
}
return irCall(callee, listOf(type)).apply {
putValueArgument(0, expression)
}
}
private fun createObjCMethodImpAnnotations(selector: String, encoding: String): Annotations {
val annotation = AnnotationDescriptorImpl(
context.interopBuiltIns.objCMethodImp.defaultType,
mapOf("selector" to selector, "encoding" to encoding)
.mapKeys { Name.identifier(it.key) }
.mapValues { StringValue(it.value, context.builtIns) },
SourceElement.NO_SOURCE
)
return AnnotationsImpl(listOf(annotation))
}
private fun checkKotlinObjCClass(irClass: IrClass) {
val kind = irClass.descriptor.kind
if (kind != ClassKind.CLASS && kind != ClassKind.OBJECT) {
@@ -597,4 +800,6 @@ private fun IrBuilder.irFloat(value: Float) =
IrConstImpl.float(startOffset, endOffset, context.builtIns.floatType, value)
private fun IrBuilder.irDouble(value: Double) =
IrConstImpl.double(startOffset, endOffset, context.builtIns.doubleType, value)
IrConstImpl.double(startOffset, endOffset, context.builtIns.doubleType, value)
private fun Annotations.hasAnnotation(descriptor: ClassDescriptor) = this.hasAnnotation(descriptor.fqNameSafe)