wip Implement Objective-C static cache compilation and linkage support
This change implements ObjCExport early caching support for Kotlin/Native compiler, allowing Objective-C bridges and type adapters to be generated per-library during static cache compilation, and resolved/linked at framework link time.
Key changes:
- Registered binary option flag 'objcExportCache'.
- Redirected output files of static caches with ObjC cache enabled to '.objc' subdirectory with '.objc.a' and '.objc.csv' extensions.
- Configured ObjCExport to translate the module of the library being cached.
- Partitioned static libraries inside Apple linker, forcing load of ObjC static caches ('.objc.a') using '-force_load'.
- Handled global interop symbol deduplication during cache creation.
- Filtered out category and method adapters for intrinsic/external functions lacking LLVM bytecode implementation (e.g. 'narrow').
- Filtered out file class type adapters and class/interface type adapters of already-cached libraries during final framework linkage.
- Fall back to compile type adapters for non-cached libraries inside the framework during linkage.
- Guarded constructor adapter generation against null adapters when LLVM implementation is missing.
- Import ObjC type adapters from dependency libraries' CSV metadata during static cache compilation, and skip generating/binding duplicate type adapters and writable type info symbols (like 'kotlin.Any') that are already defined in dependency caches.
- Handle external class writable type info symbols correctly by only declaring them as external instead of redefining them with zero initializers, unless compiling the final framework when the dependency has no ObjC cache.
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheBinariesResolver.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheBinariesResolver.kt
index 3eafe29..f5dbf9a 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheBinariesResolver.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheBinariesResolver.kt
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.backend.konan
import org.jetbrains.kotlin.konan.target.LinkerOutputKind
+import org.jetbrains.kotlin.library.KotlinLibrary
/**
* Check if we should link static caches into an object file before running full linkage.
@@ -34,6 +35,8 @@
internal fun resolveCacheBinaries(
cachedLibraries: CachedLibraries,
dependenciesTrackingResult: DependenciesTrackingResult,
+ objcExportCacheEnabled: Boolean = false,
+ allLibraries: List<KotlinLibrary> = emptyList()
): ResolvedCacheBinaries {
val staticCaches = mutableListOf<String>()
val dynamicCaches = mutableListOf<String>()
@@ -50,9 +53,30 @@
CachedLibraries.Kind.HEADER -> error("Header cache ${cache.path} cannot be used for linking")
}
- list += if (dependency.kind is DependenciesTracker.DependencyKind.CertainFiles && cache is CachedLibraries.Cache.PerFile)
+ val binaries = if (dependency.kind is DependenciesTracker.DependencyKind.CertainFiles && cache is CachedLibraries.Cache.PerFile)
dependency.kind.files.map { cache.getFileBinaryPath(it.name) }
else cache.binariesPaths
+
+ list += binaries
+ if (objcExportCacheEnabled && cache.kind == CachedLibraries.Kind.STATIC) {
+ val objcPath = cache.objcCachePath
+ objcPath?.let { list += it }
+ }
}
- return ResolvedCacheBinaries(static = staticCaches, dynamic = dynamicCaches)
+
+ if (objcExportCacheEnabled) {
+ allLibraries.forEach { library ->
+ val cache = cachedLibraries.getLibraryCache(library)
+ if (cache != null && cache.kind == CachedLibraries.Kind.STATIC) {
+ val objcPath = cache.objcCachePath
+ objcPath?.let {
+ if (!staticCaches.contains(it)) {
+ staticCaches += it
+ }
+ }
+ }
+ }
+ }
+
+ return ResolvedCacheBinaries(static = staticCaches.distinct(), dynamic = dynamicCaches.distinct())
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheStorage.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheStorage.kt
index ba4ec86e..16e364e 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheStorage.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheStorage.kt
@@ -17,6 +17,7 @@
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.library.impl.javaFile
import org.jetbrains.kotlin.library.isNativeStdlib
+import org.jetbrains.kotlin.backend.konan.objcexport.ExportedAdapterMetadata
import kotlin.random.Random
private fun NativeGenerationState.generateCacheMetadata(): CacheMetadata {
@@ -58,6 +59,9 @@
fun saveAdditionalCacheInfo() {
outputFiles.prepareTempDirectories()
+ if (generationState.config.objcExportCacheEnabled) {
+ saveObjCExportCacheCsv()
+ }
if (!generationState.config.produce.isHeaderCache) {
saveMetadata()
}
@@ -68,6 +72,15 @@
saveTrivialGetters()
}
+ private fun saveObjCExportCacheCsv() {
+ val csvFile = outputFiles.objcExportCacheCsvFile ?: return
+ csvFile.javaFile().bufferedWriter().use { writer ->
+ generationState.objCExport.exportedAdapters.forEach { metadata ->
+ writer.write("${metadata.objCName},${metadata.symbolName},${metadata.kind}\n")
+ }
+ }
+ }
+
private fun saveMetadata() {
outputFiles.cacheMetadata!!.javaFile().bufferedWriter().use {
CacheMetadataSerializer.serialize(it, generationState.generateCacheMetadata())
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheSupport.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheSupport.kt
index 56ecb98..96869bb 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheSupport.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CacheSupport.kt
@@ -15,6 +15,7 @@
import org.jetbrains.kotlin.cli.report
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.konan.config.*
+import org.jetbrains.kotlin.config.nativeBinaryOptions.BinaryOptions
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.KonanTarget
@@ -93,12 +94,12 @@
// Put the resulting library in the first cache directory.
val cacheDirectory = implicitCacheDirectories.firstOrNull() ?: return null
val singleFileStrategy = cacheDeserializationStrategy as? CacheDeserializationStrategy.SingleFile
- val baseLibraryCacheDirectory = cacheDirectory.child(
- if (singleFileStrategy == null)
- CachedLibraries.getCachedLibraryName(libraryToCache.klib)
- else
- CachedLibraries.getPerFileCachedLibraryName(libraryToCache.klib)
- )
+ val cacheName = if (singleFileStrategy == null)
+ CachedLibraries.getCachedLibraryName(libraryToCache.klib)
+ else
+ CachedLibraries.getPerFileCachedLibraryName(libraryToCache.klib)
+ val cacheNameWithSuffix = if (configuration.get(BinaryOptions.objcExportCache) == true) "$cacheName.objc" else cacheName
+ val baseLibraryCacheDirectory = cacheDirectory.child(cacheNameWithSuffix)
val singleFilePath = singleFileStrategy?.filePath
?: return baseLibraryCacheDirectory.absolutePath
@@ -131,7 +132,7 @@
implicitCacheDirectories = if (ignoreCachedLibraries) emptyList() else implicitCacheDirectories,
autoCacheDirectory = autoCacheDirectory,
autoCacheableFrom = if (ignoreCachedLibraries) emptyList() else autoCacheableFrom,
- libraryToCache = configuration.konanLibraryToAddToCache?.let { getLibrary(File(it)) },
+ libraryToCache = configuration.konanLibraryToAddToCache?.let { getLibrary(File(it)) }
)
}
@@ -145,7 +146,13 @@
val libraryToAddToCacheFile = File(it)
val libraryToAddToCache = getLibrary(libraryToAddToCacheFile)
val libraryCache = cachedLibraries.getLibraryCache(libraryToAddToCache, allowIncomplete = true)
- if (libraryCache is CachedLibraries.Cache.Monolithic)
+ val objcExportCacheEnabled = configuration.get(BinaryOptions.objcExportCache) == true
+ val alreadyCached = if (objcExportCacheEnabled) {
+ libraryCache?.objcCachePath != null
+ } else {
+ libraryCache is CachedLibraries.Cache.Monolithic
+ }
+ if (alreadyCached)
null
else {
val filesToCache = configuration.filesToCache
@@ -185,11 +192,14 @@
}
// Ensure not making cache for libraries that are already cached:
- libraryToCache?.klib?.let {
- val cache = cachedLibraries.getLibraryCache(it)
- if (cache is CachedLibraries.Cache.Monolithic) {
- configuration.reportCompilationErrorAndThrow("can't cache library '${it.location}' " +
- "that is already cached in '${cache.path}'")
+ val objcExportCacheEnabled = configuration.get(BinaryOptions.objcExportCache) == true
+ if (!objcExportCacheEnabled) {
+ libraryToCache?.klib?.let {
+ val cache = cachedLibraries.getLibraryCache(it)
+ if (cache is CachedLibraries.Cache.Monolithic) {
+ configuration.reportCompilationErrorAndThrow("can't cache library '${it.location}' " +
+ "that is already cached in '${cache.path}'")
+ }
}
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CachedLibraries.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CachedLibraries.kt
index 8f1439a..31cbef9 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CachedLibraries.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CachedLibraries.kt
@@ -59,6 +59,42 @@
val serializedEagerInitializedFiles by lazy { computeSerializedEagerInitializedFiles() }
val serializedTrivialGetters by lazy { computeSerializedTrivialGetters() }
+ val objcCachePath: String? by lazy {
+ if (kind == Kind.STATIC) {
+ val file = File(path)
+ if (file.name.endsWith(".objc.a")) {
+ file.absolutePath
+ } else {
+ val parent = file.parentFile
+ val grandParent = parent.parentFile
+ val objcGrandParent = File(grandParent.absolutePath + ".objc")
+ val objcParent = objcGrandParent.child(parent.name)
+ val objcFileName = file.name.substringBeforeLast(".") + ".objc.a"
+ val objcFile = objcParent.child(objcFileName)
+ if (objcFile.exists) objcFile.absolutePath else null
+ }
+ } else null
+ }
+
+ val objcCsvPath: String? by lazy {
+ if (kind == Kind.STATIC) {
+ val file = File(path)
+ if (file.name.endsWith(".objc.a")) {
+ val csvFileName = file.name.substringBeforeLast(".objc.a") + ".objc.csv"
+ val csvFile = file.parentFile.child(csvFileName)
+ if (csvFile.exists) csvFile.absolutePath else null
+ } else {
+ val parent = file.parentFile
+ val grandParent = parent.parentFile
+ val objcGrandParent = File(grandParent.absolutePath + ".objc")
+ val objcParent = objcGrandParent.child(parent.name)
+ val csvFileName = file.name.substringBeforeLast(".") + ".objc.csv"
+ val csvFile = objcParent.child(csvFileName)
+ if (csvFile.exists) csvFile.absolutePath else null
+ }
+ } else null
+ }
+
protected abstract fun computeBitcodeDependencies(): List<DependenciesTracker.UnresolvedDependency>
protected abstract fun computeBinariesPaths(): List<String>
protected abstract fun computeSerializedInlineFunctionBodies(): List<SerializedInlineFunctionReference>
@@ -75,6 +111,15 @@
class Monolithic(target: KonanTarget, kind: Kind, path: String)
: Cache(target, kind, path, File(path).parentFile.parentFile.absolutePath)
{
+ private val irDirectory: File by lazy {
+ val dir = File(path).absoluteFile.parentFile.parentFile
+ if (!dir.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).exists && dir.name.endsWith(".objc")) {
+ File(dir.absolutePath.substringBeforeLast(".objc"))
+ } else {
+ dir
+ }
+ }
+
override fun computeBitcodeDependencies(): List<DependenciesTracker.UnresolvedDependency> {
val directory = File(path).absoluteFile.parentFile
val data = directory.child(BITCODE_DEPENDENCIES_FILE_NAME).readStrings()
@@ -84,20 +129,17 @@
override fun computeBinariesPaths() = listOf(path)
override fun computeSerializedInlineFunctionBodies() = mutableListOf<SerializedInlineFunctionReference>().also {
- val directory = File(path).absoluteFile.parentFile.parentFile
- val data = directory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(INLINE_FUNCTION_BODIES_FILE_NAME).readBytes()
+ val data = irDirectory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(INLINE_FUNCTION_BODIES_FILE_NAME).readBytes()
InlineFunctionBodyReferenceSerializer.deserializeTo(data, it)
}
override fun computeSerializedClassFields() = mutableListOf<SerializedClassFields>().also {
- val directory = File(path).absoluteFile.parentFile.parentFile
- val data = directory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(CLASS_FIELDS_FILE_NAME).readBytes()
+ val data = irDirectory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(CLASS_FIELDS_FILE_NAME).readBytes()
ClassFieldsSerializer.deserializeTo(data, it)
}
override fun computeSerializedEagerInitializedFiles() = mutableListOf<SerializedEagerInitializedFile>().also {
- val directory = File(path).absoluteFile.parentFile.parentFile
- val data = directory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(EAGER_INITIALIZED_PROPERTIES_FILE_NAME).readBytes()
+ val data = irDirectory.child(PER_FILE_CACHE_IR_LEVEL_DIR_NAME).child(EAGER_INITIALIZED_PROPERTIES_FILE_NAME).readBytes()
EagerInitializedPropertySerializer.deserializeTo(data, it)
}
@@ -178,6 +220,7 @@
val baseName = getCachedLibraryName(library)
val dynamicFile = cacheBinaryPartDir.child(getArtifactName(target, baseName, CompilerOutputKind.DYNAMIC_CACHE))
val staticFile = cacheBinaryPartDir.child(getArtifactName(target, baseName, CompilerOutputKind.STATIC_CACHE))
+ val objcStaticFile = cacheBinaryPartDir.child(getArtifactName(target, "$baseName.objc", CompilerOutputKind.STATIC_CACHE))
val headerFile = cacheBinaryPartDir.child(getArtifactName(target, baseName, CompilerOutputKind.HEADER_CACHE))
if (dynamicFile.absolutePath in cacheBinaryPartDirContents && staticFile.absolutePath in cacheBinaryPartDirContents)
@@ -186,6 +229,7 @@
return when {
dynamicFile.absolutePath in cacheBinaryPartDirContents -> Cache.Monolithic(target, Kind.DYNAMIC, dynamicFile.absolutePath)
staticFile.absolutePath in cacheBinaryPartDirContents -> Cache.Monolithic(target, Kind.STATIC, staticFile.absolutePath)
+ objcStaticFile.absolutePath in cacheBinaryPartDirContents -> Cache.Monolithic(target, Kind.STATIC, objcStaticFile.absolutePath)
headerFile.absolutePath in cacheBinaryPartDirContents -> Cache.Monolithic(target, Kind.HEADER, headerFile.absolutePath)
else -> {
// When the per-file cache of a library is being rebuilt in parallel (one fragment per dirty file),
@@ -216,6 +260,7 @@
private fun KotlinLibrary.trySelectCacheAt(dirBuilder: (String) -> File?) =
sequenceOf(getPerFileCachedLibraryName(this), getCachedLibraryName(this))
+ .flatMap { sequenceOf(it, "$it.objc") }
.map(dirBuilder)
.mapNotNull { it?.trySelectCacheFor(this) }
.firstOrNull()
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt
index 3c8d363..7b2ed33 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/CompilerOutput.kt
@@ -199,18 +199,24 @@
val additionalModules = parseBitcodeFiles(additionalBitcodeFiles)
return LlvmModules(
runtimeModules.ifNotEmpty { this + generationState.generateRuntimeConstantsModule() } ?: emptyList(),
- additionalModules + listOfNotNull(patchObjCRuntimeModule(generationState))
+ additionalModules
)
}
private fun linkAllDependencies(generationState: NativeGenerationState, generatedBitcodeFiles: List<String>) {
+ val patchedObjCResult = patchObjCRuntimeModule(generationState)
+ val patchedModule = patchedObjCResult?.first
+ val patchedGlobalNames = patchedObjCResult?.second.orEmpty()
+
val (runtimeModules, additionalModules) = collectLlvmModules(generationState, generatedBitcodeFiles)
+ val allAdditionalModules = additionalModules + listOfNotNull(patchedModule)
+
// TODO: Possibly slow, maybe to a separate phase?
val optimizedRuntimeModules = linkRuntimeModules(generationState, runtimeModules)
// When the main module `generationState.llvmModule` is very large it is much faster to
// link all the auxiliary modules together first before linking with the main module.
- val linkedModules = (optimizedRuntimeModules + additionalModules).reduceOrNull { acc, module ->
+ val linkedModules = (optimizedRuntimeModules + allAdditionalModules).reduceOrNull { acc, module ->
val failed = llvmLinkModules2(generationState, acc, module)
if (failed != 0) {
error("Failed to link ${module.getName()}")
@@ -223,6 +229,13 @@
error("Failed to link runtime and additional modules into main module")
}
}
+
+ for (name in patchedGlobalNames) {
+ val global = LLVMGetNamedGlobal(generationState.llvmModule, name)
+ if (global != null) {
+ generationState.llvm.usedGlobals += global
+ }
+ }
}
internal fun insertAliasToEntryPoint(context: NativeBackendPhaseContext, module: LLVMModuleRef) {
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/NativeSecondStageCompilationConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/NativeSecondStageCompilationConfig.kt
index 1af02cb..116161c 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/NativeSecondStageCompilationConfig.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/NativeSecondStageCompilationConfig.kt
@@ -289,6 +289,10 @@
configuration.get(BinaryOptions.objcDisposeWithRunLoop) ?: true
}
+ val objcExportCacheEnabled: Boolean by lazy {
+ configuration.get(BinaryOptions.objcExportCache) ?: false
+ }
+
val objcEntryPoints: ObjCEntryPoints by lazy {
configuration
.get(BinaryOptions.objcExportEntryPointsPath)
@@ -420,7 +424,10 @@
internal val externalDependenciesFile = configuration.externalDependencies?.let(::File)
val fullExportedNamePrefix: String
- get() = configuration.fullExportedNamePrefix ?: implicitModuleName
+ get() = configuration.fullExportedNamePrefix
+ ?: configuration.bundleId
+ ?: configuration.get(BinaryOptions.bundleId)
+ ?: implicitModuleName
override val moduleId: String
get() = configuration.moduleName ?: implicitModuleName
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/OutputFiles.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/OutputFiles.kt
index 00a4a93..4ce58e1 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/OutputFiles.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/OutputFiles.kt
@@ -15,7 +15,17 @@
/**
* Creates and stores terminal compiler outputs.
*/
-class OutputFiles(val outputName: String, target: KonanTarget, val produce: CompilerOutputKind) {
+class OutputFiles(
+ val outputName: String,
+ target: KonanTarget,
+ val produce: CompilerOutputKind,
+ objcExportCacheEnabled: Boolean = false
+) {
+ private val adjustedOutputName = if (objcExportCacheEnabled && produce == CompilerOutputKind.STATIC_CACHE) {
+ if (outputName.endsWith(".objc")) outputName else "$outputName.objc"
+ } else {
+ outputName
+ }
private val prefix = produce.prefix(target)
private val suffix = produce.suffix(target)
@@ -23,33 +33,33 @@
/**
* Header file for dynamic library.
*/
- val cAdapterHeader by lazy { File("${outputName}_api.h") }
- val cAdapterDef by lazy { File("${outputName}.def") }
+ val cAdapterHeader by lazy { File("${adjustedOutputName}_api.h") }
+ val cAdapterDef by lazy { File("${adjustedOutputName}.def") }
/**
* Compiler's main output file.
*/
val mainFileName =
if (produce.isCache)
- outputName
+ adjustedOutputName
else
- outputName.fullOutputName()
+ adjustedOutputName.fullOutputName()
val mainFile = File(mainFileName)
- val perFileCacheFileName = File(outputName).absoluteFile.name
+ val perFileCacheFileName = File(adjustedOutputName).absoluteFile.name
- val cacheFileName = File((outputName).fullOutputName()).absoluteFile.name
+ val cacheFileName = File((adjustedOutputName).fullOutputName()).absoluteFile.name
private fun File.cacheBinaryPart() = this.child(CachedLibraries.PER_FILE_CACHE_BINARY_LEVEL_DIR_NAME)
private fun File.cacheIrPart() = this.child(CachedLibraries.PER_FILE_CACHE_IR_LEVEL_DIR_NAME)
- val dynamicCacheInstallName = File(outputName).cacheBinaryPart().child(cacheFileName).absolutePath
+ val dynamicCacheInstallName = File(adjustedOutputName).cacheBinaryPart().child(cacheFileName).absolutePath
val tempCacheDirectory =
if (produce.isCache)
- File(outputName + Random.nextLong().toString())
+ File(adjustedOutputName + Random.nextLong().toString())
else null
fun prepareTempDirectories() {
@@ -60,6 +70,10 @@
val nativeBinaryFile = tempCacheDirectory?.cacheBinaryPart()?.child(cacheFileName)?.absolutePath ?: mainFileName
+ val objcExportCacheCsvFile = tempCacheDirectory?.cacheBinaryPart()?.child(
+ cacheFileName.substringBeforeLast(".") + ".csv"
+ )
+
val symbolicInfoFile = "$nativeBinaryFile.dSYM"
val cacheMetadata = tempCacheDirectory?.child(CachedLibraries.METADATA_FILE_NAME)
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 1360f7b..0628bcc 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
@@ -543,7 +543,8 @@
configuration: CompilerConfiguration
): String? {
val argumentValue = arguments.bundleId
- return if (argumentValue != null && outputKind != CompilerOutputKind.FRAMEWORK) {
+ val objcExportCacheEnabled = configuration.get(BinaryOptions.objcExportCache) == true
+ return if (argumentValue != null && outputKind != CompilerOutputKind.FRAMEWORK && !objcExportCacheEnabled) {
configuration.report(KONAN_ARGUMENT_STRONG_WARNING, "Setting a bundle ID is only supported when producing a framework " +
"but the compiler is producing ${outputKind.name.lowercase()}")
null
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/NativeCompilerDriver.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/NativeCompilerDriver.kt
index 1821891..8e046a9 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/NativeCompilerDriver.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/NativeCompilerDriver.kt
@@ -115,8 +115,25 @@
val frontendOutput = performanceManager.tryMeasurePhaseTime(PhaseType.Analysis) { engine.runFrontend(config, environment) }
?: return
- val linkKlibsOutput = performanceManager.tryMeasurePhaseTime(PhaseType.IrLinking) { engine.linkKlibs(frontendOutput) }
- val backendContext = createBackendContext(config, frontendOutput, linkKlibsOutput)
+ val objCExportedInterface = if (config.objcExportCacheEnabled) {
+ performanceManager.tryMeasurePhaseTime(PhaseType.TranslationToIr) {
+ engine.runPhase(ProduceObjCExportInterfacePhase, frontendOutput)
+ }
+ } else null
+
+ val [linkKlibsOutput, objCCodeSpec] = performanceManager.tryMeasurePhaseTime(PhaseType.IrLinking) {
+ engine.linkKlibs(frontendOutput) {
+ if (objCExportedInterface != null) {
+ it.runPhase(CreateObjCExportCodeSpecPhase, objCExportedInterface)
+ } else {
+ null
+ }
+ }
+ }
+ val backendContext = createBackendContext(config, frontendOutput, linkKlibsOutput) {
+ it.objCExportedInterface = objCExportedInterface
+ it.objCExportCodeSpec = objCCodeSpec
+ }
engine.runBackend(backendContext, linkKlibsOutput.irModule, performanceManager)
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt
index 735d997..29c541d 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/BackendPhases.kt
@@ -10,6 +10,7 @@
import org.jetbrains.kotlin.backend.common.phaser.createSimpleNamedCompilerPhase
import org.jetbrains.kotlin.backend.konan.NativeGenerationState
import org.jetbrains.kotlin.backend.konan.OutputFiles
+import org.jetbrains.kotlin.config.nativeBinaryOptions.BinaryOptions
import org.jetbrains.kotlin.backend.konan.driver.NativeBackendPhaseContext
import org.jetbrains.kotlin.backend.konan.driver.utilities.getDefaultIrActions
import org.jetbrains.kotlin.backend.konan.ir.BackendNativeSymbols
@@ -82,7 +83,12 @@
"CreateTestBundlePhase",
) { context, input ->
val config = context.config
- val output = OutputFiles(config.outputPath, config.target, config.produce).mainFile
+ val output = OutputFiles(
+ config.outputPath,
+ config.target,
+ config.produce,
+ objcExportCacheEnabled = config.configuration.get(BinaryOptions.objcExportCache) == true
+ ).mainFile
createTestBundle(config, input.moduleDescriptor, output)
}
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 c67239c..15f3618 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
@@ -8,6 +8,7 @@
import org.jetbrains.kotlin.backend.common.phaser.createSimpleNamedCompilerPhase
import org.jetbrains.kotlin.backend.konan.LinkKlibsContext
import org.jetbrains.kotlin.backend.konan.OutputFiles
+import org.jetbrains.kotlin.config.nativeBinaryOptions.BinaryOptions
import org.jetbrains.kotlin.backend.konan.driver.NativeBackendPhaseContext
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportCodeSpec
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportedInterface
@@ -40,7 +41,12 @@
) { context, input ->
val config = context.config
// TODO: Share this instance between multiple contexts (including NativeGenerationState)?
- val outputFiles = OutputFiles(config.outputPath, config.target, config.produce)
+ val outputFiles = OutputFiles(
+ config.outputPath,
+ config.target,
+ config.produce,
+ objcExportCacheEnabled = config.configuration.get(BinaryOptions.objcExportCache) == true
+ )
createObjCFramework(config, input.moduleDescriptor, input.exportedInterface, outputFiles.mainFile)
}
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/TopLevelPhases.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/TopLevelPhases.kt
index 5a6ef5e..6dab69e 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/TopLevelPhases.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/TopLevelPhases.kt
@@ -9,6 +9,7 @@
import org.jetbrains.kotlin.backend.konan.*
import org.jetbrains.kotlin.backend.konan.driver.PerformanceManagerContext
import org.jetbrains.kotlin.backend.konan.driver.NativeBackendPhaseContext
+import org.jetbrains.kotlin.config.nativeBinaryOptions.BinaryOptions
import org.jetbrains.kotlin.backend.konan.driver.utilities.CExportFiles
import org.jetbrains.kotlin.backend.konan.driver.utilities.createTempFiles
import org.jetbrains.kotlin.backend.konan.ir.konanLibrary
@@ -82,7 +83,12 @@
fun createGenerationState(fragment: BackendJobFragment): NativeGenerationState {
val outputPath = config.cacheSupport.tryGetImplicitOutput(fragment.cacheDeserializationStrategy) ?: config.outputPath
- val outputFiles = OutputFiles(outputPath, config.target, config.produce)
+ val outputFiles = OutputFiles(
+ outputPath,
+ config.target,
+ config.produce,
+ objcExportCacheEnabled = config.configuration.get(BinaryOptions.objcExportCache) == true
+ )
val generationState = NativeGenerationState(context.config, backendContext,
fragment.cacheDeserializationStrategy, fragment.dependenciesTracker, fragment.llvmModuleSpecification, outputFiles,
llvmModuleName = "out", // TODO: Currently, all llvm modules are named as "out" which might lead to collisions.
@@ -279,7 +285,12 @@
val tempFiles = createTempFiles(context.config, null)
val bitcodeFile = tempFiles.create(context.config.shortModuleName ?: "out", ".bc").javaFile()
val outputPath = context.config.outputPath
- val outputFiles = OutputFiles(outputPath, context.config.target, context.config.produce)
+ val outputFiles = OutputFiles(
+ outputPath,
+ context.config.target,
+ context.config.produce,
+ objcExportCacheEnabled = context.config.configuration.get(BinaryOptions.objcExportCache) == true
+ )
bitcodeEngine.runBitcodePostProcessing()
runAndMeasurePhase(WriteBitcodeFilePhase, WriteBitcodeFileInput(context.llvm.module, bitcodeFile))
val moduleCompilationOutput = ModuleCompilationOutput(bitcodeFile, dependencies)
@@ -403,8 +414,16 @@
val compilationResult = temporaryFiles.create(File(outputFiles.nativeBinaryFile).name, ".o").javaFile()
runAndMeasurePhase(ObjectFilesPhase, ObjectFilesPhaseInput(moduleCompilationOutput.bitcodeFile, compilationResult))
val linkerOutputKind = determineLinkerOutput(context)
+ val objcExportCacheEnabled = context.config.configuration.get(BinaryOptions.objcExportCache) == true
val [linkerInput, cacheBinaries] = run {
- val resolvedCacheBinaries by lazy { resolveCacheBinaries(context.config.cachedLibraries, moduleCompilationOutput.dependenciesTrackingResult) }
+ val resolvedCacheBinaries by lazy {
+ resolveCacheBinaries(
+ context.config.cachedLibraries,
+ moduleCompilationOutput.dependenciesTrackingResult,
+ objcExportCacheEnabled,
+ context.config.resolvedLibraries.getFullList()
+ )
+ }
when {
context.config.produce == CompilerOutputKind.STATIC_CACHE -> {
compilationResult to ResolvedCacheBinaries(emptyList(), emptyList())
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/linkKlibs.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/linkKlibs.kt
index f3b553f..25f44c1 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/linkKlibs.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/linkKlibs.kt
@@ -16,6 +16,7 @@
import org.jetbrains.kotlin.cli.common.diagnosticsCollector
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.languageVersionSettings
+import org.jetbrains.kotlin.config.nativeBinaryOptions.BinaryOptions
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.ir.IrBuiltIns
import org.jetbrains.kotlin.ir.IrElement
@@ -104,7 +105,10 @@
val stdlibIsCached = stdlibModule.konanLibrary?.let { config.cachedLibraries.isLibraryCached(it) } == true
val stdlibIsBeingCached = libraryToCacheModule == stdlibModule
- require(!(stdlibIsCached && stdlibIsBeingCached)) { "The cache for stdlib is already built" }
+ val objcExportCacheEnabled = config.configuration.get(BinaryOptions.objcExportCache) == true
+ if (!objcExportCacheEnabled) {
+ require(!(stdlibIsCached && stdlibIsBeingCached)) { "The cache for stdlib is already built" }
+ }
val stubGenerator = DeclarationStubGeneratorImpl(
moduleDescriptor, symbolTable,
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BinaryInterface.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BinaryInterface.kt
index 425d868..4e7a51d 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BinaryInterface.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/BinaryInterface.kt
@@ -101,6 +101,12 @@
return "ktypew:" + this.fqNameForIrSerialization.toString()
}
+internal val IrClass.objCTypeAdapterSymbolName: String
+ get() {
+ assert (this.isExported())
+ return "kobjcadapter:" + this.fqNameForIrSerialization.toString()
+ }
+
internal val IrClass.globalObjectStorageSymbolName: String
get() {
assert (this.isExported())
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/ObjCDataGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/ObjCDataGenerator.kt
index 96248bd..7dd3520 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/ObjCDataGenerator.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/ObjCDataGenerator.kt
@@ -190,6 +190,9 @@
LLVMSetInitializer(classGlobal.llvm, classObjectValue.llvm)
LLVMSetSection(classGlobal.llvm, "__DATA, __objc_data")
LLVMSetAlignment(classGlobal.llvm, LLVMABIAlignmentOfType(runtime.targetData, classObjectType))
+ if (context.config.objcExportCacheEnabled) {
+ LLVMSetLinkage(classGlobal.llvm, LLVMLinkage.LLVMWeakAnyLinkage)
+ }
llvm.usedGlobals.add(classGlobal.llvm)
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/linkObjC.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/linkObjC.kt
index b1bb8f8..8fb38b2 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/linkObjC.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objc/linkObjC.kt
@@ -13,10 +13,13 @@
import org.jetbrains.kotlin.backend.konan.llvm.runtime.RuntimeModule
import org.jetbrains.kotlin.backend.konan.objcexport.NSNumberKind
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportNamer
+import org.jetbrains.kotlin.konan.target.CompilerOutputKind
-internal fun patchObjCRuntimeModule(generationState: NativeGenerationState): LLVMModuleRef? {
+internal fun patchObjCRuntimeModule(generationState: NativeGenerationState): Pair<LLVMModuleRef, List<String>>? {
val config = generationState.config
- if (!(config.isFinalBinary && config.target.family.isAppleFamily)) return null
+ if (config.produce == CompilerOutputKind.FRAMEWORK && config.objcExportCacheEnabled && config.cachedLibraries.hasStaticCaches) return null
+ if (config.objcExportCacheEnabled && config.produce == CompilerOutputKind.STATIC_CACHE && !generationState.producedLlvmModuleContainsStdlib) return null
+ if (!((config.isFinalBinary || config.objcExportCacheEnabled) && config.target.family.isAppleFamily)) return null
val patchBuilder = PatchBuilder(generationState.objCExport.namer)
patchBuilder.addObjCPatches()
@@ -24,8 +27,8 @@
val bitcodeFile = generationState.runtimeModulesConfig.absolutePathFor(RuntimeModule.OBJC)
val parsedModule = parseBitcodeFile(generationState, generationState.diagnosticReporter, generationState.llvmContext, bitcodeFile)
- patchBuilder.buildAndApply(parsedModule, generationState)
- return parsedModule
+ val patchedNames = patchBuilder.buildAndApply(parsedModule, generationState)
+ return parsedModule to patchedNames
}
private class PatchBuilder(val objCExportNamer: ObjCExportNamer) {
@@ -127,7 +130,8 @@
}
}
-private fun PatchBuilder.buildAndApply(llvmModule: LLVMModuleRef, state: NativeGenerationState) {
+private fun PatchBuilder.buildAndApply(llvmModule: LLVMModuleRef, state: NativeGenerationState): List<String> {
+ val patchedNames = mutableListOf<String>()
val nameToGlobalPatch = globalPatches.associateNonRepeatingBy { it.globalName }
val sectionToValueToLiteralPatch = literalPatches.groupBy { it.generator.section }
@@ -139,17 +143,25 @@
val globals = generateSequence(LLVMGetFirstGlobal(llvmModule), { LLVMGetNextGlobal(it) }).toList()
for (global in globals) {
- val initializer = LLVMGetInitializer(global) ?: continue
val name = LLVMGetValueName(global)?.toKString().orEmpty()
val globalPatch = nameToGlobalPatch[name]
if (globalPatch != null) {
LLVMSetValueName(global, globalPatch.newGlobalName)
+ val linkage = if (state.config.objcExportCacheEnabled && !state.config.isFinalBinary)
+ LLVMLinkage.LLVMWeakAnyLinkage
+ else
+ LLVMLinkage.LLVMExternalLinkage
+ LLVMSetLinkage(global, linkage)
+ LLVMSetVisibility(global, LLVMVisibility.LLVMDefaultVisibility)
+ patchedNames += globalPatch.newGlobalName
unusedPatches -= globalPatch
- } else if (PatchBuilder.GlobalKind.values().any { name.startsWith(it.prefix) }) {
+ } else if (PatchBuilder.GlobalKind.values().any { name.startsWith("${it.prefix}Kotlin") }) {
error("Objective-C global '$name' is not patched")
}
+ val initializer = LLVMGetInitializer(global) ?: continue
+
val section = LLVMGetSection(global)?.toKString()
sectionToValueToLiteralPatch[section]?.let { valueToLiteralPatch ->
val value = getStringValue(initializer)
@@ -163,9 +175,16 @@
}
}
- unusedPatches.firstOrNull()?.let {
- error("Patch is not applied: $it")
+ if (state.config.objcExportCacheEnabled && !state.config.isFinalBinary) {
+ val functions = generateSequence(LLVMGetFirstFunction(llvmModule), { LLVMGetNextFunction(it) }).toList()
+ for (function in functions) {
+ if (LLVMIsDeclaration(function) == 0) {
+ LLVMSetLinkage(function, LLVMLinkage.LLVMWeakAnyLinkage)
+ }
+ }
}
+
+ return patchedNames
}
private fun getStringValue(initializer: LLVMValueRef): String? = when (LLVMGetValueKind(initializer)) {
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt
index 2cc6956..21802f7 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/ObjCExportCodeGenerator.kt
@@ -21,6 +21,8 @@
import org.jetbrains.kotlin.backend.konan.lower.getLoweredConstructorFunction
import org.jetbrains.kotlin.backend.konan.lower.getObjectClassInstanceFunction
import org.jetbrains.kotlin.backend.konan.objcexport.*
+import org.jetbrains.kotlin.konan.target.CompilerOutputKind
+import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
@@ -386,7 +388,16 @@
}
private fun generateTypeAdaptersForKotlinTypes(spec: ObjCExportCodeSpec?): List<ObjCTypeAdapter> {
- val types = spec?.types.orEmpty() + objCClassForAny
+ val isStdlib = generationState.config.libraryToCache?.klib == context.stdlibModule.konanLibrary
+ val earlyNaming = generationState.config.objcExportCacheEnabled
+ val stdlibHasObjCCache = context.stdlibModule.konanLibrary?.let {
+ generationState.config.cachedLibraries.getLibraryCache(it)?.objcCachePath != null
+ } == true
+ val hasAnyInSpec = spec?.types.orEmpty().any {
+ it.irClassSymbol == context.irBuiltIns.anyClass
+ }
+ val shouldAppendAny = (!earlyNaming || isStdlib || !stdlibHasObjCCache) && !hasAnyInSpec
+ val types = spec?.types.orEmpty() + if (shouldAppendAny) listOf(objCClassForAny) else emptyList()
val allReverseAdapters = createReverseAdapters(types)
@@ -425,8 +436,66 @@
emitTypeAdapters(objCTypeAdapters)
}
+ private fun filterOutCachedTypes(spec: ObjCExportCodeSpec?): ObjCExportCodeSpec? {
+ if (spec == null) return null
+ val filteredTypes = spec.types.mapNotNull { type ->
+ val irClass = type.irClassSymbol.owner
+ val klib = irClass.konanLibrary
+ val hasObjCCache = klib?.let {
+ generationState.config.cachedLibraries.getLibraryCache(it)?.objcCachePath != null
+ } == true
+ if (!hasObjCCache) {
+ type
+ } else if (type is ObjCClassForKotlinClass && type.categoryMethods.isNotEmpty()) {
+ ObjCClassForKotlinClass(
+ type.binaryName,
+ type.irClassSymbol,
+ methods = emptyList(),
+ categoryMethods = type.categoryMethods,
+ superClassNotAny = type.superClassNotAny
+ )
+ } else {
+ null
+ }
+ }
+ val filteredFiles = spec.files.filter { file ->
+ val klib = file.klib
+ val hasObjCCache = klib?.let {
+ generationState.config.cachedLibraries.getLibraryCache(it)?.objcCachePath != null
+ } == true
+ !hasObjCCache
+ }
+ return ObjCExportCodeSpec(filteredFiles, filteredTypes)
+ }
+
internal fun generate(spec: ObjCExportCodeSpec?) {
- generateTypeAdapters(spec)
+ val earlyNaming = generationState.config.objcExportCacheEnabled
+ if (earlyNaming) {
+ importDependencyAdapters()
+ }
+ if (earlyNaming && (generationState.config.produce == CompilerOutputKind.FRAMEWORK || generationState.config.produce == CompilerOutputKind.STATIC_CACHE)) {
+ emitSortedAdaptersTables()
+ generateTypeAdapters(filterOutCachedTypes(spec))
+ } else {
+ generateTypeAdapters(spec)
+ }
+
+ if (earlyNaming && generationState.config.produce == CompilerOutputKind.STATIC_CACHE) {
+ if (generationState.producedLlvmModuleContainsStdlib) {
+ NSNumberKind.values().mapNotNull { it.mappedKotlinClassId }.forEach {
+ dataGenerator.exportClass(namer.numberBoxName(it).binaryName)
+ }
+ dataGenerator.exportClass(namer.mutableSetName.binaryName)
+ dataGenerator.exportClass(namer.mutableMapName.binaryName)
+ dataGenerator.exportClass(namer.kotlinAnyName.binaryName)
+ dataGenerator.exportClass("${namer.topLevelNamePrefix}KotlinEnum")
+ dataGenerator.exportClass("${namer.topLevelNamePrefix}KotlinArray")
+ dataGenerator.exportClass("${namer.topLevelNamePrefix}KotlinEnumCompanion")
+ } else {
+ dataGenerator.exportClass(namer.kotlinAnyName.binaryName)
+ }
+ return
+ }
NSNumberKind.values().mapNotNull { it.mappedKotlinClassId }.forEach {
dataGenerator.exportClass(namer.numberBoxName(it).binaryName)
@@ -449,12 +518,23 @@
}
private fun emitTypeAdapters(objCTypeAdapters: List<ObjCTypeAdapter>) {
- val placedClassAdapters = mutableMapOf<String, ConstPointer>()
- val placedInterfaceAdapters = mutableMapOf<String, ConstPointer>()
+ val placedClassAdapters = mutableListOf<Pair<String, ConstPointer>>()
+ val placedInterfaceAdapters = mutableListOf<Pair<String, ConstPointer>>()
+ val earlyNaming = generationState.config.objcExportCacheEnabled
objCTypeAdapters.forEach { adapter ->
- val typeAdapter = staticData.placeGlobal("", adapter).pointer
val irClass = adapter.irClass
+ val name = if (earlyNaming) {
+ if (irClass != null) irClass.objCTypeAdapterSymbolName else "kobjcadapter:objcname:${adapter.objCName}"
+ } else {
+ ""
+ }
+ val existingGlobal = if (name.isNotEmpty()) staticData.getGlobal(name) else null
+ val typeAdapter = if (existingGlobal != null) {
+ existingGlobal.pointer
+ } else {
+ staticData.placeGlobal(name, adapter, isExported = name.isNotEmpty()).pointer
+ }
val descriptorToAdapter = if (irClass?.isInterface == true) {
placedInterfaceAdapters
@@ -462,12 +542,22 @@
// Objective-C class for Kotlin class or top-level declarations.
placedClassAdapters
}
- descriptorToAdapter[adapter.objCName] = typeAdapter
+ descriptorToAdapter += (adapter.objCName to typeAdapter)
+
+ if (name.isNotEmpty()) {
+ val kind = if (irClass?.isInterface == true) "interface" else "class"
+ generationState.objCExport.exportedAdapters.add(
+ ExportedAdapterMetadata(adapter.objCName, name, kind)
+ )
+ }
if (irClass != null) {
if (!generationState.llvmModuleSpecification.importsKotlinDeclarationsFromOtherSharedLibraries()) {
try {
- codegen.bindObjCExportTypeAdapterTo(irClass, typeAdapter)
+ val existingGlobal = if (name.isNotEmpty()) staticData.getGlobal(name) else null
+ if (existingGlobal == null) {
+ codegen.bindObjCExportTypeAdapterTo(irClass, typeAdapter)
+ }
} catch (_: WritableTypeInfoOverrideError) {
// ObjCExport never tried to catch this error, so ignore.
}
@@ -478,24 +568,11 @@
}
}
- fun emitSortedAdapters(nameToAdapter: Map<String, ConstPointer>, prefix: String) {
- val sortedAdapters = nameToAdapter.toList().sortedBy { it.first }.map {
- it.second
- }
-
- if (sortedAdapters.isNotEmpty()) {
- val type = sortedAdapters.first().llvmType
- val sortedAdaptersPointer = staticData.placeGlobalConstArray("", type, sortedAdapters)
-
- // Note: this globals replace runtime globals with weak linkage:
- codegen.replaceExternalWeakOrCommonGlobalFromNativeRuntime(prefix, sortedAdaptersPointer)
- codegen.replaceExternalWeakOrCommonGlobalFromNativeRuntime("${prefix}Num", llvm.constInt32(sortedAdapters.size))
- }
+ if (!earlyNaming) {
+ emitSortedAdapters(placedClassAdapters, "Kotlin_ObjCExport_sortedClassAdapters")
+ emitSortedAdapters(placedInterfaceAdapters, "Kotlin_ObjCExport_sortedProtocolAdapters")
}
- emitSortedAdapters(placedClassAdapters, "Kotlin_ObjCExport_sortedClassAdapters")
- emitSortedAdapters(placedInterfaceAdapters, "Kotlin_ObjCExport_sortedProtocolAdapters")
-
if (generationState.llvmModuleSpecification.importsKotlinDeclarationsFromOtherSharedLibraries()) {
codegen.replaceExternalWeakOrCommonGlobalFromNativeRuntime(
"Kotlin_ObjCExport_initTypeAdapters",
@@ -504,6 +581,55 @@
}
}
+ private fun emitSortedAdapters(adaptersList: List<Pair<String, ConstPointer>>, prefix: String) {
+ val sortedAdapters = adaptersList.sortedBy { it.first }.map {
+ it.second
+ }
+
+ if (sortedAdapters.isNotEmpty()) {
+ val type = sortedAdapters.first().llvmType
+ val sortedAdaptersPointer = staticData.placeGlobalConstArray("", type, sortedAdapters)
+
+ // Note: this globals replace runtime globals with weak linkage:
+ codegen.replaceExternalWeakOrCommonGlobalFromNativeRuntime(prefix, sortedAdaptersPointer)
+ codegen.replaceExternalWeakOrCommonGlobalFromNativeRuntime("${prefix}Num", llvm.constInt32(sortedAdapters.size))
+ }
+ }
+
+ private val placedClassAdapters = mutableListOf<Pair<String, ConstPointer>>()
+ private val placedInterfaceAdapters = mutableListOf<Pair<String, ConstPointer>>()
+
+ private fun importDependencyAdapters() {
+ // Read CSV files from all cached dependencies:
+ val cachedLibraries = generationState.config.cachedLibraries
+ generationState.config.resolvedLibraries.getFullList().forEach { library ->
+ val cache = cachedLibraries.getLibraryCache(library)
+ val csvPath = cache?.objcCsvPath ?: return@forEach
+ val file = File(csvPath)
+ if (file.exists) {
+ file.forEachLine { line ->
+ val parts = line.split(',')
+ if (parts.size == 3) {
+ val objcName = parts[0]
+ val symbolName = parts[1]
+ val kind = parts[2]
+ val symbol = constPointer(codegen.importObjCGlobal(symbolName, llvm.pointerType))
+ if (kind == "class") {
+ placedClassAdapters += (objcName to symbol)
+ } else {
+ placedInterfaceAdapters += (objcName to symbol)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun emitSortedAdaptersTables() {
+ emitSortedAdapters(placedClassAdapters, "Kotlin_ObjCExport_sortedClassAdapters")
+ emitSortedAdapters(placedInterfaceAdapters, "Kotlin_ObjCExport_sortedProtocolAdapters")
+ }
+
private fun emitKt42254Hint() {
if (determineLinkerOutput(context) == LinkerOutputKind.STATIC_LIBRARY) {
// Might be affected by https://youtrack.jetbrains.com/issue/KT-42254.
@@ -562,7 +688,7 @@
)
}
- internal val directMethodAdapters = mutableMapOf<DirectAdapterRequest, ObjCToKotlinMethodAdapter>()
+ internal val directMethodAdapters = mutableMapOf<DirectAdapterRequest, ObjCToKotlinMethodAdapter?>()
internal val exceptionTypeInfoArrays = mutableMapOf<IrFunction, ConstPointer>()
internal val typeInfoArrays = mutableMapOf<Set<IrClass>, ConstPointer>()
@@ -1352,29 +1478,39 @@
baseMethod: ObjCMethodSpec.BaseMethod<*>
) = createMethodAdapter(DirectAdapterRequest(implementation, baseMethod))
+private fun ObjCExportCodeGenerator.hasLLVMImplementation(target: IrFunction): Boolean {
+ val simpleFunction = target as? IrSimpleFunction
+ ?: context.getLoweredConstructorFunction(target as IrConstructor)
+ return codegen.llvmFunctionOrNull(simpleFunction) != null
+}
+
private fun ObjCExportCodeGenerator.createFinalMethodAdapter(
baseMethod: ObjCMethodSpec.BaseMethod<IrSimpleFunctionSymbol>
-): ObjCToKotlinMethodAdapter {
+): ObjCToKotlinMethodAdapter? {
val irFunction = baseMethod.owner
require(irFunction.modality == Modality.FINAL)
+ if (!hasLLVMImplementation(irFunction)) return null
return createMethodAdapter(irFunction, baseMethod)
}
private fun ObjCExportCodeGenerator.createMethodAdapter(
request: DirectAdapterRequest
-): ObjCToKotlinMethodAdapter = this.directMethodAdapters.getOrPut(request) {
+): ObjCToKotlinMethodAdapter? = this.directMethodAdapters.getOrPut(request) {
+ val target = request.base.owner
+ val impl = request.implementation ?: target
+ if (!hasLLVMImplementation(impl)) return@getOrPut null
val selectorName = request.base.selector
val methodBridge = request.base.bridge
- val imp = generateObjCImp(request.implementation, request.base.owner, methodBridge)
+ val imp = generateObjCImp(request.implementation, target, methodBridge)
objCToKotlinMethodAdapter(selectorName, methodBridge, imp)
}
private fun ObjCExportCodeGenerator.createConstructorAdapter(
baseMethod: ObjCMethodSpec.BaseMethod<IrConstructorSymbol>
-): ObjCToKotlinMethodAdapter = createMethodAdapter(baseMethod.owner, baseMethod)
+): ObjCToKotlinMethodAdapter? = createMethodAdapter(baseMethod.owner, baseMethod)
private fun ObjCExportCodeGenerator.createArrayConstructorAdapter(
baseMethod: ObjCMethodSpec.BaseMethod<IrConstructorSymbol>
@@ -1421,7 +1557,7 @@
): ObjCTypeAdapter {
val name = fileClass.binaryName
- val adapters = fileClass.methods.map { createFinalMethodAdapter(it.baseMethod) }
+ val adapters = fileClass.methods.mapNotNull { createFinalMethodAdapter(it.baseMethod) }
return codegen.ObjCTypeAdapter(
irClass = null,
@@ -1449,7 +1585,7 @@
type.methods.forEach {
when (it) {
is ObjCInitMethodForKotlinConstructor -> {
- adapters += createConstructorAdapter(it.baseMethod)
+ createConstructorAdapter(it.baseMethod)?.let { adapter -> adapters += adapter }
}
is ObjCGetterForNSEnumType -> {
adapters += createNSEnumAdapter(it.symbol, it.bridge, it.selector)
@@ -1482,8 +1618,11 @@
if (type is ObjCClassForKotlinClass) {
type.categoryMethods.forEach {
- adapters += createFinalMethodAdapter(it.baseMethod)
- additionalReverseAdapters += nonOverridableAdapter(it.baseMethod.selector, hasSelectorAmbiguity = false)
+ val adapter = createFinalMethodAdapter(it.baseMethod)
+ if (adapter != null) {
+ adapters += adapter
+ additionalReverseAdapters += nonOverridableAdapter(it.baseMethod.selector, hasSelectorAmbiguity = false)
+ }
}
adapters += createDirectAdapters(type, superClass)
@@ -1662,7 +1801,7 @@
val inheritedAdapters = superClass?.getAllRequiredDirectAdapters().orEmpty().toSet()
val requiredAdapters = typeDeclaration.getAllRequiredDirectAdapters() - inheritedAdapters
- return requiredAdapters.distinctBy { it.base.selector }.map { createMethodAdapter(it) }
+ return requiredAdapters.distinctBy { it.base.selector }.mapNotNull { createMethodAdapter(it) }
}
private fun ObjCExportCodeGenerator.findImplementation(irClass: IrClass, method: IrSimpleFunction, context: Context): IrSimpleFunction? {
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/WritableTypeInfo.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/WritableTypeInfo.kt
index 4948685..548a9e0 100644
--- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/WritableTypeInfo.kt
+++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/objcexport/WritableTypeInfo.kt
@@ -18,6 +18,9 @@
import org.jetbrains.kotlin.backend.konan.llvm.llvmType
import org.jetbrains.kotlin.backend.konan.llvm.replaceExternalWeakOrCommonGlobal
import org.jetbrains.kotlin.backend.konan.llvm.writableTypeInfoSymbolName
+import org.jetbrains.kotlin.backend.konan.ir.konanLibrary
+import org.jetbrains.kotlin.konan.target.CompilerOutputKind
+import org.jetbrains.kotlin.library.uniqueName
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.util.isInterface
import org.jetbrains.kotlin.ir.util.kotlinFqName
@@ -56,7 +59,18 @@
* If [irClass] is exported, its [WritableTypeInfoPointer] can later be overridden once.
*/
internal fun ContextUtils.generateWritableTypeInfoForClass(irClass: IrClass): WritableTypeInfoPointer? = runtime.writableTypeInfoType?.let { type ->
- if (!irClass.isExported()) {
+ val isExternal = isExternal(irClass)
+ val hasObjCCache = irClass.konanLibrary?.let {
+ generationState.config.cachedLibraries.getLibraryCache(it)?.objcCachePath != null
+ } == true
+ val shouldDefineExternal = isExternal && !hasObjCCache && generationState.config.produce == CompilerOutputKind.FRAMEWORK
+ if (isExternal && !shouldDefineExternal) {
+ if (!irClass.isExported()) {
+ null
+ } else {
+ OverridableWritableTypeInfo(staticData.createGlobal(type, irClass.writableTypeInfoSymbolName, isExported = true))
+ }
+ } else if (!irClass.isExported()) {
// If the class not exported, its WritableTypeInfo cannot be replaced
FixedWritableTypeInfo(staticData.createGlobal(type, "").apply {
setZeroInitializer()
@@ -152,4 +166,4 @@
val writableTypeInfoType = runtime.writableTypeInfoType!!
return Struct(writableTypeInfoType, objCExportAddition)
-}
\ No newline at end of file
+}
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 bf4fb46..b0b4eb1 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
@@ -9,6 +9,7 @@
import org.jetbrains.kotlin.backend.konan.descriptors.getPackageFragments
import org.jetbrains.kotlin.backend.konan.descriptors.isInterface
import org.jetbrains.kotlin.backend.konan.driver.NativeBackendPhaseContext
+import org.jetbrains.kotlin.backend.konan.ir.konanLibrary
import org.jetbrains.kotlin.backend.konan.llvm.CodeGenerator
import org.jetbrains.kotlin.backend.konan.llvm.objcexport.ObjCExportBlockCodeGenerator
import org.jetbrains.kotlin.backend.konan.llvm.objcexport.ObjCExportCodeGenerator
@@ -40,7 +41,8 @@
): ObjCExportedInterface {
val config = context.config
require(config.target.family.isAppleFamily)
- require(config.produce == CompilerOutputKind.FRAMEWORK)
+ val objcExportCacheEnabled = config.configuration.get(BinaryOptions.objcExportCache) == true
+ require(config.produce == CompilerOutputKind.FRAMEWORK || (config.produce == CompilerOutputKind.STATIC_CACHE && objcExportCacheEnabled))
val topLevelNamePrefix = context.objCExportTopLevelNamePrefix
@@ -49,7 +51,15 @@
// and can't do this per-module, e.g. due to global name conflict resolution.
val unitSuspendFunctionExport = config.unitSuspendFunctionObjCExport
- val moduleDescriptors = listOf(moduleDescriptor) + moduleDescriptor.getExportedDependencies(config)
+ val libraryToCacheModule = config.libraryToCache?.klib?.let { klib ->
+ moduleDescriptor.allDependencyModules.singleOrNull { module -> module.konanLibrary == klib }
+ ?: error("Expected a single module for library to cache ${klib.libraryFile.absolutePath}, but found none or multiple")
+ }
+ val moduleDescriptors = if (objcExportCacheEnabled && config.produce == CompilerOutputKind.STATIC_CACHE) {
+ listOfNotNull(libraryToCacheModule)
+ } else {
+ listOf(moduleDescriptor) + moduleDescriptor.getExportedDependencies(config)
+ }
val entryPoints = config.objcEntryPoints
val expandEntryPoints = config.configuration.getBoolean(BinaryOptions.objcExportExpandEntryPoints)
val effectiveEntryPoints = if (entryPoints != ObjCEntryPoints.ALL && expandEntryPoints) {
@@ -170,6 +180,8 @@
}
// TODO: No need for such class in dynamic driver.
+internal data class ExportedAdapterMetadata(val objCName: String, val symbolName: String, val kind: String)
+
internal class ObjCExport(
private val generationState: NativeGenerationState,
private val moduleDescriptor: ModuleDescriptor,
@@ -180,6 +192,8 @@
private val target get() = config.target
private val topLevelNamePrefix get() = generationState.objCExportTopLevelNamePrefix
+ val exportedAdapters = mutableListOf<ExportedAdapterMetadata>()
+
lateinit var namer: ObjCExportNamer
internal fun generate(codegen: CodeGenerator) {
@@ -189,7 +203,7 @@
ObjCExportBlockCodeGenerator(codegen).generate()
}
- if (!config.isFinalBinary) return // TODO: emit RTTI to the same modules as classes belong to.
+ if (!config.isFinalBinary && !config.objcExportCacheEnabled) return // TODO: emit RTTI to the same modules as classes belong to.
val mapper = exportedInterface?.mapper ?: ObjCExportMapper(unitSuspendFunctionExport = config.unitSuspendFunctionObjCExport)
namer = exportedInterface?.namer ?: ObjCExportNamerImpl(
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 24b290b..1369980 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
@@ -19,6 +19,8 @@
import org.jetbrains.kotlin.ir.util.IdSignatureComposer
import org.jetbrains.kotlin.ir.util.SymbolTable
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
+import org.jetbrains.kotlin.resolve.descriptorUtil.module
+import org.jetbrains.kotlin.backend.konan.ir.konanLibrary
import java.io.PrintStream
@OptIn(ObsoleteDescriptorBasedAPI::class)
@@ -46,10 +48,13 @@
}
})
- val files = topLevel.map { [sourceFile, declarations] ->
+ val files = topLevel.entries.map { entry ->
+ val sourceFile = entry.key
+ val declarations = entry.value
val binaryName = namer.getFileClassName(sourceFile).binaryName
val methods = declarations.toObjCMethods()
- ObjCClassForKotlinFile(binaryName, sourceFile, methods)
+ val klib = declarations.firstOrNull()?.module?.konanLibrary
+ ObjCClassForKotlinFile(binaryName, sourceFile, methods, klib)
}
val classToType = mutableMapOf<ClassDescriptor, ObjCTypeForKotlinType>()
@@ -316,7 +321,8 @@
internal class ObjCClassForKotlinFile(
binaryName: String,
private val sourceFile: SourceFile,
- val methods: List<ObjCMethodForKotlinMethod>
+ val methods: List<ObjCMethodForKotlinMethod>,
+ val klib: org.jetbrains.kotlin.library.KotlinLibrary? = null
) : ObjCTypeSpec(binaryName) {
override fun toString(): String =
"ObjC spec of class `$binaryName` for `${sourceFile.name}`"
diff --git a/native/binary-options/src/main/kotlin/org/jetbrains/kotlin/config/nativeBinaryOptions/BinaryOptions.kt b/native/binary-options/src/main/kotlin/org/jetbrains/kotlin/config/nativeBinaryOptions/BinaryOptions.kt
index 7dc4a3a..96f41ae 100644
--- a/native/binary-options/src/main/kotlin/org/jetbrains/kotlin/config/nativeBinaryOptions/BinaryOptions.kt
+++ b/native/binary-options/src/main/kotlin/org/jetbrains/kotlin/config/nativeBinaryOptions/BinaryOptions.kt
@@ -123,6 +123,8 @@
*/
val macabi by booleanOption()
+ val objcExportCache by booleanOption()
+
val escapeAnalysisPropagateExiledToHeapObjects by booleanOption()
val perFileCacheForStdlib by booleanOption()
diff --git a/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.m b/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.m
new file mode 100644
index 0000000..343c722
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.m
@@ -0,0 +1,22 @@
+#import <Foundation/Foundation.h>
+#import <Kt/Kt.h>
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ KtUser *user = [[KtUser alloc] initWithName:@"Bob"];
+ NSString *greeting = [KtLibBKt sayHelloReceiver:user];
+ if (![greeting isEqualToString:@"Hello, Bob"]) {
+ NSLog(@"Expected 'Hello, Bob', got '%@'", greeting);
+ return 1;
+ }
+
+ NSString *categoryGreeting = [user sayHello];
+ if (![categoryGreeting isEqualToString:@"Hello, Bob"]) {
+ NSLog(@"Expected 'Hello, Bob', got '%@'", categoryGreeting);
+ return 1;
+ }
+
+ printf("OK\n");
+ }
+ return 0;
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.swift b/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.swift
new file mode 100644
index 0000000..77676b9
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/categoryExt/categoryExt.swift
@@ -0,0 +1,15 @@
+import Foundation
+import Kt
+
+let user = User(name: "Bob")
+let greeting = LibBKt.sayHello(user)
+if greeting != "Hello, Bob" {
+ fatalError("Expected 'Hello, Bob', got '\(greeting)'")
+}
+
+let categoryGreeting = user.sayHello()
+if categoryGreeting != "Hello, Bob" {
+ fatalError("Expected 'Hello, Bob', got '\(categoryGreeting)'")
+}
+
+print("OK")
diff --git a/native/native.tests/testData/framework/objcExportCache/categoryExt/libA/libA.kt b/native/native.tests/testData/framework/objcExportCache/categoryExt/libA/libA.kt
new file mode 100644
index 0000000..4244f99
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/categoryExt/libA/libA.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2010-2026 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 objcCache.categoryExt
+
+open class User(val name: String)
diff --git a/native/native.tests/testData/framework/objcExportCache/categoryExt/libB/libB.kt b/native/native.tests/testData/framework/objcExportCache/categoryExt/libB/libB.kt
new file mode 100644
index 0000000..b0420a8
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/categoryExt/libB/libB.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2010-2026 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 objcCache.categoryExt
+
+fun User.sayHello(): String = "Hello, $name"
diff --git a/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.m b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.m
new file mode 100644
index 0000000..b5536f9
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.m
@@ -0,0 +1,21 @@
+#import <Foundation/Foundation.h>
+#import <Kt/Kt.h>
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ NSString *a = [KtUtilsKt utilA];
+ if (![a isEqualToString:@"UtilA"]) {
+ NSLog(@"Expected 'UtilA', got '%@'", a);
+ return 1;
+ }
+
+ NSString *b = [KtUtilsKt utilB];
+ if (![b isEqualToString:@"UtilB"]) {
+ NSLog(@"Expected 'UtilB', got '%@'", b);
+ return 1;
+ }
+
+ printf("OK\n");
+ }
+ return 0;
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.swift b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.swift
new file mode 100644
index 0000000..a09b0b1
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/fileClassCollision.swift
@@ -0,0 +1,11 @@
+import Foundation
+import Kt
+
+if UtilsKt.utilA() != "UtilA" {
+ fatalError("Expected 'UtilA'")
+}
+if UtilsKt.utilB() != "UtilB" {
+ fatalError("Expected 'UtilB'")
+}
+
+print("OK")
diff --git a/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libA/Utils.kt b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libA/Utils.kt
new file mode 100644
index 0000000..7276401
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libA/Utils.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2010-2026 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 objcCache.collisionA
+
+fun utilA(): String = "UtilA"
diff --git a/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libB/Utils.kt b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libB/Utils.kt
new file mode 100644
index 0000000..50fd49d
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/fileClassCollision/libB/Utils.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2010-2026 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 objcCache.collisionB
+
+fun utilB(): String = "UtilB"
diff --git a/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.m b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.m
new file mode 100644
index 0000000..be420d5
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.m
@@ -0,0 +1,16 @@
+#import <Foundation/Foundation.h>
+#import <Kt/Kt.h>
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ KtSpecificItem *item = [[KtSpecificItem alloc] initWithId:42 tag:@"test"];
+ NSString *desc = [item describe];
+ if (![desc isEqualToString:@"Item #42"]) {
+ NSLog(@"Expected 'Item #42', got '%@'", desc);
+ return 1;
+ }
+
+ printf("OK\n");
+ }
+ return 0;
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.swift b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.swift
new file mode 100644
index 0000000..fe9fe3d
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/inheritedMethod.swift
@@ -0,0 +1,10 @@
+import Foundation
+import Kt
+
+let item = SpecificItem(id: 42, tag: "test")
+let desc = item.describe()
+if desc != "Item #42" {
+ fatalError("Expected 'Item #42', got '\(desc)'")
+}
+
+print("OK")
diff --git a/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libA/libA.kt b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libA/libA.kt
new file mode 100644
index 0000000..4c3f3e4
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libA/libA.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2010-2026 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 objcCache.inheritance
+
+open class BaseItem(val id: Int) {
+ fun describe(): String = "Item #$id"
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libB/libB.kt b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libB/libB.kt
new file mode 100644
index 0000000..ecf40a5
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/inheritedMethod/libB/libB.kt
@@ -0,0 +1,8 @@
+/*
+ * Copyright 2010-2026 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 objcCache.inheritance
+
+class SpecificItem(id: Int, val tag: String) : BaseItem(id)
diff --git a/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.m b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.m
new file mode 100644
index 0000000..6fc2730
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.m
@@ -0,0 +1,23 @@
+#import <Foundation/Foundation.h>
+#import <Kt/Kt.h>
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ id<KtGreeter> greeter = [KtLibBKt createGreeter];
+ NSString *message = [greeter greetName:@"World"];
+ if (![message isEqualToString:@"Hello, World!"]) {
+ NSLog(@"Expected 'Hello, World!', got '%@'", message);
+ return 1;
+ }
+
+ KtEnglishGreeter *englishGreeter = [[KtEnglishGreeter alloc] init];
+ NSString *message2 = [englishGreeter greetName:@"ObjC"];
+ if (![message2 isEqualToString:@"Hello, ObjC!"]) {
+ NSLog(@"Expected 'Hello, ObjC!', got '%@'", message2);
+ return 1;
+ }
+
+ printf("OK\n");
+ }
+ return 0;
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.swift b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.swift
new file mode 100644
index 0000000..96b44fb
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/interfaceImpl.swift
@@ -0,0 +1,16 @@
+import Foundation
+import Kt
+
+let greeter: Greeter = LibBKt.createGreeter()
+let message = greeter.greet(name: "World")
+if message != "Hello, World!" {
+ fatalError("Expected 'Hello, World!', got '\(message)'")
+}
+
+let englishGreeter = EnglishGreeter()
+let message2 = englishGreeter.greet(name: "Swift")
+if message2 != "Hello, Swift!" {
+ fatalError("Expected 'Hello, Swift!', got '\(message2)'")
+}
+
+print("OK")
diff --git a/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libA/libA.kt b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libA/libA.kt
new file mode 100644
index 0000000..044875b
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libA/libA.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2010-2026 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 objcCache.interfaceImpl
+
+interface Greeter {
+ fun greet(name: String): String
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libB/libB.kt b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libB/libB.kt
new file mode 100644
index 0000000..726a487
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/interfaceImpl/libB/libB.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2010-2026 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 objcCache.interfaceImpl
+
+class EnglishGreeter : Greeter {
+ override fun greet(name: String): String = "Hello, $name!"
+}
+
+fun createGreeter(): Greeter = EnglishGreeter()
diff --git a/native/native.tests/testData/framework/objcExportCache/multiLevel/libA/libA.kt b/native/native.tests/testData/framework/objcExportCache/multiLevel/libA/libA.kt
new file mode 100644
index 0000000..d118962
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/multiLevel/libA/libA.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2010-2026 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 objcCache.multiLevel
+
+open class Alpha {
+ fun alphaVal(): String = "alpha"
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/multiLevel/libB/libB.kt b/native/native.tests/testData/framework/objcExportCache/multiLevel/libB/libB.kt
new file mode 100644
index 0000000..60dced3
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/multiLevel/libB/libB.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2010-2026 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 objcCache.multiLevel
+
+open class Beta : Alpha() {
+ fun betaVal(): String = "beta"
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/multiLevel/libC/libC.kt b/native/native.tests/testData/framework/objcExportCache/multiLevel/libC/libC.kt
new file mode 100644
index 0000000..76d0d5a
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/multiLevel/libC/libC.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2010-2026 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 objcCache.multiLevel
+
+class Gamma : Beta() {
+ fun gammaVal(): String = "gamma"
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.m b/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.m
new file mode 100644
index 0000000..5dcff6b
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.m
@@ -0,0 +1,17 @@
+#import <Foundation/Foundation.h>
+#import <Kt/Kt.h>
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ KtGamma *g = [[KtGamma alloc] init];
+ if (![[g alphaVal] isEqualToString:@"alpha"] ||
+ ![[g betaVal] isEqualToString:@"beta"] ||
+ ![[g gammaVal] isEqualToString:@"gamma"]) {
+ NSLog(@"Failed multi-level cache test");
+ return 1;
+ }
+
+ printf("OK\n");
+ }
+ return 0;
+}
diff --git a/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.swift b/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.swift
new file mode 100644
index 0000000..a16b2b0
--- /dev/null
+++ b/native/native.tests/testData/framework/objcExportCache/multiLevel/multiLevel.swift
@@ -0,0 +1,9 @@
+import Foundation
+import Kt
+
+let g = Gamma()
+if g.alphaVal() != "alpha" || g.betaVal() != "beta" || g.gammaVal() != "gamma" {
+ fatalError("Failed multi-level cache test")
+}
+
+print("OK")
diff --git a/native/native.tests/testFixtures/org/jetbrains/kotlin/konan/test/blackbox/NativeSimpleTestUtils.kt b/native/native.tests/testFixtures/org/jetbrains/kotlin/konan/test/blackbox/NativeSimpleTestUtils.kt
index 349f6dd..3d98b65 100644
--- a/native/native.tests/testFixtures/org/jetbrains/kotlin/konan/test/blackbox/NativeSimpleTestUtils.kt
+++ b/native/native.tests/testFixtures/org/jetbrains/kotlin/konan/test/blackbox/NativeSimpleTestUtils.kt
@@ -193,11 +193,12 @@
internal fun AbstractNativeSimpleTest.compileToStaticCache(
klib: TestCompilationArtifact.KLIB,
cacheDir: File,
- vararg dependencies: TestCompilationArtifact.KLIBStaticCache
+ vararg dependencies: TestCompilationArtifact.KLIBStaticCache,
+ freeCompilerArgs: TestCompilerArgs = TestCompilerArgs.EMPTY,
): TestCompilationArtifact.KLIBStaticCache {
val compilation = StaticCacheCompilation(
settings = testRunSettings,
- freeCompilerArgs = TestCompilerArgs.EMPTY,
+ freeCompilerArgs = freeCompilerArgs,
StaticCacheCompilation.Options.Regular,
dependencies = buildList {
this += klib.asLibraryDependency()
diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCExportCacheTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCExportCacheTest.kt
new file mode 100644
index 0000000..4dcd602
--- /dev/null
+++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCExportCacheTest.kt
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2010-2026 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.konan.test.blackbox
+
+import com.intellij.testFramework.TestDataPath
+import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
+import org.jetbrains.kotlin.konan.test.blackbox.support.*
+import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.*
+import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess
+import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestExecutable
+import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunChecks
+import org.jetbrains.kotlin.konan.test.blackbox.support.settings.*
+import org.jetbrains.kotlin.konan.test.blackbox.support.util.*
+import org.jetbrains.kotlin.test.TestMetadata
+import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+import java.io.File
+
+@Tag("caches")
+@EnforcedHostTarget
+@TestMetadata(ObjCExportCacheTest.TEST_SUITE_PATH)
+@TestDataPath("\$PROJECT_ROOT")
+class ObjCExportCacheTest : AbstractNativeSimpleTest() {
+
+ private val testSuiteDir = ForTestCompileRuntime.transformTestDataPath(TEST_SUITE_PATH)
+ private val extras = TestCase.NoTestRunnerExtras("There's no entrypoint in Swift program")
+ private val testCompilationFactory = TestCompilationFactory()
+ private val objcCacheArgs = TestCompilerArgs(listOf("-Xbinary=objcExportCache=true"))
+
+ private fun getObjCCacheDir(cache: TestCompilationArtifact.KLIBStaticCache): File {
+ val base = cache.cacheDir
+ val objcDir = File(base.absolutePath + ".objc")
+ return if (objcDir.exists()) objcDir else base
+ }
+
+ private fun compileStdlibCache(): TestCompilationArtifact.KLIBStaticCache {
+ val stdlibPath = testRunSettings.get<KotlinNativeHome>().dir.resolve("klib/common/stdlib")
+ val stdlibKlib = TestCompilationArtifact.KLIB(stdlibPath)
+ val cacheDir = buildDir.resolve("cacheStdlib").apply { mkdirs() }
+ return compileToStaticCache(
+ stdlibKlib,
+ cacheDir,
+ freeCompilerArgs = objcCacheArgs
+ )
+ }
+
+ @Test
+ @TestMetadata("interfaceImpl")
+ fun testInterfaceImplementationAcrossCaches() {
+ val testName = "interfaceImpl"
+ val testDir = testSuiteDir.resolve(testName)
+
+ val cacheStdlib = compileStdlibCache()
+
+ val libA = compileToLibrary(
+ testDir.resolve("libA"),
+ buildDir.resolve("libA"),
+ TestCompilerArgs("-module-name", "libA"),
+ emptyList(),
+ )
+ val cacheA = compileToStaticCache(
+ libA,
+ buildDir.resolve("cacheA").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ val libB = compileToLibrary(
+ testDir.resolve("libB"),
+ buildDir.resolve("libB"),
+ TestCompilerArgs("-module-name", "libB"),
+ listOf(libA.asLibraryDependency()),
+ )
+ val cacheB = compileToStaticCache(
+ libB,
+ buildDir.resolve("cacheB").apply { mkdirs() },
+ cacheStdlib, cacheA,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheA).absolutePath}"
+ )
+ )
+ )
+
+ runObjCExportCacheFrameworkTest(testName, testDir, listOf(libA, libB), listOf(cacheStdlib, cacheA, cacheB))
+ }
+
+ @Test
+ @TestMetadata("categoryExt")
+ fun testCategoryExtensionFunctionOnCachedClass() {
+ val testName = "categoryExt"
+ val testDir = testSuiteDir.resolve(testName)
+
+ val cacheStdlib = compileStdlibCache()
+
+ val libA = compileToLibrary(
+ testDir.resolve("libA"),
+ buildDir.resolve("libA"),
+ TestCompilerArgs("-module-name", "libA"),
+ emptyList(),
+ )
+ val cacheA = compileToStaticCache(
+ libA,
+ buildDir.resolve("cacheA").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ val libB = compileToLibrary(
+ testDir.resolve("libB"),
+ buildDir.resolve("libB"),
+ TestCompilerArgs("-module-name", "libB"),
+ listOf(libA.asLibraryDependency()),
+ )
+ val cacheB = compileToStaticCache(
+ libB,
+ buildDir.resolve("cacheB").apply { mkdirs() },
+ cacheStdlib, cacheA,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheA).absolutePath}"
+ )
+ )
+ )
+
+ runObjCExportCacheFrameworkTest(testName, testDir, listOf(libA, libB), listOf(cacheStdlib, cacheA, cacheB))
+ }
+
+ @Test
+ @TestMetadata("fileClassCollision")
+ fun testFileClassSymbolCollisionAcrossCaches() {
+ val testName = "fileClassCollision"
+ val testDir = testSuiteDir.resolve(testName)
+
+ val cacheStdlib = compileStdlibCache()
+
+ val libA = compileToLibrary(
+ testDir.resolve("libA"),
+ buildDir.resolve("libA"),
+ TestCompilerArgs("-module-name", "libA"),
+ emptyList(),
+ )
+ val cacheA = compileToStaticCache(
+ libA,
+ buildDir.resolve("cacheA").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ val libB = compileToLibrary(
+ testDir.resolve("libB"),
+ buildDir.resolve("libB"),
+ TestCompilerArgs("-module-name", "libB"),
+ emptyList(),
+ )
+ val cacheB = compileToStaticCache(
+ libB,
+ buildDir.resolve("cacheB").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ runObjCExportCacheFrameworkTest(testName, testDir, listOf(libA, libB), listOf(cacheStdlib, cacheA, cacheB))
+ }
+
+ @Test
+ @TestMetadata("inheritedMethod")
+ fun testSubclassingAcrossCachesWithInheritedMethod() {
+ val testName = "inheritedMethod"
+ val testDir = testSuiteDir.resolve(testName)
+
+ val cacheStdlib = compileStdlibCache()
+
+ val libA = compileToLibrary(
+ testDir.resolve("libA"),
+ buildDir.resolve("libA"),
+ TestCompilerArgs("-module-name", "libA"),
+ emptyList(),
+ )
+ val cacheA = compileToStaticCache(
+ libA,
+ buildDir.resolve("cacheA").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ val libB = compileToLibrary(
+ testDir.resolve("libB"),
+ buildDir.resolve("libB"),
+ TestCompilerArgs("-module-name", "libB"),
+ listOf(libA.asLibraryDependency()),
+ )
+ val cacheB = compileToStaticCache(
+ libB,
+ buildDir.resolve("cacheB").apply { mkdirs() },
+ cacheStdlib, cacheA,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheA).absolutePath}"
+ )
+ )
+ )
+
+ runObjCExportCacheFrameworkTest(testName, testDir, listOf(libA, libB), listOf(cacheStdlib, cacheA, cacheB))
+ }
+
+ @Test
+ @TestMetadata("multiLevel")
+ fun testMultiLevelCacheChain() {
+ val testName = "multiLevel"
+ val testDir = testSuiteDir.resolve(testName)
+
+ val cacheStdlib = compileStdlibCache()
+
+ val libA = compileToLibrary(
+ testDir.resolve("libA"),
+ buildDir.resolve("libA"),
+ TestCompilerArgs("-module-name", "libA"),
+ emptyList(),
+ )
+ val cacheA = compileToStaticCache(
+ libA,
+ buildDir.resolve("cacheA").apply { mkdirs() },
+ cacheStdlib,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}"
+ )
+ )
+ )
+
+ val libB = compileToLibrary(
+ testDir.resolve("libB"),
+ buildDir.resolve("libB"),
+ TestCompilerArgs("-module-name", "libB"),
+ listOf(libA.asLibraryDependency()),
+ )
+ val cacheB = compileToStaticCache(
+ libB,
+ buildDir.resolve("cacheB").apply { mkdirs() },
+ cacheStdlib, cacheA,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheA).absolutePath}"
+ )
+ )
+ )
+
+ val libC = compileToLibrary(
+ testDir.resolve("libC"),
+ buildDir.resolve("build_libC"),
+ TestCompilerArgs("-module-name", "libC"),
+ listOf(libA.asLibraryDependency(), libB.asLibraryDependency()),
+ )
+ val cacheC = compileToStaticCache(
+ libC,
+ buildDir.resolve("cacheC").apply { mkdirs() },
+ cacheStdlib, cacheA, cacheB,
+ freeCompilerArgs = TestCompilerArgs(
+ listOf(
+ "-Xbinary=objcExportCache=true",
+ "-Xcache-directory=${getObjCCacheDir(cacheStdlib).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheA).absolutePath}",
+ "-Xcache-directory=${getObjCCacheDir(cacheB).absolutePath}"
+ )
+ )
+ )
+
+ runObjCExportCacheFrameworkTest(testName, testDir, listOf(libA, libB, libC), listOf(cacheStdlib, cacheA, cacheB, cacheC))
+ }
+
+ private fun runObjCExportCacheFrameworkTest(
+ testName: String,
+ testDir: File,
+ klibs: List<TestCompilationArtifact.KLIB>,
+ caches: List<TestCompilationArtifact.KLIBStaticCache>
+ ) {
+ Assumptions.assumeTrue(targets.testTarget.family.isAppleFamily)
+ Assumptions.assumeFalse(testRunSettings.get<CacheMode>() == CacheMode.WithoutCache)
+
+ val frameworkName = "Kt"
+ val frameworkOpts = listOf(
+ "-Xstatic-framework",
+ "-Xbinary=objcExportCache=true",
+ "-Xbinary=bundleId=$frameworkName",
+ "-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
+ "-module-name", frameworkName
+ ) + klibs.map { "-Xexport-library=${it.klibFile.absolutePath}" } + caches.flatMap {
+ val dir = getObjCCacheDir(it)
+ listOf("-Xcache-directory=${it.cacheDir.absolutePath}", "-Xcache-directory=${dir.absolutePath}")
+ }
+
+ val dummyFile = buildDir.resolve("dummy_${testName}.kt").also {
+ if (!it.exists()) it.writeText("package com.example\n\n// dummy file for framework compilation")
+ }
+ val testCase = generateObjCFrameworkTestCase(
+ TestKind.STANDALONE_NO_TR, extras, frameworkName,
+ listOf(dummyFile),
+ freeCompilerArgs = TestCompilerArgs(frameworkOpts),
+ givenDependencies = klibs.map { TestModule.Given(it.klibFile) }.toSet(),
+ checks = TestRunChecks.Default(testRunSettings.get<Timeouts>().executionTimeout * 2),
+ )
+
+ val success = testCompilationFactory.testCaseToObjCFrameworkCompilation(
+ testCase, testRunSettings, exportedLibraries = klibs
+ ).result.assertSuccess()
+ val frameworkArtifact = success.resultingArtifact
+
+ compileAndRunSwift(testName, testCase, testDir, frameworkArtifact)
+ compileAndRunObjC(testName, testCase, testDir, frameworkArtifact)
+ }
+
+ private fun compileAndRunSwift(
+ testName: String,
+ testCase: TestCase,
+ testDir: File,
+ frameworkArtifact: TestCompilationArtifact.ObjCFramework,
+ ) {
+ val swiftSources = testDir.listFiles { file: File -> file.name.endsWith(".swift") }?.toList().orEmpty()
+ if (swiftSources.isEmpty()) return
+
+ val swiftCompilation = SwiftCompilation(
+ testRunSettings,
+ swiftSources,
+ TestCompilationArtifact.Executable(buildDir.resolve("${testName}_swiftExecutable")),
+ listOf(
+ "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
+ "-Xlinker", "-rpath", "-Xlinker", buildDir.absolutePath,
+ "-F", buildDir.absolutePath
+ ),
+ outputFile = { executable -> executable.executableFile }
+ ).result.assertSuccess()
+
+ val testExecutable = TestExecutable(
+ swiftCompilation.resultingArtifact,
+ swiftCompilation.loggedData,
+ listOf(TestName(testName))
+ )
+ runExecutableAndVerify(testCase, testExecutable)
+ }
+
+ private fun compileAndRunObjC(
+ testName: String,
+ testCase: TestCase,
+ testDir: File,
+ frameworkArtifact: TestCompilationArtifact.ObjCFramework,
+ ) {
+ val objcSources = testDir.listFiles { file: File -> file.name.endsWith(".m") }?.toList().orEmpty()
+ if (objcSources.isEmpty()) return
+
+ val executableArtifact = TestCompilationArtifact.Executable(buildDir.resolve("${testName}_objcExecutable"))
+ val clangResult = compileWithClang(
+ clangMode = ClangMode.C,
+ sourceFiles = objcSources,
+ outputFile = executableArtifact.executableFile,
+ frameworkDirectories = listOf(buildDir),
+ additionalClangFlags = listOf(
+ "-fobjc-arc",
+ "-F", buildDir.absolutePath,
+ "-framework", frameworkArtifact.frameworkName,
+ "-Wl,-rpath,${buildDir.absolutePath}"
+ )
+ ).assertSuccess()
+
+ val testExecutable = TestExecutable(
+ clangResult.resultingArtifact,
+ clangResult.loggedData,
+ listOf(TestName(testName))
+ )
+ runExecutableAndVerify(testCase, testExecutable)
+ }
+
+ companion object {
+ const val TEST_SUITE_PATH = "native/native.tests/testData/framework/objcExportCache"
+ }
+}
diff --git a/native/utils/src/org/jetbrains/kotlin/konan/target/Linker.kt b/native/utils/src/org/jetbrains/kotlin/konan/target/Linker.kt
index 1dd48d8..ccf6eba5 100644
--- a/native/utils/src/org/jetbrains/kotlin/konan/target/Linker.kt
+++ b/native/utils/src/org/jetbrains/kotlin/konan/target/Linker.kt
@@ -290,10 +290,17 @@
}.toList()
override fun LinkerArguments.finalLinkCommands(): List<Command> {
- val staticLibrariesArgs = if (staticLibraries.isEmpty())
- staticLibraries
+ val (objcStaticLibs, regularStaticLibs) = if (kind == LinkerOutputKind.STATIC_LIBRARY) {
+ Pair(emptyList<String>(), staticLibraries)
+ } else {
+ staticLibraries.partition { it.endsWith(".objc.a") }
+ }
+ val objcLinkFlags = objcStaticLibs.flatMap { listOf("-force_load", it) }
+
+ val staticLibrariesArgs = if (regularStaticLibs.isEmpty())
+ emptyList()
else tempFiles.create("libraries").let { librariesListFile ->
- librariesListFile.writeLines(staticLibraries)
+ librariesListFile.writeLines(regularStaticLibs)
listOf("-filelist", librariesListFile.absolutePath)
}
@@ -339,6 +346,7 @@
+linkerKonanFlags
if (compilerRtLibrary != null) +compilerRtLibrary!!
+staticLibrariesArgs
+ +objcLinkFlags
+dynamicLibrariesArgs
+linkerArgs
+rpath(dynamic, sanitizer)