[K/N] Add direct dispatch to objcexport
If an instance method does not require virtual dispatch, the ObjC
exported class can use direct dispatch instead of message passing. This
can be done exactly when a Kotlin method does not override anything, and
is not declared open. This feature is under a flag, and is enabled with
-Xbinary=objcExportDirectMethods=true
diff --git a/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt b/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt
index 4065aec..ad444c7 100644
--- a/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt
+++ b/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfigurationKeys.kt
@@ -130,6 +130,8 @@
= CompilerConfigurationKey.create("debug info format version")
val OBJC_GENERICS: CompilerConfigurationKey<Boolean>
= CompilerConfigurationKey.create("write objc header with generics support")
+ val OBJC_DIRECT_METHODS: CompilerConfigurationKey<Boolean>
+ = CompilerConfigurationKey.create("use direct dispatch where possible in objc exported classes")
val DEBUG_PREFIX_MAP: CompilerConfigurationKey<Map<String, String>>
= CompilerConfigurationKey.create("remap file source paths in debug info")
val PRE_LINK_CACHES: CompilerConfigurationKey<Boolean>
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
index ec4a0a4..52f1251 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt
@@ -42,6 +42,8 @@
val objcExportEntryPointsPath by stringOption()
+ val objcExportDirectMethods by booleanOption()
+
val dumpObjcSelectorToSignatureMapping by stringOption()
val gc by option<GC>(shortcut = { it.shortcut })
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanCompilerFrontendServices.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanCompilerFrontendServices.kt
index 5e995ac..9233732 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanCompilerFrontendServices.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanCompilerFrontendServices.kt
@@ -42,6 +42,9 @@
override val objcGenerics: Boolean
get() = config.configuration.getBoolean(KonanConfigKeys.OBJC_GENERICS)
+ override val objcDirectMethods: Boolean
+ get() = config.configuration.getBoolean(KonanConfigKeys.OBJC_DIRECT_METHODS)
+
override val disableSwiftMemberNameMangling: Boolean
get() = config.configuration.getBoolean(BinaryOptions.objcExportDisableSwiftMemberNameMangling)
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
index 376c942..c96cd88 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt
@@ -261,6 +261,10 @@
?: ObjCEntryPoints.ALL
}
+ val objcExportDirectMethods: Boolean by lazy {
+ configuration.get(BinaryOptions.objcExportDirectMethods) ?: false
+ }
+
/**
* Path to store ObjC selector to Kotlin signature mapping
*/
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanDriver.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanDriver.kt
index 159968d..012d5e1 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanDriver.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanDriver.kt
@@ -212,6 +212,7 @@
copy(BinaryOptions.objcExportDisableSwiftMemberNameMangling)
copy(BinaryOptions.objcExportIgnoreInterfaceMethodCollisions)
copy(KonanConfigKeys.OBJC_GENERICS)
+ copy(KonanConfigKeys.OBJC_DIRECT_METHODS)
// KT-71976: Restore keys, which are reset within `compilationSpawner.spawn(emptyList())`,
// during invocation of `prepareEnvironment()` with empty arguments.
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
index e553e59..19ab000 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/SetupConfiguration.kt
@@ -181,6 +181,7 @@
put(DEBUG_INFO_VERSION, arguments.debugInfoFormatVersion.toInt())
put(OBJC_GENERICS, !arguments.noObjcGenerics)
+ put(OBJC_DIRECT_METHODS, get(BinaryOptions.objcExportDirectMethods) ?: false)
put(DEBUG_PREFIX_MAP, parseDebugPrefixMap(arguments, this@setupFromArguments))
val libraryToAddToCache = parseLibraryToAddToCache(arguments, this@setupFromArguments, outputKind)
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt
index ea82828..9bfd67d 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt
@@ -48,10 +48,13 @@
val unitSuspendFunctionExport = config.unitSuspendFunctionObjCExport
val entryPoints = config.objcEntryPoints
+ val directMethods = config.objcExportDirectMethods
val mapper = ObjCExportMapper(
frontendServices.deprecationResolver,
unitSuspendFunctionExport = unitSuspendFunctionExport,
- entryPoints = entryPoints)
+ entryPoints = entryPoints,
+ directMethods = directMethods,
+ )
val moduleDescriptors = listOf(moduleDescriptor) + moduleDescriptor.getExportedDependencies(config)
val objcGenerics = config.configuration.getBoolean(KonanConfigKeys.OBJC_GENERICS)
val disableSwiftMemberNameMangling = config.configuration.getBoolean(BinaryOptions.objcExportDisableSwiftMemberNameMangling)
@@ -180,7 +183,7 @@
if (!config.isFinalBinary) return // TODO: emit RTTI to the same modules as classes belong to.
- val mapper = exportedInterface?.mapper ?: ObjCExportMapper(unitSuspendFunctionExport = config.unitSuspendFunctionObjCExport)
+ val mapper = exportedInterface?.mapper ?: ObjCExportMapper(unitSuspendFunctionExport = config.unitSuspendFunctionObjCExport, directMethods = config.objcExportDirectMethods)
namer = exportedInterface?.namer ?: ObjCExportNamerImpl(
setOf(moduleDescriptor),
moduleDescriptor.builtIns,
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt
index 76a677a..55b5a54 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportLazy.kt
@@ -46,6 +46,7 @@
fun isIncluded(moduleInfo: ModuleInfo): Boolean
fun getCompilerModuleName(moduleInfo: ModuleInfo): String
val objcGenerics: Boolean
+ val objcDirectMethods: Boolean
val disableSwiftMemberNameMangling: Boolean
get() = false
@@ -105,6 +106,7 @@
local = true,
configuration.unitSuspendFunctionExport,
configuration.entryPoints,
+ directMethods = configuration.objcDirectMethods,
)
private val namer = ObjCExportNamerImpl(namerConfiguration, builtIns, mapper, problemCollector, local = true)
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt
index e8f5717..a291df6 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt
@@ -32,6 +32,7 @@
private val local: Boolean = false,
internal val unitSuspendFunctionExport: UnitSuspendFunctionObjCExport,
internal val entryPoints: ObjCEntryPoints = ObjCEntryPoints.ALL,
+ internal val directMethods: Boolean,
) {
fun getCustomTypeMapper(descriptor: ClassDescriptor): CustomTypeMapper? = CustomTypeMappers.getMapper(descriptor)
@@ -469,3 +470,6 @@
return bridgeType(descriptor.type)
}
+
+internal fun ObjCExportMapper.shouldBeDirect(descriptor: FunctionDescriptor) =
+ directMethods && descriptor !is ConstructorDescriptor && descriptor.overriddenDescriptors.isEmpty() && descriptor.modality == Modality.FINAL
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt
index d046442..c299867 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt
@@ -114,10 +114,11 @@
moduleDescriptor: ModuleDescriptor,
exportedDependencies: List<ModuleDescriptor>,
topLevelNamePrefix: String,
+ directMethods: Boolean = false,
): ObjCExportNamer = ObjCExportNamerImpl(
(exportedDependencies + moduleDescriptor).toSet(),
moduleDescriptor.builtIns,
- ObjCExportMapper(local = true, unitSuspendFunctionExport = UnitSuspendFunctionObjCExport.DEFAULT),
+ ObjCExportMapper(local = true, unitSuspendFunctionExport = UnitSuspendFunctionObjCExport.DEFAULT, directMethods = directMethods),
ObjCExportProblemCollector.SILENT,
topLevelNamePrefix,
local = true
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt
index e266568..28ea5ed 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslator.kt
@@ -641,6 +641,10 @@
attributes += "objc_designated_initializer"
}
+ if (mapper.shouldBeDirect(method)) {
+ attributes += "objc_direct"
+ }
+
if (unavailable) {
attributes += "unavailable"
} else {
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslatorMobile.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslatorMobile.kt
index e151ab9..9ac632e 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslatorMobile.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportTranslatorMobile.kt
@@ -15,6 +15,7 @@
local = true,
unitSuspendFunctionExport = configuration.unitSuspendFunctionExport,
entryPoints = configuration.entryPoints,
+ directMethods = configuration.objcDirectMethods,
)
return ObjCExportTranslatorMobile(
ObjCExportTranslatorImpl(
diff --git a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjcExportHeaderGeneratorMobile.kt b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjcExportHeaderGeneratorMobile.kt
index 9beb025..4d8d4eb 100644
--- a/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjcExportHeaderGeneratorMobile.kt
+++ b/native/objcexport-header-generator/impl/k1/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjcExportHeaderGeneratorMobile.kt
@@ -30,7 +30,7 @@
local: Boolean = false,
restrictToLocalModules: Boolean = false,
): ObjCExportHeaderGenerator {
- val mapper = ObjCExportMapper(deprecationResolver, local, configuration.unitSuspendFunctionExport, configuration.entryPoints)
+ val mapper = ObjCExportMapper(deprecationResolver, local, configuration.unitSuspendFunctionExport, configuration.entryPoints, directMethods = configuration.objcDirectMethods)
val namerConfiguration = createNamerConfiguration(configuration)
val namer = ObjCExportNamerImpl(namerConfiguration, builtIns, mapper, problemCollector, local)
diff --git a/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/Fe10ObjCExportHeaderGenerator.kt b/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/Fe10ObjCExportHeaderGenerator.kt
index 8cdd024..80c4d7b 100644
--- a/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/Fe10ObjCExportHeaderGenerator.kt
+++ b/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/Fe10ObjCExportHeaderGenerator.kt
@@ -90,6 +90,7 @@
),
unitSuspendFunctionExport = UnitSuspendFunctionObjCExport.DEFAULT,
entryPoints = entryPoints,
+ directMethods = configuration.directMethods,
)
val exportedModuleDescriptors = moduleDescriptors + moduleDescriptors
diff --git a/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/ObjCExportUtils.kt b/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/ObjCExportUtils.kt
index b6374c7..63e50e8 100644
--- a/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/ObjCExportUtils.kt
+++ b/native/objcexport-header-generator/impl/k1/test/org/jetbrains/kotlin/backend/konan/testUtils/ObjCExportUtils.kt
@@ -31,8 +31,9 @@
deprecationResolver: DeprecationResolver? = null,
local: Boolean = false,
unitSuspendFunctionObjCExport: UnitSuspendFunctionObjCExport = UnitSuspendFunctionObjCExport.DEFAULT,
+ directMethods: Boolean = false,
): ObjCExportMapper {
- return ObjCExportMapper(deprecationResolver, local, unitSuspendFunctionObjCExport)
+ return ObjCExportMapper(deprecationResolver, local, unitSuspendFunctionObjCExport, directMethods = directMethods)
}
internal fun createObjCExportNamer(
diff --git a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/testUtils/HeaderGenerator.kt b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/testUtils/HeaderGenerator.kt
index dae800ba..e7f7749 100644
--- a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/testUtils/HeaderGenerator.kt
+++ b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/testUtils/HeaderGenerator.kt
@@ -33,6 +33,8 @@
* will result in the entire public API surface of the said library to be translated in the header
*/
val exportedDependencies: Set<Path> = emptySet(),
+
+ val directMethods: Boolean = false,
)
diff --git a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt
index e1f6c2a..5c9d14f 100644
--- a/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt
+++ b/native/objcexport-header-generator/test/org/jetbrains/kotlin/backend/konan/tests/ObjCExportHeaderGeneratorTest.kt
@@ -608,6 +608,12 @@
doTest(headersTestDataDir.resolve("mangleThrowsAnnotation"))
}
+ @Test
+ @TodoAnalysisApi
+ fun `test - direct methods`() {
+ doTest(headersTestDataDir.resolve("directMethods"), configuration = Configuration(directMethods = true))
+ }
+
private fun doTest(root: File, configuration: Configuration = Configuration()) {
if (!root.isDirectory) fail("Expected ${root.absolutePath} to be directory")
val generatedHeaders = generator.generateHeaders(root, configuration).toString()
diff --git "a/native/objcexport-header-generator/testData/headers/directMethods/\041directMethods.h" "b/native/objcexport-header-generator/testData/headers/directMethods/\041directMethods.h"
new file mode 100644
index 0000000..adee68b
--- /dev/null
+++ "b/native/objcexport-header-generator/testData/headers/directMethods/\041directMethods.h"
@@ -0,0 +1,44 @@
+#import <Foundation/NSArray.h>
+#import <Foundation/NSDictionary.h>
+#import <Foundation/NSError.h>
+#import <Foundation/NSObject.h>
+#import <Foundation/NSSet.h>
+#import <Foundation/NSString.h>
+#import <Foundation/NSValue.h>
+
+@class Bar, Foo;
+
+NS_ASSUME_NONNULL_BEGIN
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
+#pragma clang diagnostic ignored "-Wincompatible-property-type"
+#pragma clang diagnostic ignored "-Wnullability"
+
+#pragma push_macro("_Nullable_result")
+#if !__has_feature(nullability_nullable_result)
+#undef _Nullable_result
+#define _Nullable_result _Nullable
+#endif
+
+__attribute__((objc_subclassing_restricted))
+@interface Bar : Base
+- (instancetype)initWithA:(int32_t)a b:(NSString *)b __attribute__((swift_name("init(a:b:)"))) __attribute__((objc_designated_initializer));
+- (Bar *)doCopyA:(int32_t)a b:(NSString *)b __attribute__((swift_name("doCopy(a:b:)"))) __attribute__((objc_direct));
+- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
+- (NSUInteger)hash __attribute__((swift_name("hash()")));
+- (NSString *)description __attribute__((swift_name("description()")));
+@property (readonly) int32_t a __attribute__((swift_name("a")));
+@property (readonly) NSString *b __attribute__((swift_name("b")));
+@end
+
+@interface Foo : Base
+- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
++ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
+- (void)finalNonOveriding __attribute__((swift_name("finalNonOveriding()"))) __attribute__((objc_direct));
+- (Foo *)open __attribute__((swift_name("open()")));
+- (NSString *)description __attribute__((swift_name("description()")));
+@end
+
+#pragma pop_macro("_Nullable_result")
+#pragma clang diagnostic pop
+NS_ASSUME_NONNULL_END
diff --git a/native/objcexport-header-generator/testData/headers/directMethods/Foo.kt b/native/objcexport-header-generator/testData/headers/directMethods/Foo.kt
new file mode 100644
index 0000000..c22eac8
--- /dev/null
+++ b/native/objcexport-header-generator/testData/headers/directMethods/Foo.kt
@@ -0,0 +1,7 @@
+open class Foo {
+ fun finalNonOveriding() {}
+ override fun toString() = "Foo"
+ open fun open(): Foo = Foo()
+}
+
+data class Bar(val a: Int, val b: String)