
Load separately compiled runtime modules into running apps via a tiny common API, generating automatic bridges, supporting UI module embedding, integrity verification hooks, and build plugins.
Load separately compiled Kotlin code into a running Kotlin Multiplatform app.
Runtime Loader gives your app a small common API for attaching code at runtime. Your application still owns the extension model, permissions, metadata, update system, UI, and trust policy.
Typical uses include:
| Target | Status |
|---|---|
| JVM | Supported |
| Android | Supported |
| WasmJs | Supported |
| Linux x64 | Supported |
| Linux arm64 | Supported |
| Windows x64 | Supported |
| macOS x64 | Supported |
| macOS arm64 | Supported |
| iOS arm64 | Build and signing support |
| iOS Simulator arm64 | Supported for compilation |
Use the current version shown in the Maven Central badge above for the library and Runtime Loader Gradle plugins.
Make sure your build can use Maven Central and the Gradle Plugin Portal:
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}Add Runtime Loader to the host application:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.host") version "<version>"
}
kotlin {
// Add the targets your app uses.
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation("dev.brahmkshatriya.runtimeloader:runtime-loader:<version>")
}
}For a runtime-loadable module, apply the module plugin:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.module") version "<version>"
}A simple setup normally has three projects:
:extension-api shared interface between the host and modules
:app the host application
:my-extension separately compiled runtime-loadable code
The shared project contains the contract that both sides understand:
interface GreetingExtension {
fun greeting(): String
}The extension implements that contract:
class GreetingExtensionImpl : GreetingExtension {
override fun greeting(): String = "Hello from a loaded extension"
}Apply the host plugin and add the Runtime Loader library:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.host")
}
runtimeLoaderHost {
// Required for Kotlin/Native hosts.
entryPoint.set("com.example.app.main")
// Required when using the JVM host tasks.
jvmMainClass.set("com.example.app.MainKt")
}
kotlin {
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation(project(":extension-api"))
implementation("dev.brahmkshatriya.runtimeloader:runtime-loader:<version>")
}
}If you only use JVM, Android, or Wasm, configure only the settings needed by those targets.
The module plugin needs to know:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.module")
}
runtimeLoaderModule {
api.set(projects.extensionApi)
hosts.set(listOf(projects.app))
entry {
contract.set("com.example.extensions.GreetingExtension")
implementation.set("com.example.extensions.GreetingExtensionImpl")
}
}
kotlin {
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation(projects.extensionApi)
}
}The implementation class must have a no-argument constructor.
Runtime Loader generates the small bridge needed by each platform. Your app does not need to know platform-specific symbol or class names.
The common API is the same across supported platforms:
import dev.brahmkshatriya.runtimeloader.createEntry
import dev.brahmkshatriya.runtimeloader.loadCode
val code = loadCode(pathToModule)
val extension = code.createEntry<GreetingExtension>()
println(extension.greeting())
code.close()There is also a callback API:
RuntimeCodeLoader.load(
path = pathToModule,
onLoaded = { code ->
val extension = code.createEntry<GreetingExtension>()
println(extension.greeting())
},
onError = { error ->
println("Could not load extension: $error")
},
)If the module came from the network or an external store, authenticate it before loading it.
val code = loadVerifiedCode(
path = downloadedModule,
verifier = RuntimeCodeVerifier { path ->
verifyMySignatureOrDigest(path)
},
)
val extension = code.createEntry<GreetingExtension>()Runtime Loader checks whether Native modules are compatible with the host, but compatibility checks are not a security signature. Your application is responsible for deciding which downloaded code it trusts.
The shared contract can expose Compose functions just like any other Kotlin interface.
For example:
interface UiExtension {
@Composable
fun Content()
}The separately compiled module can implement that interface and render inside the host application's existing Compose UI.
The Runtime Loader host and module plugins automatically apply the Native dependency alignment they need. Runtime Loader does not require a separate Compose compatibility plugin.
JVM modules are loaded from JARs. Android modules are loaded from DEX JARs. Wasm modules are loaded dynamically by the generated JavaScript output.
Linux, Windows, and macOS use native shared libraries generated by the Runtime Loader Gradle plugin. The host app and its modules should be built with the same supported Kotlin/Native toolchain and compatible dependencies.
iOS uses signed frameworks. A downloaded iOS framework must be verified and signed with signing material appropriate for the host application before it can be loaded. Runtime Loader includes the signing integration used by the demo, but whether dynamically loaded code is permitted still depends on the target iOS environment and distribution method.
The Runtime Loader library supports Android API 23 and newer. Runtime-loadable Android modules default to min SDK 24; this can be changed in the module configuration if needed:
runtimeLoaderModule {
androidMinSdk.set(24)
}For Kotlin/Native hosts, the host plugin creates target-specific build tasks. Examples:
runtimeLoaderNativeBuildLinuxX64
runtimeLoaderNativeBuildLinuxArm64
runtimeLoaderNativeBuildMingwX64
runtimeLoaderNativeBuildMacosX64
runtimeLoaderNativeBuildMacosArm64
runtimeLoaderNativeBuildIosArm64
If the host project contains multiple supported Native targets, Runtime Loader can infer them automatically. You normally do not need to set runtimeLoaderHost.target yourself.
Runtime Loader intentionally does not define:
Those are application-level decisions. Runtime Loader only handles building, loading, compatibility checks, and creation of the shared entry object.
This repository includes working examples for JVM, Android, WasmJs, Linux, Windows, macOS, and iOS under demo/.
There is also a small consumer-style fixture under fixture/published-consumer showing the host/API/module structure without depending on the demo extension system.
Check the changelog for release-specific compatibility notes and migration information.
Apache License 2.0.
Load separately compiled Kotlin code into a running Kotlin Multiplatform app.
Runtime Loader gives your app a small common API for attaching code at runtime. Your application still owns the extension model, permissions, metadata, update system, UI, and trust policy.
Typical uses include:
| Target | Status |
|---|---|
| JVM | Supported |
| Android | Supported |
| WasmJs | Supported |
| Linux x64 | Supported |
| Linux arm64 | Supported |
| Windows x64 | Supported |
| macOS x64 | Supported |
| macOS arm64 | Supported |
| iOS arm64 | Build and signing support |
| iOS Simulator arm64 | Supported for compilation |
Use the current version shown in the Maven Central badge above for the library and Runtime Loader Gradle plugins.
Make sure your build can use Maven Central and the Gradle Plugin Portal:
// settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}Add Runtime Loader to the host application:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.host") version "<version>"
}
kotlin {
// Add the targets your app uses.
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation("dev.brahmkshatriya.runtimeloader:runtime-loader:<version>")
}
}For a runtime-loadable module, apply the module plugin:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.module") version "<version>"
}A simple setup normally has three projects:
:extension-api shared interface between the host and modules
:app the host application
:my-extension separately compiled runtime-loadable code
The shared project contains the contract that both sides understand:
interface GreetingExtension {
fun greeting(): String
}The extension implements that contract:
class GreetingExtensionImpl : GreetingExtension {
override fun greeting(): String = "Hello from a loaded extension"
}Apply the host plugin and add the Runtime Loader library:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.host")
}
runtimeLoaderHost {
// Required for Kotlin/Native hosts.
entryPoint.set("com.example.app.main")
// Required when using the JVM host tasks.
jvmMainClass.set("com.example.app.MainKt")
}
kotlin {
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation(project(":extension-api"))
implementation("dev.brahmkshatriya.runtimeloader:runtime-loader:<version>")
}
}If you only use JVM, Android, or Wasm, configure only the settings needed by those targets.
The module plugin needs to know:
plugins {
kotlin("multiplatform")
id("dev.brahmkshatriya.runtime-loader.module")
}
runtimeLoaderModule {
api.set(projects.extensionApi)
hosts.set(listOf(projects.app))
entry {
contract.set("com.example.extensions.GreetingExtension")
implementation.set("com.example.extensions.GreetingExtensionImpl")
}
}
kotlin {
jvm()
linuxX64()
sourceSets.commonMain.dependencies {
implementation(projects.extensionApi)
}
}The implementation class must have a no-argument constructor.
Runtime Loader generates the small bridge needed by each platform. Your app does not need to know platform-specific symbol or class names.
The common API is the same across supported platforms:
import dev.brahmkshatriya.runtimeloader.createEntry
import dev.brahmkshatriya.runtimeloader.loadCode
val code = loadCode(pathToModule)
val extension = code.createEntry<GreetingExtension>()
println(extension.greeting())
code.close()There is also a callback API:
RuntimeCodeLoader.load(
path = pathToModule,
onLoaded = { code ->
val extension = code.createEntry<GreetingExtension>()
println(extension.greeting())
},
onError = { error ->
println("Could not load extension: $error")
},
)If the module came from the network or an external store, authenticate it before loading it.
val code = loadVerifiedCode(
path = downloadedModule,
verifier = RuntimeCodeVerifier { path ->
verifyMySignatureOrDigest(path)
},
)
val extension = code.createEntry<GreetingExtension>()Runtime Loader checks whether Native modules are compatible with the host, but compatibility checks are not a security signature. Your application is responsible for deciding which downloaded code it trusts.
The shared contract can expose Compose functions just like any other Kotlin interface.
For example:
interface UiExtension {
@Composable
fun Content()
}The separately compiled module can implement that interface and render inside the host application's existing Compose UI.
The Runtime Loader host and module plugins automatically apply the Native dependency alignment they need. Runtime Loader does not require a separate Compose compatibility plugin.
JVM modules are loaded from JARs. Android modules are loaded from DEX JARs. Wasm modules are loaded dynamically by the generated JavaScript output.
Linux, Windows, and macOS use native shared libraries generated by the Runtime Loader Gradle plugin. The host app and its modules should be built with the same supported Kotlin/Native toolchain and compatible dependencies.
iOS uses signed frameworks. A downloaded iOS framework must be verified and signed with signing material appropriate for the host application before it can be loaded. Runtime Loader includes the signing integration used by the demo, but whether dynamically loaded code is permitted still depends on the target iOS environment and distribution method.
The Runtime Loader library supports Android API 23 and newer. Runtime-loadable Android modules default to min SDK 24; this can be changed in the module configuration if needed:
runtimeLoaderModule {
androidMinSdk.set(24)
}For Kotlin/Native hosts, the host plugin creates target-specific build tasks. Examples:
runtimeLoaderNativeBuildLinuxX64
runtimeLoaderNativeBuildLinuxArm64
runtimeLoaderNativeBuildMingwX64
runtimeLoaderNativeBuildMacosX64
runtimeLoaderNativeBuildMacosArm64
runtimeLoaderNativeBuildIosArm64
If the host project contains multiple supported Native targets, Runtime Loader can infer them automatically. You normally do not need to set runtimeLoaderHost.target yourself.
Runtime Loader intentionally does not define:
Those are application-level decisions. Runtime Loader only handles building, loading, compatibility checks, and creation of the shared entry object.
This repository includes working examples for JVM, Android, WasmJs, Linux, Windows, macOS, and iOS under demo/.
There is also a small consumer-style fixture under fixture/published-consumer showing the host/API/module structure without depending on the demo extension system.
Check the changelog for release-specific compatibility notes and migration information.
Apache License 2.0.