From 11372994973f47f06eb9e884b708f5fe986a884a Mon Sep 17 00:00:00 2001 From: Sergey Bogolepov Date: Fri, 9 Dec 2022 13:28:09 +0200 Subject: [PATCH] [K/N] Support ObjCExport in dynamic driver --- .../kotlin/backend/konan/CompilerOutput.kt | 2 + .../konan/driver/DynamicCompilerDriver.kt | 26 +++- .../backend/konan/driver/phases/ObjCExport.kt | 55 +++++++ .../backend/konan/objcexport/ObjCExport.kt | 134 ++++++++++-------- 4 files changed, 153 insertions(+), 64 deletions(-) create mode 100644 kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt 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 4d83f48ce6d..d8bf047c4c7 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 @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.backend.common.serialization.KlibIrVersion import org.jetbrains.kotlin.backend.konan.cexport.produceCAdapterBitcode import org.jetbrains.kotlin.backend.konan.llvm.* import org.jetbrains.kotlin.backend.konan.llvm.objc.patchObjCRuntimeModule +import org.jetbrains.kotlin.backend.konan.objcexport.createObjCFramework import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.konan.CURRENT import org.jetbrains.kotlin.konan.CompilerVersion @@ -203,6 +204,7 @@ internal fun linkBitcodeDependencies(generationState: NativeGenerationState) { } +// TODO: Remove this function after dynamic driver is complete. internal fun produceOutput(generationState: NativeGenerationState) { val context = generationState.context val config = context.config 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 40aaa41554a..f08cf130a00 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 @@ -25,6 +25,7 @@ internal class DynamicCompilerDriver : CompilerDriver() { CompilerOutputKind.LIBRARY, CompilerOutputKind.DYNAMIC_CACHE, CompilerOutputKind.STATIC_CACHE, + CompilerOutputKind.FRAMEWORK, ) } @@ -36,7 +37,7 @@ internal class DynamicCompilerDriver : CompilerDriver() { CompilerOutputKind.PROGRAM -> produceBinary(engine, config, environment) CompilerOutputKind.DYNAMIC -> error("Dynamic compiler driver does not support `dynamic` output yet.") CompilerOutputKind.STATIC -> error("Dynamic compiler driver does not support `static` output yet.") - CompilerOutputKind.FRAMEWORK -> error("Dynamic compiler driver does not support `framework` output yet.") + CompilerOutputKind.FRAMEWORK -> produceObjCFramework(engine, config, environment) CompilerOutputKind.LIBRARY -> produceKlib(engine, config, environment) CompilerOutputKind.BITCODE -> error("Dynamic compiler driver does not support `bitcode` output yet.") CompilerOutputKind.DYNAMIC_CACHE -> produceBinary(engine, config, environment) @@ -48,6 +49,29 @@ internal class DynamicCompilerDriver : CompilerDriver() { } } + /** + * Create an Objective-C framework which is a directory consisting of + * - Objective-C header + * - Info.plist + * - Binary (if -Xomit-framework-binary is not passed). + */ + private fun produceObjCFramework(engine: PhaseEngine, config: KonanConfig, environment: KotlinCoreEnvironment) { + val frontendOutput = engine.runFrontend(config, environment) ?: return + 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) + } + val backendContext = createBackendContext(config, frontendOutput, psiToIrOutput) { + it.objCExportedInterface = objCExportedInterface + it.objCExportCodeSpec = objCCodeSpec + } + engine.runBackend(backendContext) + } + private fun produceKlib(engine: PhaseEngine, config: KonanConfig, environment: KotlinCoreEnvironment) { val frontendOutput = engine.runFrontend(config, environment) ?: return val psiToIrOutput = if (config.metadataKlib) { 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 new file mode 100644 index 00000000000..c70243cea46 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/driver/phases/ObjCExport.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.konan.driver.phases + +import org.jetbrains.kotlin.backend.konan.OutputFiles +import org.jetbrains.kotlin.backend.konan.driver.PhaseContext +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportCodeSpec +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.produceObjCExportInterface +import org.jetbrains.kotlin.descriptors.ModuleDescriptor + +/** + * Create internal representation of Objective-C wrapper. + */ +internal val ProduceObjCExportInterfacePhase = createSimpleNamedCompilerPhase( + "ObjCExportInterface", + "Objective-C header generation", + outputIfNotEnabled = { _, _, _, _ -> error("Cannot disable `ObjCExportInterface` phase when producing ObjC framework") } +) { context, input -> + produceObjCExportInterface(context, input.moduleDescriptor, input.frontendServices) +} + +internal data class CreateObjCFrameworkInput( + val moduleDescriptor: ModuleDescriptor, + val exportedInterface: ObjCExportedInterface, +) + +/** + * Create Objective-C framework in the given directory without binary. + */ +internal val CreateObjCFrameworkPhase = createSimpleNamedCompilerPhase( + "CreateObjCFramework", + "Create Objective-C framework" +) { context, input -> + val config = context.config + // TODO: Share this instance between multiple contexts (including NativeGenerationState)? + val outputFiles = OutputFiles(config.outputPath, config.target, config.produce) + createObjCFramework(config, input.moduleDescriptor, input.exportedInterface, outputFiles.mainFile) +} + +/** + * Create specification for bridges between exported Objective-C interfaces and their Kotlin origins. + */ +internal val CreateObjCExportCodeSpecPhase = createSimpleNamedCompilerPhase( + "ObjCExportCodeCodeSpec", + "Objective-C IR symbols", + outputIfNotEnabled = { _, _, _, _, -> ObjCExportCodeSpec(emptyList(), emptyList()) } +) { context, input -> + input.createCodeSpec(context.symbolTable!!) +} \ 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 edf289e75d1..fe2190d5f53 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 @@ -61,6 +61,33 @@ internal fun produceObjCExportInterface( return headerGenerator.buildInterface() } +/** + * Populate framework directory with headers, module and info.plist. + */ +internal fun createObjCFramework( + config: KonanConfig, + moduleDescriptor: ModuleDescriptor, + exportedInterface: ObjCExportedInterface, + frameworkDirectory: File +) { + val frameworkName = frameworkDirectory.name.removeSuffix(".framework") + val frameworkBuilder = FrameworkBuilder( + config, + infoPListBuilder = InfoPListBuilder(config), + moduleMapBuilder = ModuleMapBuilder(), + objCHeaderWriter = ObjCHeaderWriter(), + mainPackageGuesser = MainPackageGuesser(), + ) + frameworkBuilder.build( + moduleDescriptor, + frameworkDirectory, + frameworkName, + exportedInterface.headerLines, + moduleDependencies = emptySet() + ) +} + +// TODO: No need for such class in dynamic driver. internal class ObjCExport( private val generationState: NativeGenerationState, private val moduleDescriptor: ModuleDescriptor, @@ -93,7 +120,7 @@ internal class ObjCExport( val objCCodeGenerator = ObjCExportCodeGenerator(codegen, namer, mapper) - exportedInterface?.generateWorkaroundForSwiftSR10177() + exportedInterface?.generateWorkaroundForSwiftSR10177(generationState) objCCodeGenerator.generate(codeSpec) objCCodeGenerator.dispose() @@ -104,68 +131,49 @@ internal class ObjCExport( */ fun produceFrameworkInterface() { if (exportedInterface != null) { - produceFrameworkSpecific(exportedInterface.headerLines) - } - } - - private fun produceFrameworkSpecific(headerLines: List) { - val frameworkDirectory = File(generationState.outputFile) - val frameworkName = frameworkDirectory.name.removeSuffix(".framework") - val frameworkBuilder = FrameworkBuilder( - config, - infoPListBuilder = InfoPListBuilder(config), - moduleMapBuilder = ModuleMapBuilder(), - objCHeaderWriter = ObjCHeaderWriter(), - mainPackageGuesser = MainPackageGuesser(), - ) - frameworkBuilder.build( - moduleDescriptor, - frameworkDirectory, - frameworkName, - headerLines, - moduleDependencies = emptySet() - ) - } - - // See https://bugs.swift.org/browse/SR-10177 - private fun ObjCExportedInterface.generateWorkaroundForSwiftSR10177() { - // Code for all protocols from the header should get into the binary. - // Objective-C protocols ABI is complicated (consider e.g. undocumented extended type encoding), - // so the easiest way to achieve this (quickly) is to compile a stub by clang. - - val protocolsStub = listOf( - "__attribute__((used)) static void __workaroundSwiftSR10177() {", - buildString { - append(" ") - generatedClasses.forEach { - if (it.isInterface) { - val protocolName = namer.getClassOrProtocolName(it).objCName - append("@protocol($protocolName); ") - } - } - }, - "}" - ) - - val source = createTempFile("protocols", ".m").deleteOnExit() - source.writeLines(headerLines + protocolsStub) - - val bitcode = createTempFile("protocols", ".bc").deleteOnExit() - - val clangCommand = config.clang.clangC( - source.absolutePath, - "-O2", - "-emit-llvm", - "-c", "-o", bitcode.absolutePath - ) - - val result = Command(clangCommand).getResult(withErrors = true) - - if (result.exitCode == 0) { - generationState.llvm.additionalProducedBitcodeFiles += bitcode.absolutePath - } else { - // Note: ignoring compile errors intentionally. - // In this case resulting framework will likely be unusable due to compile errors when importing it. + createObjCFramework(generationState.config, moduleDescriptor, exportedInterface, File(generationState.outputFile)) } } } + +// See https://bugs.swift.org/browse/SR-10177 +private fun ObjCExportedInterface.generateWorkaroundForSwiftSR10177(generationState: NativeGenerationState) { + // Code for all protocols from the header should get into the binary. + // Objective-C protocols ABI is complicated (consider e.g. undocumented extended type encoding), + // so the easiest way to achieve this (quickly) is to compile a stub by clang. + + val protocolsStub = listOf( + "__attribute__((used)) static void __workaroundSwiftSR10177() {", + buildString { + append(" ") + generatedClasses.forEach { + if (it.isInterface) { + val protocolName = namer.getClassOrProtocolName(it).objCName + append("@protocol($protocolName); ") + } + } + }, + "}" + ) + + val source = createTempFile("protocols", ".m").deleteOnExit() + source.writeLines(headerLines + protocolsStub) + + val bitcode = createTempFile("protocols", ".bc").deleteOnExit() + + val clangCommand = generationState.config.clang.clangC( + source.absolutePath, + "-O2", + "-emit-llvm", + "-c", "-o", bitcode.absolutePath + ) + + val result = Command(clangCommand).getResult(withErrors = true) + + if (result.exitCode == 0) { + generationState.llvm.additionalProducedBitcodeFiles += bitcode.absolutePath + } else { + // Note: ignoring compile errors intentionally. + // In this case resulting framework will likely be unusable due to compile errors when importing it. + } +}