Skip to content

Commit c9972c7

Browse files
authored
Introduce Native IR transformations (#363)
Native IR transformations are now available: compiler plugin implements atomic operations via K/N stdlib atomic intrinsics. Atomic arrays, delegated properties and traces are supported by the compiler plugin as well. To enable Native IR transformations, set the flag `kotlinx.atomicfu.enableNativeIrTransformations=true` in the `gradle.properties` file.
1 parent 5fe6c0d commit c9972c7

File tree

5 files changed

+122
-22
lines changed

5 files changed

+122
-22
lines changed

README.md

+10-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
>We do provide a compatibility of atomicfu-transformed artifacts between releases, but we do not provide
1010
>strict compatibility guarantees on plugin API and its general stability between Kotlin versions.
1111
12-
**Atomicfu** is a multiplatform library that provides the idiomatic and effective way of using atomic operations in Kotlin.
12+
**Atomicfu** is a multiplatform library that provides the idiomatic and efficient way of using atomic operations in Kotlin.
1313

1414
## Table of contents
1515
- [Requirements](#requirements)
@@ -46,7 +46,8 @@ Starting from version `0.22.0` of the library your project is required to use:
4646
* Code it like a boxed value `atomic(0)`, but run it in production efficiently:
4747
* For **JVM**: an atomic value is represented as a plain value atomically updated with `java.util.concurrent.atomic.AtomicXxxFieldUpdater` from the Java standard library.
4848
* For **JS**: an atomic value is represented as a plain value.
49-
* For **Native** and **Wasm**: an atomic value is not transformed, it remains boxed, and `kotlinx-atomicfu` library is used as a runtime dependency.
49+
* For **Native**: atomic operations are delegated to Kotlin/Native atomic intrinsics.
50+
* For **Wasm**: an atomic value is not transformed, it remains boxed, and `kotlinx-atomicfu` library is used as a runtime dependency.
5051
* Use Kotlin-specific extensions (e.g. inline `loop`, `update`, `updateAndGet` functions).
5152
* Use atomic arrays, user-defined extensions on atomics and locks (see [more features](#more-features)).
5253
* [Tracing operations](#tracing-operations) for debugging.
@@ -247,17 +248,13 @@ public var foo: T by _foo // public delegated property (val/var)
247248
(more specifically, `complex_expression` should not have branches in its compiled representation).
248249
Extract `complex_expression` into a variable when needed.
249250

250-
## Transformation modes
251+
## Atomicfu compiler plugin
251252

252-
Basically, Atomicfu library provides an effective usage of atomic values by performing the transformations of the compiled code.
253-
For JVM and JS there 2 transformation modes available:
254-
* **Post-compilation transformation** that modifies the compiled bytecode or `*.js` files.
255-
* **IR transformation** that is performed by the atomicfu compiler plugin.
256-
257-
### Atomicfu compiler plugin
258-
259-
Compiler plugin transformation is less fragile than transformation of the compiled sources
260-
as it depends on the compiler IR tree.
253+
To provide a user-friendly atomic API on the frontend and efficient usage of atomic values on the backend kotlinx-atomicfu library uses the compiler plugin to transform
254+
IR for all the target backends:
255+
* **JVM**: atomics are replaced with `java.util.concurrent.atomic.AtomicXxxFieldUpdater`.
256+
* **Native**: atomics are implemented via atomic intrinsics on Kotlin/Native.
257+
* **JS**: atomics are unboxed and represented as plain values.
261258

262259
To turn on IR transformation set these properties in your `gradle.properties` file:
263260

@@ -266,6 +263,7 @@ To turn on IR transformation set these properties in your `gradle.properties` fi
266263

267264
```groovy
268265
kotlinx.atomicfu.enableJvmIrTransformation=true // for JVM IR transformation
266+
kotlinx.atomicfu.enableNativeIrTransformation=true // for Native IR transformation
269267
kotlinx.atomicfu.enableJsIrTransformation=true // for JS IR transformation
270268
```
271269

atomicfu-gradle-plugin/src/main/kotlin/kotlinx/atomicfu/plugin/gradle/AtomicFUGradlePlugin.kt

+24-9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ private const val TEST_IMPLEMENTATION_CONFIGURATION = "testImplementation"
3535
private const val ENABLE_JS_IR_TRANSFORMATION_LEGACY = "kotlinx.atomicfu.enableIrTransformation"
3636
private const val ENABLE_JS_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJsIrTransformation"
3737
private const val ENABLE_JVM_IR_TRANSFORMATION = "kotlinx.atomicfu.enableJvmIrTransformation"
38+
private const val ENABLE_NATIVE_IR_TRANSFORMATION = "kotlinx.atomicfu.enableNativeIrTransformation"
3839
private const val MIN_SUPPORTED_GRADLE_VERSION = "7.0"
3940
private const val MIN_SUPPORTED_KGP_VERSION = "1.7.0"
4041

@@ -78,6 +79,7 @@ private fun Project.applyAtomicfuCompilerPlugin() {
7879
extensions.getByType(AtomicfuKotlinGradleSubplugin.AtomicfuKotlinGradleExtension::class.java).apply {
7980
isJsIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JS_IR_TRANSFORMATION)
8081
isJvmIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)
82+
isNativeIrTransformationEnabled = rootProject.getBooleanProperty(ENABLE_NATIVE_IR_TRANSFORMATION)
8183
}
8284
} else {
8385
// for KGP >= 1.6.20 && KGP <= 1.7.20:
@@ -171,6 +173,11 @@ private fun Project.needsJvmIrTransformation(target: KotlinTarget): Boolean =
171173
rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION) &&
172174
(target.platformType == KotlinPlatformType.jvm || target.platformType == KotlinPlatformType.androidJvm)
173175

176+
private fun Project.needsNativeIrTransformation(target: KotlinTarget): Boolean =
177+
rootProject.getBooleanProperty(ENABLE_NATIVE_IR_TRANSFORMATION) &&
178+
(target.platformType == KotlinPlatformType.native)
179+
180+
174181
private fun KotlinTarget.isJsIrTarget() =
175182
(this is KotlinJsTarget && this.irTarget != null) ||
176183
(this is KotlinJsIrTarget && this.platformType != KotlinPlatformType.wasm)
@@ -179,7 +186,8 @@ private fun Project.isTransformationDisabled(target: KotlinTarget): Boolean {
179186
val platformType = target.platformType
180187
return !config.transformJvm && (platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm) ||
181188
!config.transformJs && platformType == KotlinPlatformType.js ||
182-
platformType == KotlinPlatformType.wasm
189+
platformType == KotlinPlatformType.wasm ||
190+
!needsNativeIrTransformation(target) && platformType == KotlinPlatformType.native
183191
}
184192

185193
// Adds kotlinx-atomicfu-runtime as an implementation dependency to the JS IR target:
@@ -280,20 +288,29 @@ private fun Project.configureTasks() {
280288

281289
private fun Project.configureJvmTransformation() {
282290
if (kotlinExtension is KotlinJvmProjectExtension || kotlinExtension is KotlinAndroidProjectExtension) {
283-
configureTransformationForTarget((kotlinExtension as KotlinSingleTargetExtension<*>).target)
291+
val target = (kotlinExtension as KotlinSingleTargetExtension<*>).target
292+
if (!needsJvmIrTransformation(target)) {
293+
configureTransformationForTarget(target)
294+
}
284295
}
285296
}
286297

287-
private fun Project.configureJsTransformation() =
288-
configureTransformationForTarget((kotlinExtension as KotlinJsProjectExtension).js())
298+
private fun Project.configureJsTransformation() {
299+
val target = (kotlinExtension as KotlinJsProjectExtension).js()
300+
if (!needsJsIrTransformation(target)) {
301+
configureTransformationForTarget(target)
302+
}
303+
}
289304

290305
private fun Project.configureMultiplatformTransformation() =
291306
withKotlinTargets { target ->
307+
// Skip transformation for common, native and wasm targets or in case IR transformation by the compiler plugin is enabled (for JVM or JS targets)
292308
if (target.platformType == KotlinPlatformType.common ||
293309
target.platformType == KotlinPlatformType.native ||
294-
target.platformType == KotlinPlatformType.wasm
310+
target.platformType == KotlinPlatformType.wasm ||
311+
needsJvmIrTransformation(target) || needsJsIrTransformation(target)
295312
) {
296-
return@withKotlinTargets // skip creation of transformation task for common, native and wasm targets
313+
return@withKotlinTargets
297314
}
298315
configureTransformationForTarget(target)
299316
}
@@ -302,8 +319,6 @@ private fun Project.configureTransformationForTarget(target: KotlinTarget) {
302319
val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>()
303320
val config = config
304321
target.compilations.all compilations@{ compilation ->
305-
// do not modify directories if compiler plugin is applied
306-
if (needsJvmIrTransformation(target) || needsJsIrTransformation(target)) return@compilations
307322
val compilationType = compilation.name.compilationNameToType()
308323
?: return@compilations // skip unknown compilations
309324
val classesDirs = compilation.output.classesDirs
@@ -329,7 +344,7 @@ private fun Project.configureTransformationForTarget(target: KotlinTarget) {
329344
val transformTask = when (target.platformType) {
330345
KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> {
331346
// create transformation task only if transformation is required and JVM IR compiler transformation is not enabled
332-
if (config.transformJvm && !rootProject.getBooleanProperty(ENABLE_JVM_IR_TRANSFORMATION)) {
347+
if (config.transformJvm) {
333348
project.registerJvmTransformTask(compilation)
334349
.configureJvmTask(
335350
compilation.compileDependencyFiles,

integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/cases/MppProjectTest.kt

+18
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,22 @@ class MppProjectTest {
4848
mppSample.checkMppWasmJsImplementationDependencies()
4949
mppSample.checkMppWasmWasiImplementationDependencies()
5050
}
51+
52+
@Test
53+
fun testMppNativeWithEnabledIrTransformation() {
54+
mppSample.enableNativeIrTransformation = true
55+
assertTrue(mppSample.cleanAndBuild().isSuccessful)
56+
mppSample.checkMppNativeCompileOnlyDependencies()
57+
// TODO: klib checks are skipped for now because of this problem KT-61143
58+
//mppSample.buildAndCheckNativeKlib()
59+
}
60+
61+
@Test
62+
fun testMppNativeWithDisabledIrTransformation() {
63+
mppSample.enableNativeIrTransformation = false
64+
assertTrue(mppSample.cleanAndBuild().isSuccessful)
65+
mppSample.checkMppNativeImplementationDependencies()
66+
// TODO: klib checks are skipped for now because of this problem KT-61143
67+
//mppSample.buildAndCheckNativeKlib()
68+
}
5169
}

integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/ArtifactChecker.kt

+57-1
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import kotlinx.atomicfu.gradle.plugin.test.framework.runner.GradleBuild
88
import kotlinx.atomicfu.gradle.plugin.test.framework.runner.cleanAndBuild
99
import org.objectweb.asm.*
1010
import java.io.File
11+
import java.net.URLClassLoader
1112
import kotlin.test.assertFalse
1213

1314
internal abstract class ArtifactChecker(private val targetDir: File) {
1415

1516
private val ATOMIC_FU_REF = "Lkotlinx/atomicfu/".toByteArray()
1617
protected val KOTLIN_METADATA_DESC = "Lkotlin/Metadata;"
1718

18-
private val projectName = targetDir.name.substringBeforeLast("-")
19+
protected val projectName = targetDir.name.substringBeforeLast("-")
1920

2021
val buildDir
2122
get() = targetDir.resolve("build").also {
@@ -60,8 +61,63 @@ private class BytecodeChecker(targetDir: File) : ArtifactChecker(targetDir) {
6061
}
6162
}
6263

64+
private class KlibChecker(targetDir: File) : ArtifactChecker(targetDir) {
65+
66+
val nativeJar = System.getProperty("kotlin.native.jar")
67+
68+
val classLoader: ClassLoader = URLClassLoader(arrayOf(File(nativeJar).toURI().toURL()), this.javaClass.classLoader)
69+
70+
private fun invokeKlibTool(
71+
kotlinNativeClassLoader: ClassLoader?,
72+
klibFile: File,
73+
functionName: String,
74+
hasOutput: Boolean,
75+
vararg args: Any
76+
): String {
77+
val libraryClass = Class.forName("org.jetbrains.kotlin.cli.klib.Library", true, kotlinNativeClassLoader)
78+
val entryPoint = libraryClass.declaredMethods.single { it.name == functionName }
79+
val lib = libraryClass.getDeclaredConstructor(String::class.java, String::class.java, String::class.java)
80+
.newInstance(klibFile.canonicalPath, null, "host")
81+
82+
val output = StringBuilder()
83+
84+
// This is a hack. It would be better to get entryPoint properly
85+
if (args.isNotEmpty()) {
86+
entryPoint.invoke(lib, output, *args)
87+
} else if (hasOutput) {
88+
entryPoint.invoke(lib, output)
89+
} else {
90+
entryPoint.invoke(lib)
91+
}
92+
return output.toString()
93+
}
94+
95+
override fun checkReferences() {
96+
val classesDir = buildDir.resolve("classes/kotlin/")
97+
if (classesDir.exists() && classesDir.isDirectory) {
98+
classesDir.walkBottomUp().singleOrNull { it.isFile && it.name == "$projectName.klib" }?.let { klib ->
99+
val klibIr = invokeKlibTool(
100+
kotlinNativeClassLoader = classLoader,
101+
klibFile = klib,
102+
functionName = "ir",
103+
hasOutput = true,
104+
false
105+
)
106+
assertFalse(klibIr.toByteArray().findAtomicfuRef(), "Found kotlinx/atomicfu in klib ${klib.path}:\n $klibIr")
107+
} ?: error(" Native klib $projectName.klib is not found in $classesDir")
108+
}
109+
}
110+
}
111+
63112
internal fun GradleBuild.buildAndCheckBytecode() {
64113
val buildResult = cleanAndBuild()
65114
require(buildResult.isSuccessful) { "Build of the project $projectName failed:\n ${buildResult.output}" }
66115
BytecodeChecker(this.targetDir).checkReferences()
67116
}
117+
118+
// TODO: klib checks are skipped for now because of this problem KT-61143
119+
internal fun GradleBuild.buildAndCheckNativeKlib() {
120+
val buildResult = cleanAndBuild()
121+
require(buildResult.isSuccessful) { "Build of the project $projectName failed:\n ${buildResult.output}" }
122+
KlibChecker(this.targetDir).checkReferences()
123+
}

integration-testing/src/functionalTest/kotlin/kotlinx.atomicfu.gradle.plugin.test/framework/checker/DependenciesChecker.kt

+13
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ internal fun GradleBuild.checkMppWasmWasiImplementationDependencies() {
5959
checkAtomicfuDependencyIsPresent(listOf("wasmWasiCompileClasspath", "wasmWasiRuntimeClasspath"), commonAtomicfuDependency)
6060
}
6161

62+
// Checks Native target of an MPP project
63+
internal fun GradleBuild.checkMppNativeCompileOnlyDependencies() {
64+
// Here the name of the native target is hardcoded because the tested mpp-sample project declares this target and
65+
// KGP generates the same set of dependencies for every declared native target ([mingwX64|linuxX64|macosX64...]CompileKlibraries)
66+
checkAtomicfuDependencyIsPresent(listOf("macosX64CompileKlibraries"), commonAtomicfuDependency)
67+
checkAtomicfuDependencyIsAbsent(listOf("macosX64MainImplementation"), commonAtomicfuDependency)
68+
}
69+
70+
// Checks Native target of an MPP project
71+
internal fun GradleBuild.checkMppNativeImplementationDependencies() {
72+
checkAtomicfuDependencyIsPresent(listOf("macosX64CompileKlibraries", "macosX64MainImplementation"), commonAtomicfuDependency)
73+
}
74+
6275
// Some dependencies may be not resolvable but consumable and will not be present in the output of :dependencies task,
6376
// in this case we should check .pom or .module file of the published project.
6477
// This method checks if the .module file in the sample project publication contains org.jetbrains.kotlinx:atomicfu dependency included.

0 commit comments

Comments
 (0)