Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flag for Native IR integration tests #363

Merged
merged 4 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
>We do provide a compatibility of atomicfu-transformed artifacts between releases, but we do not provide
>strict compatibility guarantees on plugin API and its general stability between Kotlin versions.

**Atomicfu** is a multiplatform library that provides the idiomatic and effective way of using atomic operations in Kotlin.
**Atomicfu** is a multiplatform library that provides the idiomatic and efficient way of using atomic operations in Kotlin.

## Table of contents
- [Requirements](#requirements)
Expand Down Expand Up @@ -46,7 +46,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:
* For **JVM**: an atomic value is represented as a plain value atomically updated with `java.util.concurrent.atomic.AtomicXxxFieldUpdater` from the Java standard library.
* For **JS**: an atomic value is represented as a plain value.
* For **Native** and **Wasm**: an atomic value is not transformed, it remains boxed, and `kotlinx-atomicfu` library is used as a runtime dependency.
* For **Native**: atomic operations are delegated to Kotlin/Native atomic intrinsics.
* For **Wasm**: an atomic value is not transformed, it remains boxed, and `kotlinx-atomicfu` library is used as a runtime dependency.
* 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 @@ -247,17 +248,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 efficient 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 @@ -266,6 +263,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,6 +173,11 @@ 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 && this.platformType != KotlinPlatformType.wasm)
Expand All @@ -179,7 +186,8 @@ 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 ||
platformType == KotlinPlatformType.wasm
platformType == KotlinPlatformType.wasm ||
!needsNativeIrTransformation(target) && platformType == KotlinPlatformType.native
}

// Adds kotlinx-atomicfu-runtime as an implementation dependency to the JS IR target:
Expand Down Expand Up @@ -280,20 +288,29 @@ 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 ->
// Skip transformation for common, native and wasm targets or in case IR transformation by the compiler plugin is enabled (for JVM or JS targets)
if (target.platformType == KotlinPlatformType.common ||
target.platformType == KotlinPlatformType.native ||
target.platformType == KotlinPlatformType.wasm
target.platformType == KotlinPlatformType.wasm ||
needsJvmIrTransformation(target) || needsJsIrTransformation(target)
) {
return@withKotlinTargets // skip creation of transformation task for common, native and wasm targets
return@withKotlinTargets
}
configureTransformationForTarget(target)
}
Expand All @@ -302,8 +319,6 @@ 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 @@ -329,7 +344,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 @@ -48,4 +48,22 @@ class MppProjectTest {
mppSample.checkMppWasmJsImplementationDependencies()
mppSample.checkMppWasmWasiImplementationDependencies()
}

@Test
fun testMppNativeWithEnabledIrTransformation() {
mppSample.enableNativeIrTransformation = true
assertTrue(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
assertTrue(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,63 @@ private class BytecodeChecker(targetDir: File) : ArtifactChecker(targetDir) {
}
}

private class KlibChecker(targetDir: File) : ArtifactChecker(targetDir) {

val nativeJar = System.getProperty("kotlin.native.jar")
fzhinkin marked this conversation as resolved.
Show resolved Hide resolved

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 classesDir = buildDir.resolve("classes/kotlin/")
if (classesDir.exists() && classesDir.isDirectory) {
classesDir.walkBottomUp().singleOrNull { it.isFile && it.name == "$projectName.klib" }?.let { klib ->
val klibIr = invokeKlibTool(
kotlinNativeClassLoader = classLoader,
klibFile = klib,
functionName = "ir",
hasOutput = true,
false
)
assertFalse(klibIr.toByteArray().findAtomicfuRef(), "Found kotlinx/atomicfu in klib ${klib.path}:\n $klibIr")
} ?: error(" Native klib $projectName.klib is not found in $classesDir")
}
}
}

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 @@ -59,6 +59,19 @@ internal fun GradleBuild.checkMppWasmWasiImplementationDependencies() {
checkAtomicfuDependencyIsPresent(listOf("wasmWasiCompileClasspath", "wasmWasiRuntimeClasspath"), 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 dependencies 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