[KN] CInterop: __attribute__((objc_direct)) support

Merge-request: KT-MR-8828
Merged-by: Gleb Lukianets <Gleb.Lukianets@jetbrains.com>
This commit is contained in:
Gleb Lukianets
2023-03-02 10:36:53 +00:00
committed by Space Team
parent ae07d0e9ce
commit fc96eb6d8d
17 changed files with 319 additions and 36 deletions
@@ -1150,14 +1150,15 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole
}
return ObjCMethod(
selector, encoding, parameters, returnType,
isVariadic = clang_Cursor_isVariadic(cursor) != 0,
isClass = isClass,
nsConsumesSelf = clang_Cursor_isObjCConsumingSelfMethod(cursor) != 0,
nsReturnsRetained = clang_Cursor_isObjCReturningRetainedMethod(cursor) != 0,
isOptional = (clang_Cursor_isObjCOptional(cursor) != 0),
isInit = (clang_Cursor_isObjCInitMethod(cursor) != 0),
isExplicitlyDesignatedInitializer = hasAttribute(cursor, OBJC_DESGINATED_INITIALIZER)
selector, encoding, parameters, returnType,
isVariadic = clang_Cursor_isVariadic(cursor) != 0,
isClass = isClass,
nsConsumesSelf = clang_Cursor_isObjCConsumingSelfMethod(cursor) != 0,
nsReturnsRetained = clang_Cursor_isObjCReturningRetainedMethod(cursor) != 0,
isOptional = (clang_Cursor_isObjCOptional(cursor) != 0),
isInit = (clang_Cursor_isObjCInitMethod(cursor) != 0),
isExplicitlyDesignatedInitializer = hasAttribute(cursor, OBJC_DESIGNATED_INITIALIZER),
isDirect = hasAttribute(cursor, OBJC_DIRECT),
)
}
@@ -1201,7 +1202,8 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole
}
private val NS_CONSUMED = "ns_consumed"
private val OBJC_DESGINATED_INITIALIZER = "objc_designated_initializer"
private val OBJC_DESIGNATED_INITIALIZER = "objc_designated_initializer"
private val OBJC_DIRECT = "objc_direct"
private fun hasAttribute(cursor: CValue<CXCursor>, name: String): Boolean {
var result = false
@@ -257,7 +257,7 @@ sealed class ObjCClassOrProtocol(val name: String) : ObjCContainer(), TypeDeclar
data class ObjCMethod(
val selector: String, val encoding: String, val parameters: List<Parameter>, private val returnType: Type,
val isVariadic: Boolean, val isClass: Boolean, val nsConsumesSelf: Boolean, val nsReturnsRetained: Boolean,
val isOptional: Boolean, val isInit: Boolean, val isExplicitlyDesignatedInitializer: Boolean
val isOptional: Boolean, val isInit: Boolean, val isExplicitlyDesignatedInitializer: Boolean, val isDirect: Boolean
) {
fun returnsInstancetype(): Boolean = returnType is ObjCInstanceType
@@ -112,6 +112,10 @@ annotation class ExternalObjCClass(val protocolGetter: String = "", val binaryNa
@Retention(AnnotationRetention.BINARY)
annotation class ObjCMethod(val selector: String, val encoding: String, val isStret: Boolean = false)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class ObjCDirect(val symbol: String)
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.BINARY)
annotation class ObjCConstructor(val initSelector: String, val designated: Boolean)
@@ -132,7 +132,7 @@ private class ObjCMethodStubBuilder(
kotlinMethodParameters = method.getKotlinParameters(context, forConstructorOrFactory = false)
external = (container !is ObjCProtocol)
modality = when (container) {
is ObjCClass -> MemberStubModality.OPEN
is ObjCClass -> if (method.isDirect) MemberStubModality.FINAL else MemberStubModality.OPEN
is ObjCProtocol -> if (method.isOptional) MemberStubModality.OPEN else MemberStubModality.ABSTRACT
is ObjCCategory -> MemberStubModality.FINAL
}
@@ -145,7 +145,17 @@ private class ObjCMethodStubBuilder(
private fun buildObjCMethodAnnotations(main: AnnotationStub): List<AnnotationStub> = listOfNotNull(
main,
AnnotationStub.ObjC.ConsumesReceiver.takeIf { method.nsConsumesSelf },
AnnotationStub.ObjC.ReturnsRetained.takeIf { method.nsReturnsRetained }
AnnotationStub.ObjC.ReturnsRetained.takeIf { method.nsReturnsRetained },
if (method.isDirect) {
when (container) {
is ObjCClass -> container.name
is ObjCCategory -> container.clazz.name
is ObjCProtocol -> null
}?.let {
val prefix = if (method.isClass) '+' else '-'
AnnotationStub.ObjC.Direct("$prefix[$it ${method.selector}]")
}
} else { null },
)
fun isDefaultConstructor(): Boolean =
@@ -185,6 +185,9 @@ sealed class AnnotationStub(val classifier: Classifier) {
class Method(val selector: String, val encoding: String, val isStret: Boolean = false) :
ObjC(Classifier.topLevel(cinteropPackage, "ObjCMethod"))
class Direct(val symbol: String) :
ObjC(Classifier.topLevel(cinteropPackage, "ObjCDirect"))
class Factory(val selector: String, val encoding: String, val isStret: Boolean = false) :
ObjC(Classifier.topLevel(cinteropPackage, "ObjCFactory"))
@@ -394,6 +394,9 @@ private class MappingExtensions(
("encoding" to encoding).asAnnotationArgument(),
("isStret" to KmAnnotationArgument.BooleanValue(isStret))
)
is AnnotationStub.ObjC.Direct -> mapOfNotNull(
("symbol" to symbol).asAnnotationArgument(),
)
is AnnotationStub.ObjC.Factory -> mapOfNotNull(
("selector" to selector).asAnnotationArgument(),
("encoding" to encoding).asAnnotationArgument(),
@@ -458,6 +458,7 @@ class StubIrTextEmitter(
val encoding = annotationStub.encoding.quoteAsKotlinLiteral()
"@ObjCMethod($selector, $encoding$stret)"
}
is AnnotationStub.ObjC.Direct -> "@ObjCDirect(${annotationStub.symbol.quoteAsKotlinLiteral()})"
is AnnotationStub.ObjC.Factory -> {
val stret = if (annotationStub.isStret) ", true" else ""
val selector = annotationStub.selector.quoteAsKotlinLiteral()
@@ -0,0 +1,60 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.native.interop.gen
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.native.interop.indexer.buildNativeIndex
import org.junit.Assume
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class ObjCMethodSignaturesTest : InteropTestsBase() {
companion object {
@BeforeClass
@JvmStatic
fun assumeMacOS() {
Assume.assumeTrue(HostManager.hostIsMac)
}
}
@Test
fun `ObjC Direct`() {
val files = TempFiles("ObjCDirect")
files.file("header.h", """
#import <Foundation/Foundation.h>
@interface Foo : NSObject
+ (void)direct __attribute__((objc_direct));
- (void)direct __attribute__((objc_direct));
@end
@interface Foo(Ext)
+ (void)directExt __attribute__((objc_direct));
- (void)directExt __attribute__((objc_direct));
@end
""".trimIndent())
val defFile = files.file("direct.def", """
language = Objective-C
headers = header.h
""".trimIndent())
val library = buildNativeLibraryFrom(defFile, files.directory)
val index = buildNativeIndex(library, false).index
index.objCClasses.find { it.name == "Foo" }.let { cls ->
assertNotNull(cls, "Class 'Foo' not found in native library $library")
assertNotNull(cls.methods.find { it.selector == "direct" && it.isClass && it.isDirect })
assertNotNull(cls.methods.find { it.selector == "direct" && !it.isClass && it.isDirect })
}
index.objCCategories.find { it.name == "Ext" && it.clazz.name == "Foo" }.let { cat ->
assertNotNull(cat, "Category 'Foo(Ext)' not found in native library $library")
assertNotNull(cat.methods.find { it.selector == "directExt" && it.isClass && it.isDirect })
assertNotNull(cat.methods.find { it.selector == "directExt" && !it.isClass && it.isDirect })
}
}
}
@@ -37,6 +37,7 @@ private val objCProtocolFqName = interopPackageName.child(Name.identifier("ObjCP
private val objCProtocolIdSignature = getTopLevelPublicSignature(objCProtocolFqName)
internal val externalObjCClassFqName = interopPackageName.child(Name.identifier("ExternalObjCClass"))
private val objCMethodFqName = interopPackageName.child(Name.identifier("ObjCMethod"))
private val objCDirectFqName = interopPackageName.child(Name.identifier("ObjCDirect"))
private val objCConstructorFqName = FqName("kotlinx.cinterop.ObjCConstructor")
private val objCFactoryFqName = interopPackageName.child(Name.identifier("ObjCFactory"))
private val objcnamesForwardDeclarationsPackageName = Name.identifier("objcnames")
@@ -111,30 +112,53 @@ fun IrClass.isKotlinObjCClass(): Boolean = this.isObjCClass() && !this.isExterna
data class ObjCMethodInfo(val selector: String,
val encoding: String,
val isStret: Boolean)
val isStret: Boolean,
val directSymbol: String?)
private fun FunctionDescriptor.decodeObjCMethodAnnotation(): ObjCMethodInfo? {
assert (this.kind.isReal)
val methodAnnotation = this.annotations.findAnnotation(objCMethodFqName) ?: return null
return objCMethodInfo(methodAnnotation)
val methodInfo = this.annotations.findAnnotation(objCMethodFqName)?.let {
ObjCMethodInfo(
selector = it.getStringValue("selector"),
encoding = it.getStringValue("encoding"),
isStret = it.getArgumentValueOrNull<Boolean>("isStret") ?: false,
directSymbol = this.annotations.findAnnotation(objCDirectFqName)?.let {
it.getStringValue("symbol")
},
)
}
return methodInfo
}
private fun IrFunction.decodeObjCMethodAnnotation(): ObjCMethodInfo? {
assert (this.isReal)
val methodAnnotation = this.annotations.findAnnotation(objCMethodFqName) ?: return null
return objCMethodInfo(methodAnnotation)
val methodInfo = this.annotations.findAnnotation(objCMethodFqName)?.let {
ObjCMethodInfo(
selector = it.getAnnotationStringValue("selector"),
encoding = it.getAnnotationStringValue("encoding"),
isStret = it.getAnnotationValueOrNull<Boolean>("isStret") ?: false,
directSymbol = this.annotations.findAnnotation(objCDirectFqName)?.getAnnotationStringValue("symbol"),
)
}
return methodInfo
}
private fun objCMethodInfo(annotation: AnnotationDescriptor) = ObjCMethodInfo(
selector = annotation.getStringValue("selector"),
encoding = annotation.getStringValue("encoding"),
isStret = annotation.getArgumentValueOrNull<Boolean>("isStret") ?: false
isStret = annotation.getArgumentValueOrNull<Boolean>("isStret") ?: false,
directSymbol = null,
)
private fun objCMethodInfo(annotation: IrConstructorCall) = ObjCMethodInfo(
selector = annotation.getAnnotationStringValue("selector"),
encoding = annotation.getAnnotationStringValue("encoding"),
isStret = annotation.getAnnotationValueOrNull<Boolean>("isStret") ?: false
isStret = annotation.getAnnotationValueOrNull<Boolean>("isStret") ?: false,
directSymbol = null,
)
/**
@@ -307,12 +307,15 @@ internal fun KotlinStubs.generateObjCCall(
method: IrSimpleFunction,
isStret: Boolean,
selector: String,
directSymbolName: String?,
call: IrFunctionAccessExpression,
superQualifier: IrClassSymbol?,
receiver: ObjCCallReceiver,
arguments: List<IrExpression?>
) = builder.irBlock {
val resolved = method.resolveFakeOverride(allowAbstract = true)?: method
val isDirect = directSymbolName != null
val exceptionMode = ForeignExceptionMode.byValue(
resolved.konanLibrary?.manifestProperties
?.getProperty(ForeignExceptionMode.manifestKey)
@@ -325,20 +328,23 @@ internal fun KotlinStubs.generateObjCCall(
isMutable = true
)
val messenger = irCall(if (isStret) {
symbols.interopGetMessengerStret
} else {
symbols.interopGetMessenger
}.owner).apply {
putValueArgument(0, irGet(superClass)) // TODO: check superClass statically.
}
val targetPtrParameter = if (!isDirect) {
val messenger = irCall(if (isStret) {
symbols.interopGetMessengerStret
} else {
symbols.interopGetMessenger
}.owner).apply {
putValueArgument(0, irGet(superClass)) // TODO: check superClass statically.
}
val targetPtrParameter = callBuilder.passThroughBridge(
messenger,
symbols.interopCPointer.starProjectedType,
CTypes.voidPtr
).name
val targetFunctionName = "targetPtr"
callBuilder.passThroughBridge(
messenger,
symbols.interopCPointer.starProjectedType,
CTypes.voidPtr
).name
} else {
null
}
val preparedReceiver = if (method.objCConsumesReceiver()) {
when (receiver) {
@@ -381,17 +387,31 @@ internal fun KotlinStubs.generateObjCCall(
receiverOrSuper, symbols.nativePtrType, CTypes.voidPtr).name
callBuilder.cFunctionBuilder.addParameter(CTypes.voidPtr)
callBuilder.cCallBuilder.arguments += "@selector($selector)"
callBuilder.cFunctionBuilder.addParameter(CTypes.voidPtr)
if (isDirect) {
callBuilder.cCallBuilder.arguments += "0"
callBuilder.cFunctionBuilder.addParameter(CTypes.voidPtr)
} else {
callBuilder.cCallBuilder.arguments += "@selector($selector)"
callBuilder.cFunctionBuilder.addParameter(CTypes.voidPtr)
}
callBuilder.addArguments(arguments, method)
val returnValuePassing = mapReturnType(method.returnType, call, signature = method)
val targetFunctionName = getUniqueCName("knbridge_targetPtr")
val result = callBuilder.buildCall(targetFunctionName, returnValuePassing)
val targetFunctionVariable = CVariable(CTypes.pointer(callBuilder.cFunctionBuilder.getType()), targetFunctionName)
callBuilder.cBridgeBodyLines.add(0, "$targetFunctionVariable = $targetPtrParameter;")
if (isDirect) {
// This declares a function
val targetFunctionVariable = CVariable(callBuilder.cFunctionBuilder.getType(), targetFunctionName)
callBuilder.cBridgeBodyLines.add(0, "$targetFunctionVariable __asm(\"$directSymbolName\");")
} else {
val targetFunctionVariable = CVariable(CTypes.pointer(callBuilder.cFunctionBuilder.getType()), targetFunctionName)
callBuilder.cBridgeBodyLines.add(0, "$targetFunctionVariable = $targetPtrParameter;")
}
callBuilder.emitCBridge()
@@ -551,6 +551,7 @@ private class InteropLoweringPart1(val generationState: NativeGenerationState) :
method,
info.isStret,
info.selector,
info.directSymbol,
call,
superQualifier,
receiver,
@@ -626,6 +627,7 @@ private class InteropLoweringPart1(val generationState: NativeGenerationState) :
require(expression.superQualifierSymbol?.owner?.isInterface != true) { renderCompilerError(expression) }
builder.at(expression)
return builder.genLoweredObjCMethodCall(
methodInfo,
superQualifier = expression.superQualifierSymbol,
@@ -4379,6 +4379,13 @@ if (PlatformInfo.isAppleTarget(project)) {
it.headers "$projectDir/interop/objc/msg_send/messaging.h"
}
createInterop("objcDirect") {
it.defFile 'interop/objc/direct/direct.def'
it.headers "$projectDir/interop/objc/direct/direct.h"
it.extraOpts "-Xcompile-source", "$projectDir/interop/objc/direct/direct.m", "-Xsource-compiler-option", "-DNS_FORMAT_ARGUMENT(A)="
it.compilerOpts '-DNS_FORMAT_ARGUMENT(A)='
}
createInterop("foreignException") {
it.defFile 'interop/objc/foreignException/objc_wrap.def'
it.headers "$projectDir/interop/objc/foreignException/objc_wrap.h"
@@ -5081,6 +5088,11 @@ if (PlatformInfo.isAppleTarget(project)) {
}
}
interopTest("interop_objc_direct") {
source = "interop/objc/direct/main.kt"
interop = "objcDirect"
}
interopTest("interop_objc_foreignException") {
disabled = isK2(project) // KT-55909
source = "interop/objc/foreignException/objc_wrap.kt"
@@ -6583,6 +6595,15 @@ fileCheckTest("filecheck_constants_merge") {
annotatedSource = project.file('filecheck/constants_merge.kt')
}
fileCheckTest("filecheck_objc_direct") {
enabled = enabled && cacheTesting == null
annotatedSource = project.file('filecheck/objc_direct.kt')
interop = "objcDirect"
enabled = target.family.appleFamily
}
dependencies {
nopPluginApi kotlinCompilerModule
nopPluginApi project(":native:kotlin-native-utils")
@@ -0,0 +1,26 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
import direct.*
import kotlinx.cinterop.*
//CHECK-LABEL: kfun:#callDirect(){}kotlin.ULong
fun callDirect(): ULong {
val cc = CallingConventions()
//CHECK: invoke i64 @_{{[a-zA-Z0-9]+}}_knbridge{{[0-9]+}}(i8* %{{[0-9]+}}, i64 42)
return cc.direct(42)
}
//CHECK-LABEL: kfun:#callRegular(){}kotlin.ULong
fun callRegular(): ULong {
val cc = CallingConventions()
//CHECK: invoke i64 @_{{[a-zA-Z0-9]+}}_knbridge{{[0-9]+}}(i8* %{{[0-9]+}}, i8* %{{[0-9]+}}, i64 42)
return cc.regular(42)
}
fun main() {
callDirect()
callRegular()
}
@@ -0,0 +1,2 @@
language = Objective-C
headerFilter = **/direct.h
@@ -0,0 +1,32 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
__attribute__((objc_runtime_name("CC")))
__attribute__((swift_name("CC")))
@interface CallingConventions : NSObject
+ (NSUInteger)regular:(NSUInteger)arg;
- (NSUInteger)regular:(NSUInteger)arg;
+ (NSUInteger)direct:(NSUInteger)arg __attribute__((objc_direct));
- (NSUInteger)direct:(NSUInteger)arg __attribute__((objc_direct));
@end
@interface CallingConventions(Ext)
+ (NSUInteger)regularExt:(NSUInteger)arg;
- (NSUInteger)regularExt:(NSUInteger)arg;
+ (NSUInteger)directExt:(NSUInteger)arg __attribute__((objc_direct));
- (NSUInteger)directExt:(NSUInteger)arg __attribute__((objc_direct));
@end
__attribute__((objc_runtime_name("CCH")))
__attribute__((swift_name("CCH")))
@interface CallingConventionsHeir : CallingConventions
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,27 @@
#import "direct.h"
#define TEST_METHOD_IMPL(NAME) (NSUInteger)NAME:(NSUInteger)arg { return arg; }
@implementation CallingConventions : NSObject
+ TEST_METHOD_IMPL(regular);
- TEST_METHOD_IMPL(regular);
+ TEST_METHOD_IMPL(direct);
- TEST_METHOD_IMPL(direct);
@end
@implementation CallingConventions(Ext)
+ TEST_METHOD_IMPL(regularExt);
- TEST_METHOD_IMPL(regularExt);
+ TEST_METHOD_IMPL(directExt);
- TEST_METHOD_IMPL(directExt);
@end
@implementation CallingConventionsHeir
@end
@@ -0,0 +1,46 @@
import direct.*
import kotlinx.cinterop.*
import kotlin.test.*
class CallingConventionsNativeHeir() : CallingConventions() {
// nothing
}
typealias CC = CallingConventions
typealias CCH = CallingConventionsHeir
typealias CCN = CallingConventionsNativeHeir
// KT-54610
fun main(args: Array<String>) {
autoreleasepool {
val cc = CC()
val cch = CCH()
val ccn = CCN()
assertEquals(42UL, CC.regular(42))
assertEquals(42UL, cc.regular(42))
assertEquals(42UL, CC.regularExt(42))
assertEquals(42UL, cc.regularExt(42))
assertEquals(42UL, CCH.regular(42))
assertEquals(42UL, cch.regular(42))
assertEquals(42UL, CCH.regularExt(42))
assertEquals(42UL, cch.regularExt(42))
assertEquals(42UL, ccn.regular(42UL))
assertEquals(42UL, ccn.regularExt(42UL))
assertEquals(42UL, CC.direct(42))
assertEquals(42UL, cc.direct(42))
assertEquals(42UL, CC.directExt(42))
assertEquals(42UL, cc.directExt(42))
assertEquals(42UL, CCH.direct(42))
assertEquals(42UL, cch .direct(42))
assertEquals(42UL, CCH.directExt(42))
assertEquals(42UL, cch .directExt(42))
assertEquals(42UL, ccn .direct(42UL))
assertEquals(42UL, ccn .directExt(42UL))
}
}