RAkII (or Kotlin RAII) is a lightweight runtime library and compiler plugin which allows using structured RAII with support for error handling in Kotlin Multiplatform.
RAII is a concept commonly known from native languages such as C++ and Rust,
used for managing the lifetime of memory implicitly.
However, since Cleaners are not suitable for managing micro-allocations in
a granular manner in Kotlin, this library can be employed to reduce potential for
common errors, resource leaks and double-frees.
Simply add the required repositories to your build configuration, apply the Gradle plugin and add a dependency on the runtime:
In your settings.gradle.kts:
pluginManagement {
repositories {
maven("https://central.sonatype.com/repository/maven-snapshots")
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
maven("https://central.sonatype.com/repository/maven-snapshots")
mavenCentral()
}
}In your build.gradle.kts::
plugins {
id("dev.karmakrafts.rakii.rakii-gradle-plugin") version "<version>"
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("dev.karmakrafts.rakii:rakii-runtime:<version>")
}
}
}
}Note: It is recommended to use Gradle version catalogs which were omitted here for simplification.
import dev.karmakrafts.rakii.Drop
import dev.karmakrafts.rakii.deferring
class Foo : Drop {
fun doTheThing() {
println("I am doing the thing!")
}
}
class Bar : Drop {
val foo1 by dropping(::Foo)
// When the initialization of foo2 fails, foo1 will be dropped immediately
val foo2 by dropping(::Foo).dropOnAnyError(Bar::foo1)
fun doTheThings() {
foo1.doTheThing()
foo2.doTheThing()
}
fun helloWorld() = deferring {
// Tied to the surrounding deferring scope
val foo3 by dropping(::Foo)
foo3.doTheThing()
}
}
fun main() {
Bar().use {
// Normally use Bar and its contained Foo instance
// Once the use-scope ends, Foo is dropped and Bar is dropped afterward
it.doTheThings()
it.helloWorld()
}
}The RAkII compiler relies on the fact, that it can fall back to the runtime implementation
when no optimization is applicable for a certain case.
This means that certain usages of RAII constructs can employ a certain runtime overhead,
which may not always be desirable.
Take the following use cases of a deferring scope as an example:
import dev.karmakrafts.rakii.deferring
fun test(closure1: () -> Unit, closure2: () -> Unit): String = deferring {
// Can be reached with compiler optimization
defer { println("HELLO WORLD!") }
// Cannot be reached with compiler optimization
defer(closure1)
fun test2(): DropDelegate<String, DroppingScope.Owner> {
// Cannot be reached with compiler optimization
defer { println("HELLO WORLD!") }
closure2()
// Cannot be reached with compiler optimization
return dropping(::println) { "Testing" }
}
// Cannot be reached with compiler optimization
val value by test2()
// Can be reached with compiler optimization
val value2 by dropping(::println) { "!!!" }
value + value2
}As the above example illustrates as a fact:
If the DroppingScope instance is implicitly captured, compiler optimizations may not apply.
There is many more edge cases which are handled by the runtime due to complexity constraints in the compiler.