From d36618754becb1a1d7cad8487b03049247193ff8 Mon Sep 17 00:00:00 2001 From: mvicsokolova Date: Thu, 12 Oct 2023 16:25:10 +0200 Subject: [PATCH] Support a flag to enable Native IR transformations --- README.md | 22 +++----- .../plugin/gradle/AtomicFUGradlePlugin.kt | 33 +++++++---- .../cases/MppProjectTest.kt | 18 ++++++ .../framework/checker/ArtifactChecker.kt | 55 ++++++++++++++++++- .../framework/checker/DependenciesChecker.kt | 13 +++++ 5 files changed, 117 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 043eff90..0d484e1f 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,8 @@ Starting from version `0.22.0` of the library your project is required to use: * Code it like a boxed value `atomic(0)`, but run it in production efficiently: * as `java.util.concurrent.atomic.AtomicXxxFieldUpdater` on Kotlin/JVM * as a plain unboxed value on Kotlin/JS -* Multiplatform: write common Kotlin code with atomics that compiles for Kotlin JVM, JS, and Native backends: - * Compile-only dependency for JVM and JS (no runtime dependencies) - * Compile and runtime dependency for Kotlin/Native + * as Kotlin/Native atomic intrinsics on Kotlin/Native +* Multiplatform: write common Kotlin code with atomics that compiles for Kotlin JVM, JS, and Native backends. * Use Kotlin-specific extensions (e.g. inline `loop`, `update`, `updateAndGet` functions). * Use atomic arrays, user-defined extensions on atomics and locks (see [more features](#more-features)). * [Tracing operations](#tracing-operations) for debugging. @@ -248,17 +247,13 @@ public var foo: T by _foo // public delegated property (val/var) (more specifically, `complex_expression` should not have branches in its compiled representation). Extract `complex_expression` into a variable when needed. -## Transformation modes +## Atomicfu compiler plugin -Basically, Atomicfu library provides an effective usage of atomic values by performing the transformations of the compiled code. -For JVM and JS there 2 transformation modes available: -* **Post-compilation transformation** that modifies the compiled bytecode or `*.js` files. -* **IR transformation** that is performed by the atomicfu compiler plugin. - -### Atomicfu compiler plugin - -Compiler plugin transformation is less fragile than transformation of the compiled sources -as it depends on the compiler IR tree. +To provide a user-friendly atomic API on the frontend and effective usage of atomic values on the backend kotlinx-atomicfu library uses the compiler plugin to transform +IR for all the target backends: +* **JVM**: atomics are replaced with `java.util.concurrent.atomic.AtomicXxxFieldUpdater`. +* **Native**: atomics are implemented via atomic intrinsics on Kotlin/Native. +* **JS**: atomics are unboxed and represented as plain values. To turn on IR transformation set these properties in your `gradle.properties` file: @@ -267,6 +262,7 @@ To turn on IR transformation set these properties in your `gradle.properties` fi ```groovy kotlinx.atomicfu.enableJvmIrTransformation=true // for JVM IR transformation +kotlinx.atomicfu.enableNativeIrTransformation=true // for Native IR transformation kotlinx.atomicfu.enableJsIrTransformation=true // for JS IR transformation ``` diff --git a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt index a9c7c25e..fca9a504 100644 --- a/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt +++ b/atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt @@ -35,6 +35,7 @@ private const val TEST_IMPLEMENTATION_CONFIGURATION = "testImplementation" private const val ENABLE_JS_IR_TRANSFORMATION_LEGACY = "kotlinx.atomicfu.enableIrTransformation" private const val ENABLE_JS_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJsIrTransformation" private const val ENABLE_JVM_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJvmIrTransformation" +private const val ENABLE_NATIVE_IR_TRANSFORMATION = "kotlinx.atomicfu.enableNativeIrTransformation" private const val MIN_SUPPORTED_GRADLE_VERSION = "7.0" private const val MIN_SUPPORTED_KGP_VERSION = "1.7.0" @@ -78,6 +79,7 @@ private fun Project.applyAtomicfuCompilerPlugin() { extensions.getByType(AtomicfuKotlinGradleSubplugin.AtomicfuKotlinGradleExtension::class.java).apply { isJsIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION) isJvmIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION) + isNativeIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_NATIVE_IR_TRANSFORMATION) } } else { // for KGP >= 1.6.20 && KGP <= 1.7.20: @@ -171,12 +173,17 @@ private fun Project.needsJvmIrTransformation(target: KotlinTarget): Boolean = rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION) && (target.platformType == KotlinPlatformType.jvm || target.platformType == KotlinPlatformType.androidJvm) +private fun Project.needsNativeIrTransformation(target: KotlinTarget): Boolean = + rootProject.getBooleanProperty(ENABLE_NATIVE_IR_TRANSFORMATION) && + (target.platformType == KotlinPlatformType.native) + private fun KotlinTarget.isJsIrTarget() = (this is KotlinJsTarget && this.irTarget != null) || this is KotlinJsIrTarget private fun Project.isTransformationDisabled(target: KotlinTarget): Boolean { val platformType = target.platformType return !config.transformJvm && (platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm) || - !config.transformJs && platformType == KotlinPlatformType.js + !config.transformJs && platformType == KotlinPlatformType.js || + !needsNativeIrTransformation(target) && platformType == KotlinPlatformType.native } // Adds kotlinx-atomicfu-runtime as an implementation dependency to the JS IR target: @@ -277,18 +284,26 @@ private fun Project.configureTasks() { private fun Project.configureJvmTransformation() { if (kotlinExtension is KotlinJvmProjectExtension || kotlinExtension is KotlinAndroidProjectExtension) { - configureTransformationForTarget((kotlinExtension as KotlinSingleTargetExtension<*>).target) + val target = (kotlinExtension as KotlinSingleTargetExtension<*>).target + if (!needsJvmIrTransformation(target)) { + configureTransformationForTarget(target) + } } } -private fun Project.configureJsTransformation() = - configureTransformationForTarget((kotlinExtension as KotlinJsProjectExtension).js()) +private fun Project.configureJsTransformation() { + val target = (kotlinExtension as KotlinJsProjectExtension).js() + if (!needsJsIrTransformation(target)) { + configureTransformationForTarget(target) + } +} private fun Project.configureMultiplatformTransformation() = withKotlinTargets { target -> - if (target.platformType == KotlinPlatformType.common || target.platformType == KotlinPlatformType.native) { - return@withKotlinTargets // skip the common & native targets -- no transformation for them - } + // Skip transformation for common and native targets and in case IR transformation by the compiler plugin is enabled (for JVM or JS targets) + if (target.platformType == KotlinPlatformType.common || target.platformType == KotlinPlatformType.native || + needsJvmIrTransformation(target) || needsJsIrTransformation(target)) + return@withKotlinTargets configureTransformationForTarget(target) } @@ -296,8 +311,6 @@ private fun Project.configureTransformationForTarget(target: KotlinTarget) { val originalDirsByCompilation = hashMapOf, FileCollection>() val config = config target.compilations.all compilations@{ compilation -> - // do not modify directories if compiler plugin is applied - if (needsJvmIrTransformation(target) || needsJsIrTransformation(target)) return@compilations val compilationType = compilation.name.compilationNameToType() ?: return@compilations // skip unknown compilations val classesDirs = compilation.output.classesDirs @@ -323,7 +336,7 @@ private fun Project.configureTransformationForTarget(target: KotlinTarget) { val transformTask = when (target.platformType) { KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> { // create transformation task only if transformation is required and JVM IR compiler transformation is not enabled - if (config.transformJvm && !rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) { + if (config.transformJvm) { project.registerJvmTransformTask(compilation) .configureJvmTask( compilation.compileDependencyFiles, diff --git a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt index b99f6acc..c01d2c04 100644 --- a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt +++ b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt @@ -41,4 +41,22 @@ class MppProjectTest { assert(mppSample.cleanAndBuild().isSuccessful) mppSample.checkConsumableDependencies() } + + @Test + fun testMppNativeWithEnabledIrTransformation() { + mppSample.enableNativeIrTransformation = true + assert(mppSample.cleanAndBuild().isSuccessful) + mppSample.checkMppNativeCompileOnlyDependencies() + // TODO: klib checks are skipped for now because of this problem KT-61143 + //mppSample.buildAndCheckNativeKlib() + } + + @Test + fun testMppNativeWithDisabledIrTransformation() { + mppSample.enableNativeIrTransformation = false + assert(mppSample.cleanAndBuild().isSuccessful) + mppSample.checkMppNativeImplementationDependencies() + // TODO: klib checks are skipped for now because of this problem KT-61143 + //mppSample.buildAndCheckNativeKlib() + } } diff --git a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt index 7ef56dc7..31abca1f 100644 --- a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt +++ b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt @@ -8,6 +8,7 @@ import kotlinx.atomicfu.gradle.plugin.test.framework.runner.GradleBuild import kotlinx.atomicfu.gradle.plugin.test.framework.runner.cleanAndBuild import org.objectweb.asm.* import java.io.File +import java.net.URLClassLoader import kotlin.test.assertFalse internal abstract class ArtifactChecker(private val targetDir: File) { @@ -15,7 +16,7 @@ internal abstract class ArtifactChecker(private val targetDir: File) { private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray() protected val KOTLIN_METADATA_DESC = "Lkotlin/Metadata;" - private val projectName = targetDir.name.substringBeforeLast("-") + protected val projectName = targetDir.name.substringBeforeLast("-") val buildDir get() = targetDir.resolve("build").also { @@ -60,8 +61,60 @@ private class BytecodeChecker(targetDir: File) : ArtifactChecker(targetDir) { } } +private class KlibChecker(targetDir: File) : ArtifactChecker(targetDir) { + + val nativeJar = System.getProperty("kotlin.native.jar") + + val classLoader: ClassLoader = URLClassLoader(arrayOf(File(nativeJar).toURI().toURL()), this.javaClass.classLoader) + + private fun invokeKlibTool( + kotlinNativeClassLoader: ClassLoader?, + klibFile: File, + functionName: String, + hasOutput: Boolean, + vararg args: Any + ): String { + val libraryClass = Class.forName("org.jetbrains.kotlin.cli.klib.Library", true, kotlinNativeClassLoader) + val entryPoint = libraryClass.declaredMethods.single { it.name == functionName } + val lib = libraryClass.getDeclaredConstructor(String::class.java, String::class.java, String::class.java) + .newInstance(klibFile.canonicalPath, null, "host") + + val output = StringBuilder() + + // This is a hack. It would be better to get entryPoint properly + if (args.isNotEmpty()) { + entryPoint.invoke(lib, output, *args) + } else if (hasOutput) { + entryPoint.invoke(lib, output) + } else { + entryPoint.invoke(lib) + } + return output.toString() + } + + override fun checkReferences() { + val myKlib = buildDir.resolve("classes/kotlin/macosX64/main/klib/$projectName.klib") + require(myKlib.exists()) { "Native klib is not found: ${myKlib.path}" } + val klibIr = invokeKlibTool( + kotlinNativeClassLoader = classLoader, + klibFile = myKlib, + functionName = "ir", + hasOutput = true, + false + ) + assertFalse(klibIr.toByteArray().findAtomicfuRef(), "Found kotlinx/atomicfu in klib ${myKlib.path}:\n $klibIr") + } +} + internal fun GradleBuild.buildAndCheckBytecode() { val buildResult = cleanAndBuild() require(buildResult.isSuccessful) { "Build of the project $projectName failed:\n ${buildResult.output}" } BytecodeChecker(this.targetDir).checkReferences() } + +// TODO: klib checks are skipped for now because of this problem KT-61143 +internal fun GradleBuild.buildAndCheckNativeKlib() { + val buildResult = cleanAndBuild() + require(buildResult.isSuccessful) { "Build of the project $projectName failed:\n ${buildResult.output}" } + KlibChecker(this.targetDir).checkReferences() +} diff --git a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt index f7edac71..6f9d5cc7 100644 --- a/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt +++ b/integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt @@ -50,6 +50,19 @@ internal fun GradleBuild.checkMppJvmCompileOnlyDependencies() { checkAtomicfuDependencyIsAbsent(listOf("jvmRuntimeClasspath", "jvmApiElements", "jvmRuntimeElements"), commonAtomicfuDependency) } +// Checks Native target of an MPP project +internal fun GradleBuild.checkMppNativeCompileOnlyDependencies() { + // Here the name of the native target is hardcoded because the tested mpp-sample project declares this target and + // KGP generates the same set of depependencies for every declared native target ([mingwX64|linuxX64|macosX64...]CompileKlibraries) + checkAtomicfuDependencyIsPresent(listOf("macosX64CompileKlibraries"), commonAtomicfuDependency) + checkAtomicfuDependencyIsAbsent(listOf("macosX64MainImplementation"), commonAtomicfuDependency) +} + +// Checks Native target of an MPP project +internal fun GradleBuild.checkMppNativeImplementationDependencies() { + checkAtomicfuDependencyIsPresent(listOf("macosX64CompileKlibraries", "macosX64MainImplementation"), commonAtomicfuDependency) +} + // Some dependencies may be not resolvable but consumable and will not be present in the output of :dependencies task, // in this case we should check .pom or .module file of the published project. // This method checks if the .module file in the sample project publication contains org.jetbrains.kotlinx:atomicfu dependency included.