Skip to content

Commit

Permalink
Support a flag to enable Native IR transformations
Browse files Browse the repository at this point in the history
  • Loading branch information
mvicsokolova committed Nov 13, 2023
1 parent 77121f8 commit d366187
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 24 deletions.
22 changes: 9 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand All @@ -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
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -277,27 +284,33 @@ 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)
}

private fun Project.configureTransformationForTarget(target: KotlinTarget) {
val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, 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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ 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) {

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 {
Expand Down Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit d366187

Please sign in to comment.