[K/N] Add flag to dump ObjC selector to signature mapping
This commit add the flag -Xbinary=dumpObjcSelectorToSignatureMapping,
which allows dumping the mapping between objective-C selectors and
Kotlin signatures, generated during ObjCExport. This is prerequisite for
generating correct input to -Xbinary=objcExportEntryPointsPath, which so
far has relied on heuristics and hand crafted mappings.
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 5b050ef..cd27c3e 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
@@ -41,6 +41,8 @@
val objcExportEntryPointsPath by stringOption()
+ val dumpObjcSelectorToSignatureMapping by stringOption()
+
val gc by option<GC>(shortcut = { it.shortcut })
val gcSchedulerType by option<GCSchedulerType>(hideValue = { it.deprecatedWithReplacement != null })
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 abdbe43..baa8b63 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
@@ -256,6 +256,13 @@
?: ObjCEntryPoints.ALL
}
+ /**
+ * Path to store ObjC selector to Kotlin signature mapping
+ */
+ val dumpObjcSelectorToSignatureMapping: String? by lazy {
+ configuration.get(BinaryOptions.dumpObjcSelectorToSignatureMapping)
+ }
+
val enableSafepointSignposts: Boolean = configuration.get(BinaryOptions.enableSafepointSignposts)?.also {
if (it && !target.supportsSignposts) {
configuration.report(CompilerMessageSeverity.STRONG_WARNING, "Signposts are not available on $target. The setting will have no effect.")
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt
index 51e2a74..ad0f34d 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/DynamicCompilerDriver.kt
@@ -65,12 +65,12 @@
val (objCExportedInterface, psiToIrOutput, objCCodeSpec) = performanceManager.tryMeasurePhaseTime(PhaseType.TranslationToIr) {
val objCExportedInterface = engine.runPhase(ProduceObjCExportInterfacePhase, frontendOutput)
engine.runPhase(CreateObjCFrameworkPhase, CreateObjCFrameworkInput(frontendOutput.moduleDescriptor, objCExportedInterface))
- if (config.omitFrameworkBinary) {
- return
- }
val (psiToIrOutput, objCCodeSpec) = engine.runPsiToIr(frontendOutput, isProducingLibrary = false) {
it.runPhase(CreateObjCExportCodeSpecPhase, objCExportedInterface)
}
+ if (config.omitFrameworkBinary) {
+ return
+ }
require(psiToIrOutput is PsiToIrOutput.ForBackend)
Triple(objCExportedInterface, psiToIrOutput, objCCodeSpec)
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt
index d54c408..de57361 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt
@@ -12,6 +12,7 @@
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportedInterface
import org.jetbrains.kotlin.backend.konan.objcexport.createCodeSpec
import org.jetbrains.kotlin.backend.konan.objcexport.createObjCFramework
+import org.jetbrains.kotlin.backend.konan.objcexport.dumpSelectorToSignatureMapping
import org.jetbrains.kotlin.backend.konan.objcexport.produceObjCExportInterface
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
@@ -49,5 +50,9 @@
"ObjCExportCodeCodeSpec",
outputIfNotEnabled = { _, _, _, _, -> ObjCExportCodeSpec(emptyList(), emptyList()) }
) { context, input ->
- input.createCodeSpec(context.symbolTable!!)
+ input.createCodeSpec(context.symbolTable!!).also {
+ context.config.dumpObjcSelectorToSignatureMapping?.let { path ->
+ it.dumpSelectorToSignatureMapping(path)
+ }
+ }
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt
index 88aa735..7e6f8f7 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportCodeSpec.kt
@@ -13,8 +13,10 @@
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.symbols.*
+import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.SymbolTable
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
+import java.io.PrintStream
@OptIn(ObsoleteDescriptorBasedAPI::class)
internal fun ObjCExportedInterface.createCodeSpec(symbolTable: SymbolTable): ObjCExportCodeSpec {
@@ -142,6 +144,53 @@
val types: List<ObjCTypeForKotlinType>
)
+internal fun ObjCExportCodeSpec.dumpSelectorToSignatureMapping(path: String) {
+ PrintStream(path).use { out ->
+ out.println("# Classes mapping")
+ for (type in types) {
+ val objcClass = type.binaryName
+ val kotlinClass = type.irClassSymbol.signature.toString()
+ out.println("$objcClass,$kotlinClass")
+ }
+ fun ObjCMethodSpec.isInstanceMethod(): Boolean = when (this) {
+ is ObjCFactoryMethodForKotlinArrayConstructor -> baseMethod.bridge.isInstance
+ is ObjCInitMethodForKotlinConstructor -> baseMethod.bridge.isInstance
+ is ObjCMethodForKotlinMethod -> baseMethod.bridge.isInstance
+ is ObjCKotlinThrowableAsErrorMethod -> true
+ is ObjCClassMethodForKotlinEnumValuesOrEntries -> false
+ is ObjCGetterForKotlinEnumEntry -> false
+ is ObjCGetterForObjectInstance -> false
+ }
+
+ fun ObjCMethodSpec.getMapping(objcClass: String): String? = when (this) {
+ is ObjCClassMethodForKotlinEnumValuesOrEntries -> "$objcClass.$selector,${valuesFunctionSymbol.signature}"
+ is ObjCFactoryMethodForKotlinArrayConstructor -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
+ is ObjCGetterForKotlinEnumEntry -> "$objcClass.$selector,${irEnumEntrySymbol.signature}"
+ is ObjCGetterForObjectInstance -> "$objcClass.$selector,${classSymbol.signature}"
+ is ObjCInitMethodForKotlinConstructor -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
+ is ObjCKotlinThrowableAsErrorMethod -> null
+ is ObjCMethodForKotlinMethod -> "$objcClass.${baseMethod.selector},${baseMethod.symbol.signature}"
+ }
+ out.println("\n# Instance methods mapping")
+ for (type in types) {
+ for (mapping in type.methods.filter { it.isInstanceMethod() }) {
+ out.println(mapping.getMapping(type.binaryName) ?: continue)
+ }
+ }
+ out.println("\n# Class methods mapping")
+ for (type in types) {
+ for (mapping in type.methods.filterNot { it.isInstanceMethod() }) {
+ out.println(mapping.getMapping(type.binaryName) ?: continue)
+ }
+ }
+ for (file in files) {
+ for (mapping in file.methods) {
+ out.println(mapping.getMapping(file.binaryName) ?: continue)
+ }
+ }
+ }
+}
+
internal sealed class ObjCMethodSpec {
/**
* Aggregates base method (as defined by [ObjCExportMapper.isBaseMethod])
diff --git a/native/native.tests/testData/framework/selectorToSignatureDump/golden.txt b/native/native.tests/testData/framework/selectorToSignatureDump/golden.txt
new file mode 100644
index 0000000..aa260f1
--- /dev/null
+++ b/native/native.tests/testData/framework/selectorToSignatureDump/golden.txt
@@ -0,0 +1,78 @@
+# Classes mapping
+STSDBar,/Bar|null[0]
+STSDKotlinEnum,kotlin/Enum|null[0]
+STSDKotlinComparable,kotlin/Comparable|null[0]
+STSDFoo,/Foo|null[0]
+STSDKotlinThrowable,kotlin/Throwable|null[0]
+STSDKotlinEnumCompanion,kotlin/Enum.Companion|null[0]
+STSDKotlinArray,kotlin/Array|null[0]
+STSDKotlinIterator,kotlin.collections/Iterator|null[0]
+
+# Instance methods mapping
+STSDBar.compareToOther:,kotlin/Comparable.compareTo|compareTo(1:0){}[0]
+STSDBar.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDBar.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDBar.description,kotlin/Any.toString|toString(){}[0]
+STSDBar.name,kotlin/Enum.name.<get-name>|<get-name>(){}[0]
+STSDBar.ordinal,kotlin/Enum.ordinal.<get-ordinal>|<get-ordinal>(){}[0]
+STSDKotlinEnum.compareToOther:,kotlin/Comparable.compareTo|compareTo(1:0){}[0]
+STSDKotlinEnum.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinEnum.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinEnum.description,kotlin/Any.toString|toString(){}[0]
+STSDKotlinEnum.name,kotlin/Enum.name.<get-name>|<get-name>(){}[0]
+STSDKotlinEnum.ordinal,kotlin/Enum.ordinal.<get-ordinal>|<get-ordinal>(){}[0]
+STSDKotlinEnum.initWithName:ordinal:,kotlin/Enum.<init>|<init>(kotlin.String;kotlin.Int){}[0]
+STSDKotlinComparable.compareToOther:,kotlin/Comparable.compareTo|compareTo(1:0){}[0]
+STSDKotlinComparable.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinComparable.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinComparable.description,kotlin/Any.toString|toString(){}[0]
+STSDFoo.bar,/Foo.bar|bar(){}[0]
+STSDFoo.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDFoo.getStackTrace,kotlin/Throwable.getStackTrace|getStackTrace(){}[0]
+STSDFoo.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDFoo.printStackTrace,kotlin/Throwable.printStackTrace|printStackTrace(){}[0]
+STSDFoo.description,kotlin/Any.toString|toString(){}[0]
+STSDFoo.baz,/Foo.baz.<get-baz>|<get-baz>(){}[0]
+STSDFoo.cause,kotlin/Throwable.cause.<get-cause>|<get-cause>(){}[0]
+STSDFoo.message,kotlin/Throwable.message.<get-message>|<get-message>(){}[0]
+STSDFoo.name,/Foo.name.<get-name>|<get-name>(){}[0]
+STSDFoo.qux,/Foo.qux.<get-qux>|<get-qux>(){}[0]
+STSDFoo.initWithName:,/Foo.<init>|<init>(kotlin.String){}[0]
+STSDKotlinThrowable.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinThrowable.getStackTrace,kotlin/Throwable.getStackTrace|getStackTrace(){}[0]
+STSDKotlinThrowable.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinThrowable.printStackTrace,kotlin/Throwable.printStackTrace|printStackTrace(){}[0]
+STSDKotlinThrowable.description,kotlin/Any.toString|toString(){}[0]
+STSDKotlinThrowable.cause,kotlin/Throwable.cause.<get-cause>|<get-cause>(){}[0]
+STSDKotlinThrowable.message,kotlin/Throwable.message.<get-message>|<get-message>(){}[0]
+STSDKotlinThrowable.initWithMessage:,kotlin/Throwable.<init>|<init>(kotlin.String?){}[0]
+STSDKotlinThrowable.initWithCause:,kotlin/Throwable.<init>|<init>(kotlin.Throwable?){}[0]
+STSDKotlinThrowable.init,kotlin/Throwable.<init>|<init>(){}[0]
+STSDKotlinThrowable.initWithMessage:cause:,kotlin/Throwable.<init>|<init>(kotlin.String?;kotlin.Throwable?){}[0]
+STSDKotlinEnumCompanion.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinEnumCompanion.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinEnumCompanion.description,kotlin/Any.toString|toString(){}[0]
+STSDKotlinArray.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinArray.getIndex:,kotlin/Array.get|get(kotlin.Int){}[0]
+STSDKotlinArray.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinArray.iterator,kotlin/Array.iterator|iterator(){}[0]
+STSDKotlinArray.setIndex:value:,kotlin/Array.set|set(kotlin.Int;1:0){}[0]
+STSDKotlinArray.description,kotlin/Any.toString|toString(){}[0]
+STSDKotlinArray.size,kotlin/Array.size.<get-size>|<get-size>(){}[0]
+STSDKotlinIterator.isEqual:,kotlin/Any.equals|equals(kotlin.Any?){}[0]
+STSDKotlinIterator.hasNext,kotlin.collections/Iterator.hasNext|hasNext(){}[0]
+STSDKotlinIterator.hash,kotlin/Any.hashCode|hashCode(){}[0]
+STSDKotlinIterator.next,kotlin.collections/Iterator.next|next(){}[0]
+STSDKotlinIterator.description,kotlin/Any.toString|toString(){}[0]
+
+# Class methods mapping
+STSDBar.a,/Bar.A|null[0]
+STSDBar.b,/Bar.B|null[0]
+STSDBar.c,/Bar.C|null[0]
+STSDBar.values,/Bar.values|values#static(){}[0]
+STSDBar.entries,/Bar.entries.<get-entries>|<get-entries>#static(){}[0]
+STSDKotlinEnum.companion,kotlin/Enum.Companion|null[0]
+STSDKotlinEnumCompanion.companion,kotlin/Enum.Companion|null[0]
+STSDKotlinEnumCompanion.shared,kotlin/Enum.Companion|null[0]
+STSDKotlinArray.arrayWithSize:init:,kotlin/Array.<init>|<init>(kotlin.Int;kotlin.Function1<kotlin.Int,1:0>){}[0]
+STSDMainKt.fooTimes:name:,/foo|foo(kotlin.Int;kotlin.String){}[0]
diff --git a/native/native.tests/testData/framework/selectorToSignatureDump/main.kt b/native/native.tests/testData/framework/selectorToSignatureDump/main.kt
new file mode 100644
index 0000000..092c872
--- /dev/null
+++ b/native/native.tests/testData/framework/selectorToSignatureDump/main.kt
@@ -0,0 +1,11 @@
+class Foo(val name: String): Throwable(name) {
+ fun bar(): Foo = Foo(name + name)
+ val baz = 42
+ val qux: String get() = name
+}
+
+enum class Bar {
+ A, B, C
+}
+
+fun foo(times: Int, name: String): List<Foo> = (1..times).map{Foo(name)}
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/FrameworkTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/FrameworkTest.kt
index 48a041c8..cf8556b 100644
--- a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/FrameworkTest.kt
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/FrameworkTest.kt
@@ -18,11 +18,13 @@
import org.jetbrains.kotlin.konan.test.blackbox.support.settings.*
import org.jetbrains.kotlin.konan.test.blackbox.support.util.createTestProvider
import org.jetbrains.kotlin.native.executors.runProcess
+import org.jetbrains.kotlin.test.KotlinTestUtils.assertEqualsToFile
import org.jetbrains.kotlin.test.KtAssert.fail
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Test
import java.io.File
+import kotlin.test.assertEquals
import kotlin.time.Duration
@ClassicPipeline()
@@ -402,6 +404,43 @@
listOf("-D", "DISALLOW_SUSPEND_ANY_THREAD"), true, false)
}
+ @Test
+ fun objCExportDumpObjcSelectorToSignatureMapping() {
+ Assumptions.assumeTrue(testRunSettings.get<KotlinNativeTargets>().testTarget.family == Family.OSX)
+ val testName = "selectorToSignatureDump"
+ val testDir = testSuiteDir.resolve(testName)
+ val dumpFile = buildDir.resolve("dump.txt")
+ val goldenFile = testDir.resolve("golden.txt")
+ val freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=bundleId=$testName",
+ "-Xbinary=bundleVersion=FooBundleVersion",
+ "-Xbinary=bundleShortVersionString=FooBundleShortVersionString",
+ "-Xbinary=dumpObjcSelectorToSignatureMapping=${dumpFile.absolutePath}",
+ "-Xomit-framework-binary"
+ )
+ )
+ val testCase = generateObjCFrameworkTestCase(
+ TestKind.STANDALONE_NO_TR, extras, testName,
+ listOf(
+ testDir.resolve("main.kt"),
+ ),
+ freeCompilerArgs
+ )
+ testCompilationFactory.testCaseToObjCFrameworkCompilation(testCase, testRunSettings).result.assertSuccess()
+
+ fun File.parseDump(): List<Set<String>> =
+ readText().split("\n\n").map { it.lines().drop(1).toSet() }
+
+ val dump = dumpFile.parseDump()
+ val golden = goldenFile.parseDump()
+ if (dump != golden) {
+ // The following assert will fail here, and provide better UX than asserting that dump is equal to golden
+ assertEqualsToFile(goldenFile, dumpFile.readText())
+ }
+ }
+
+
private fun objCExportTestImpl(
suffix: String,
frameworkOpts: List<String>,