A Kotlin Multiplatform implementation of an embeddable Lua 5.5.1 runtime.
Rebranding in progress. BlueLuaK is becoming Basalt Luak (or simply Luak). The name is moving ahead of everything else: releases still publish to Blueva's Maven repository, the coordinates are still
net.blueva:blueluak-*, and the packages are stillnet.blueva.luak. The install instructions below are the ones that work today.
Basalt Luak (or simply Luak) is a Kotlin-first implementation of an embeddable Lua runtime, built as a Kotlin Multiplatform library. Its shared module currently targets:
- JVM 17+
- JavaScript IR, tested on Node.js
- WebAssembly, tested on Node.js
- Kotlin/Native for Linux x64, Linux ARM64, Windows x64, macOS x64, and macOS ARM64
The Lua runtime, value model, bytecode compiler, and standard libraries live in commonMain. JVM-specific integration is isolated from the shared runtime.
Luak currently implements Lua 5.5.1 and provides:
- An embeddable Lua VM written entirely in Kotlin.
- Lua bytecode compilation and execution across the configured KMP targets.
- Tables, metatables, functions, coroutines, and Lua 5.5.1 standard libraries.
LuaPlatform.standardGlobals(), one entry point that builds a fully loadedGlobalson every target.- A shared
iolibrary (io.open,io.lines,io.tmpfile, file handles,os.remove/rename/tmpname) on every target, not just the JVM. - Shared tests for the runtime, compiler, and libraries across KMP targets.
- JVM integrations for processes, Java reflection, script engines, and
luajava.
You can find more information on our website: luaklang.org
| Source set or module | Purpose |
|---|---|
luak-core/src/commonMain/kotlin/ |
Shared Lua runtime, compiler, and libraries |
luak-core/src/jvmMain/kotlin/ |
JVM implementations of platform abstractions |
luak-core/src/nonJvmMain/kotlin/ |
Portable implementations shared by JavaScript and Wasm |
luak-core/src/jsHostMain/kotlin/ |
JavaScript-host implementations (node:fs, process) for the JS and Wasm-JS targets |
luak-core/src/wasmWasiMain/kotlin/ |
WASI implementations over raw wasi_snapshot_preview1 syscalls |
luak-core/src/nativeMain/kotlin/ |
Kotlin/Native implementations of platform abstractions |
luak-core/src/nativePosixMain/kotlin/ |
64-bit file offsets for Linux and macOS |
luak-core/src/nativeWindowsMain/kotlin/ |
64-bit file offsets for Windows |
luak-core/src/commonTest/kotlin/ |
Tests shared by all core targets |
luak-jvm/src/main/kotlin/ |
JVM-only integrations and command-line tooling |
examples/ |
Kotlin and Lua usage examples |
Gradle modules:
| Module | Targets | Purpose |
|---|---|---|
luak-core |
JVM, JavaScript IR, Wasm, Kotlin/Native | Multiplatform Lua runtime, compiler, and libraries |
luak-jvm |
JVM | JVM platform adapters, luajava, scripting, CLI, and JIT support |
Platform-dependent functionality is exposed through expect/actual implementations. Code intended to run on every target belongs in commonMain; Java and JVM APIs remain confined to JVM source sets and luak-jvm. No type in the public commonMain API is platform-specific.
The host surface every shared library is built on is deliberately small: console streams, resource lookup, a random-access file handle, delete/rename/temp-name, environment variables, exit, GC, and weak references. Everything else (the value model, the compiler, and all nine standard libraries) is shared code.
Releases publish to repo.blueva.net, a public Maven repository, so no authentication is needed to depend on Luak.
Two artifacts are available. Pick one:
| Artifact | Contains | Use it when |
|---|---|---|
blueluak-jvm |
The multiplatform core (as a compile dependency) plus JvmPlatform.standardGlobals(), luajava, io.popen/os.execute, the luajc JIT compiler, CLI tooling, and javax.script integration |
You want a ready-to-use Lua runtime, the common case |
blueluak-core-jvm |
Just the shared runtime, compiler, and standard libraries on the JVM target, including LuaPlatform.standardGlobals(), but without luajava, io.popen, os.execute, or the JIT |
You don't need the JVM-only integrations, or want the smallest possible footprint |
blueluak-jvm pulls in blueluak-core-jvm transitively, so depending on it alone is enough for most projects.
Gradle (Kotlin DSL)
repositories {
maven("https://repo.blueva.net/releases")
}
dependencies {
implementation("net.blueva:blueluak-jvm:26.7")
}Maven
<repositories>
<repository>
<id>blueva</id>
<url>https://repo.blueva.net/releases</url>
</repository>
</repositories>
<dependency>
<groupId>net.blueva</groupId>
<artifactId>blueluak-jvm</artifactId>
<version>26.7</version>
</dependency>blueluak-core is only distributed as a Kotlin Multiplatform library: every non-JVM target is a Kotlin .klib, consumable from another Kotlin Multiplatform Gradle project. It is not a raw JS/npm package, and not a C-callable Native library.
LuaPlatform.standardGlobals() works on every target, so no target needs a hand-assembled Globals:
import net.blueva.luak.lib.LuaPlatform
val globals = LuaPlatform.standardGlobals()
globals.load("print('hello, world')")!!.call()LuaPlatform.debugGlobals() adds the debug library. Loading the individual classes in net.blueva.luak.lib (BaseLib, PackageLib, StringLib, TableLib, MathLib, CoroutineLib, OsLib, IoLib, Bit32Lib) by hand remains available when you want a smaller footprint.
Add the repo.blueva.net/releases repository shown above at the project level, then depend on the shared `net.blueva:blueluak-core:26.7
| Target | Gradle target function | Source set | Tested on |
|---|---|---|---|
| JavaScript IR | js { nodejs() } |
jsMain |
Node.js |
| WebAssembly | wasmJs { nodejs() } |
wasmJsMain |
Node.js |
| WebAssembly (WASI) | wasmWasi { nodejs() } |
wasmWasiMain |
Node.js's experimental node:wasi |
| Kotlin/Native | linuxX64(), linuxArm64(), mingwX64(), macosX64(), macosArm64() |
linuxX64Main, linuxArm64Main, mingwX64Main, macosX64Main, macosArm64Main |
Matching GitHub Actions runners in CI |
repositories {
maven("https://repo.blueva.net/releases")
}
kotlin {
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
linuxX64()
linuxArm64()
macosArm64()
sourceSets {
commonMain {
dependencies {
implementation("net.blueva:blueluak-core:26.7")
}
}
}
}A host that runs plugins it did not write needs bounds the plugin cannot lift. These live on Globals, are off by default, and cost next to nothing when unused.
val globals = LuaPlatform.standardGlobals()
globals.budget = Budget().apply { instructions = 10_000_000 } // per resumption
globals.memoryceiling = 64L * 1024 * 1024 // bytes
globals.textonly = true // no binary chunks
globals.seedrandom(laneId, runId) // reproducible draws
globals.bind(plugin).call()| Bound | What it does | What it costs when unused |
|---|---|---|
Globals.budget |
Stops a resumption that runs too long, or one a watchdog calls Budget.interrupt() on, with an ordinary Lua error. Needs no debug library, unlike debug.sethook. |
One null check per instruction |
Globals.memoryceiling |
Raises Lua's own not enough memory past a cap on what the state has been charged for. Each state counts its own objects, so one lane cannot spend another's. |
Nothing |
Globals.textonly |
Refuses binary chunks through every route into the loader — load, loadfile, dofile, require, and the host's own Globals.load. The undumper reads a format, not a language, and a malformed dump is not something checking makes safe. |
Nothing |
Globals.seedrandom(x, y) |
Fixes math.random's sequence from outside Lua. Unseeded states already differ from one another; this is for repeating a run. |
Nothing |
Both ceilings stay reached once they are: catching the error and carrying on meets it again, so only the host — through Budget.refill(), which the next resumption does anyway, or Globals.startmemorycount() — puts the state back to work.
Compiling the plugin is what a new lane costs, not building the standard libraries: for a 200-function chunk on the JVM that is 1094 µs against 61 µs. Compile it once and hand it to each lane, which a Prototype is safe for — it is finished code and constants, never written to again.
val plugin: Prototype = template.compile(source, "@plugin.lua")
for (lane in lanes) lane.bind(plugin).call()A whole lane costs 34 µs that way against 901 µs compiling per lane. Sharing the environment instead is not on offer, and deliberately: math.random carries a generator, io carries open files, and package carries what has been required, so a lane reaching into another's would be no sandbox at all.
Build every target and module from a clean checkout:
./gradlew clean buildBuild only the multiplatform core:
./gradlew :luak-core:buildCompile an individual target:
./gradlew :luak-core:compileKotlinJvm
./gradlew :luak-core:compileKotlinJs
./gradlew :luak-core:compileKotlinWasmJs
./gradlew :luak-core:compileKotlinMacosArm64Run every test suite available on the current host:
./gradlew :luak-core:allTestsRun an individual target suite:
./gradlew :luak-core:jvmTest
./gradlew :luak-core:jsNodeTest
./gradlew :luak-core:wasmJsNodeTest
./gradlew :luak-core:wasmWasiNodeTest
./gradlew :luak-core:macosArm64TestNative tests can only run on their matching host. Cross-platform Native compilation remains available from supported hosts.
| Component | Requirement |
|---|---|
| JDK | 17 or later, for Gradle and JVM targets |
| Kotlin | 2.4.10 |
| Gradle | 9.6.1 through the included wrapper |
| Node.js | Used for JavaScript and Wasm tests; managed by the Kotlin Gradle plugin |
| Native toolchain | Required only to link or run Kotlin/Native binaries on the host |
Use the included wrapper rather than a system Gradle installation.
The shared runtime, compiler, and standard libraries behave identically on every target. What differs is what the host can provide, and Luak reports those gaps the way Lua does, returning nil plus a message or raising an ordinary Lua error, rather than omitting functions:
| Capability | JVM | Kotlin/Native | JavaScript / Wasm-JS | Wasm-WASI |
|---|---|---|---|---|
Files (io.open, io.lines, os.remove, os.rename) |
Yes | Yes, POSIX stdio |
Yes under Node (node:fs); unavailable in a browser |
Yes, limited to the directories the host pre-opens |
Script lookup (require, dofile) |
Filesystem, then classpath | Filesystem | Filesystem under Node | Pre-opened directories |
os.getenv |
Environment, then system properties | getenv |
process.env under Node |
WASI environ_get |
io.popen, os.execute |
Yes | No (no portable process API) | No | No |
package.loadlib |
Yes | No | No | No |
Weak tables (__mode) |
Yes | Yes | No (no weak references in the host) | No |
os.date / os.time |
UTC | UTC | UTC | UTC |
Where a host grants no filesystem at all, io.open returns nil and a message and the rest of the library keeps working. io.popen behaves the same way outside luak-jvm.
Basalt Luak is distributed under the MIT License.
