
Run JavaScript asynchronously with coroutine-style host bindings, ES module and bytecode compile/evaluate, configurable type converters for seamless object mapping, and concise DSL for bindings.
2024-02-14 to 2025-04-26 (latest version from quickjs-zh)-Wl,-z,max-page-size=65536
Starting from Android 15, some devices (especially those with new architectures) use 16KB page size instead of the traditional 4KB. To ensure forward compatibility, this library now builds with a maximum page size of 64KB.
Verify the build:
readelf -l libquickjs.so | grep LOAD
# Check that Align value is 0x10000 (64KB)This is a QuickJS binding for idiomatic Kotlin, inspired by Cash App's Zipline (previously Duktape Android) but with more flexibility.
There are a few QuickJS wrappers for Android already. Some written in Java are not Kotlin Multiplatform friendly, and some lack updates.
Zipline is great and KMP-friendly, but it focuses on running Kotlin/JS modules. Its API is limited to running arbitrary JavaScript code with platform bindings.
That's why I created this library, with some good features:
async and suspended. See #Async
Android, JVM and Kotlin/Native
In build.gradle.kts:
implementation("io.github.dokar3:quickjs-kt:<VERSION>")Or in libs.versions.toml:
quickjs-kt = { module = "io.github.dokar3:quickjs-kt", version = "<VERSION>" }with DSL (This is recommended if you don't need long-live instances):
coroutineScope.launch {
val result = quickJs {
evaluate<Int>("1 + 2")
}
}without DSL:
val quickJs = QuickJs.create(Dispatchers.Default)
coroutineScope.launch {
val result = quickJs.evaluate<Int>("1 + 2")
quickJs.close()
}Evaluate the compiled bytecode:
coroutineScope.launch {
quickJs {
val bytecode = compile("1 + 2")
val result = evaluate<Int>(bytecode)
}
}With DSL:
quickJs {
define("console") {
function("log") { args ->
println(args.joinToString(" "))
}
}
function("fetch") { args ->
someClient.request(args[0])
}
function<String, String>("greet") { "Hello, $it!" }
evaluate<Any?>(
"""
console.log("Hello from JavaScript!")
fetch("https://www.example.com")
greet("Jack")
""".trimIndent()
)
}With Reflection (JVM only):
class Console {
fun log(args: Array<Any?>) = TODO()
}
class Http {
fun fetch(url: String) = TODO()
}
quickJs {
define<Console>("console", Console())
define<Http>("http", Http())
evaluate<Any?>(
"""
console.log("Hello from JavaScript!")
http.fetch("https://www.example.com")
""".trimIndent()
)
}Binding classes need to be added to Android's ProGuard rules files.
-keep class com.example.Console { *; }
-keep class com.example.Http { *; }
This library gives you the ability to define async functions. Within the QuickJs instance, a coroutine scope is created to launch async jobs, a job Dispatcher can also be passed when creating the instance.
evaluate() and quickJs{} are suspend functions, which make your async jobs await in the caller scope. All pending jobs will be canceled when the caller scope is canceled or the instance is closed.
To define async functions, easily call asyncFunction():
quickJs {
define("http") {
asyncFunction("request") {
// Call suspend functions here
}
}
asyncFunction("fetch") {
// Call suspend functions here
}
}In JavaScript, you can use the top level await to easily get the result:
const resp = await http.request("https://www.example.com");
const next = await fetch("https://www.example.com");Or use Promise.all() to run your request concurrently!
const responses = await Promise.all([
fetch("https://www.example.com/0"),
fetch("https://www.example.com/1"),
fetch("https://www.example.com/2"),
])ES Modules are supported when evaluate() or compile() has the parameter asModule = true.
quickJs {
// ...
evaluate<String>(
"""
import * as hello from "hello";
// Use hello
""".trimIndent(),
asModule = true,
)
}Modules can be added using addModule() functions, both code and QuickJS bytecode are supported.
quickJs {
val helloModuleCode = """
export function greeting() {
return "Hi from the hello module!";
}
""".trimIndent()
addModule(name = "hello", code = helloModuleCode)
// OR
val bytecode = compile(
code = helloModuleCode,
filename = "hello",
asModule = true,
)
addModule(bytecode)
// ...
}When evaluating ES module code, no return values will be captured, you may need a function binding to receive the result.
quickJs {
// ...
var result: Any? = null
function("returns") { result = it.first() }
evaluate<Any?>(
"""
import * as hello from "hello";
// Pass the script result here
returns(hello.greeting());
""".trimIndent(),
asModule = true,
)
assertEquals("Hi from the hello module!", result)
}Want shorter DSL names?
quickJs {
def("console") {
prop("level") {
getter { "DEBUG" }
}
func("log") { }
}
func("fetch") { "Hello" }
asyncFunc("delay") { delay(1000) }
eval<Any?>("fetch()")
eval<Any?>(compile(code = "fetch()"))
}Use the DSL aliases then!
-import com.dokar.quickjs.binding.define
-import com.dokar.quickjs.binding.function
-import com.dokar.quickjs.binding.asyncFunction
-import com.dokar.quickjs.evaluate
+import com.dokar.quickjs.alias.def
+import com.dokar.quickjs.alias.func
+import com.dokar.quickjs.alias.asyncFunc
+import com.dokar.quickjs.alias.eval
+import com.dokar.quickjs.alias.propSome built-in types are mapped automatically between C and Kotlin, this table shows how they are mapped.
| JavaScript type | Kotlin type |
|---|---|
| null | null |
| undefined | null (1) |
| boolean | Boolean |
| Number | Long/Int/Short/Byte, Double/Float (2) |
| string | String |
| Array | List<Any?> |
| Set | Set<Any?> |
| Map | Map<Any?, Any?> |
| Error | Error |
| object | JsObject |
| Int8Array | ByteArray |
| UInt8Array | UByteArray |
(1) A Kotlin Unit will be mapped to a JavaScript undefined, conversely, JavaScript undefined won't be mapped to Kotlin Unit.
(2) When converting a JavaScript Number to Kotlin Int, Short, Byte or Float and the value is out of range, it will throw
TypeConverters are used to support mapping non-built-in types. You can implement your own type
converters:
data class FetchParams(val url: String, val method: String)
// interface JsObjectConverter<T : Any?> : TypeConverter<JsObject, T>
object FetchParamsConverter : JsObjectConverter<FetchParams> {
override val targetType: KType = typeOf<FetchParams>()
override fun convertToTarget(value: JsObject): FetchParams = FetchParams(
url = value["url"] as String,
method = value["method"] as String,
)
override fun convertToSource(value: FetchParams): JsObject =
mapOf("url" to value.url, "method" to value.method).toJsObject()
}
quickJs {
addTypeConverters(FetchParamsConverter)
asyncFunction<FetchParams, String>("fetch") {
// Use the typed fetch params
val (url, method) = it
TODO()
}
val result = evaluate<String>(
"""await fetch({ url: "https://example.com", method: "GET" })"""
)
}You can also use the converter from quickjs-kt-converter-ktxserialization
and quickjs-kt-convereter-moshi (JVM only).
Add the dependency
implementation("io.github.dokar3:quickjs-kt-converter-ktxserialization:<VERSION>")
// Or use the moshi converter
implementation("io.github.dokar3:quickjs-kt-converter-moshi:<VERSION>")Add the type converters of your classes
import com.dokar.quickjs.conveter.SerializableConverter
// For moshi
import com.dokar.quickjs.conveter.JsonClassConverter
@kotlinx.serialization.Serializable
// For moshi
@com.squareup.moshi.JsonClass(generateAdapter = true)
data class FetchParams(val url: String, val method: String)
quickJs {
addTypeConverters(SerializableConverter<FetchParams>())
// For moshi
addTypeConverters(JsonClassConverter<FetchParams>())
asyncFunction<FetchParams, String>("fetch") {
// Use the typed fetch params
val (url, method) = it
TODO()
}
val result = evaluate<String>(
"""await fetch({ url: "https://example.com", method: "GET" })"""
)
}[!NOTE] Functions with generic <T, R> require exactly 1 parameter on the JS side, it will throw if no parameter is passed or multiple parameters are passed.
Most of functions may throw:
IllegalStateException, if some function was called after calling close
evaluate() and compile() may throw:
QuickJsException, if a JavaScript error occurred or failed to map a type between JavaScript and KotlinIf you find other suspicious errors, please feel free to open an issue to report
js-eval but it has some Web API polyfills to run the bundled openai-node
# Install Zig compiler (for cross-compiling)
brew install zig
# Install CMake and Ninja (if not already installed)
brew install cmake ninja# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y cmake ninja-build
# Install Zig
# Download from https://ziglang.org/download/If you need to build for Android, install Android SDK and NDK.
The script supports different modes for local development and CI/CD environments:
# Local development (detailed output)
./publish-local.sh
# CI mode (minimal output)
./publish-local.sh --quiet
# or
./publish-local.sh --ci
# or
./publish-local.sh -q
# Skip clean step (faster rebuild)
./publish-local.sh --skip-clean
# Show help
./publish-local.sh --helpScript features:
# 1. Set execution permissions
chmod +x gradlew
chmod +x quickjs/native/cmake/*.sh
# 2. Configure local.properties (based on your platform)
# macOS ARM64
echo "JAVA_HOME_MACOS_AARCH64=/path/to/your/java/home" >> local.properties
# macOS x64
echo "JAVA_HOME_MACOS_X64=/path/to/your/java/home" >> local.properties
# Linux x64
echo "JAVA_HOME_LINUX_X64=/path/to/your/java/home" >> local.properties
# Linux ARM64
echo "JAVA_HOME_LINUX_AARCH64=/path/to/your/java/home" >> local.properties
# 3. Build and publish
./gradlew clean build publishToMavenLocalAfter successful publishing to ~/.m2/repository:
Add to your build.gradle.kts:
repositories {
mavenLocal() // Add local Maven repository
mavenCentral()
// ... other repositories
}
dependencies {
implementation("io.github.qdsfdhvh:quickjs-kt:<version>")
// Optional: Add converters
implementation("io.github.qdsfdhvh:quickjs-converter-ktxserialization:<version>")
// or
implementation("io.github.qdsfdhvh:quickjs-converter-moshi:<version>")
}# macOS
brew install zig
# Linux
# Download from https://ziglang.org/download/chmod +x gradlew
chmod +x quickjs/native/cmake/*.sh# macOS
export JAVA_HOME=$(/usr/libexec/java_home)
# Linux
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# Or find your Java installation pathEnsure CMake 3.10+ and Ninja are installed:
cmake --version
ninja --versionname: Build and Publish
on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Install Zig (macOS)
if: runner.os == 'macOS'
run: brew install zig
- name: Install Zig (Linux)
if: runner.os == 'Linux'
run: |
wget https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
tar -xf zig-linux-x86_64-0.11.0.tar.xz
echo "$PWD/zig-linux-x86_64-0.11.0" >> $GITHUB_PATH
- name: Publish to Maven Local
run: ./publish-local.sh --ci| Version | QuickJS Version | Date | Changes |
|---|---|---|---|
| 1.0.0-alpha14 | 2025-04-26 | 2025-11-26 | Update QuickJS to latest, add Android 64KB page support |
| 1.0.0-alpha13 | 2024-02-14 | 2024-xx-xx | Previous version |
Copyright 2024 dokar3
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2024-02-14 to 2025-04-26 (latest version from quickjs-zh)-Wl,-z,max-page-size=65536
Starting from Android 15, some devices (especially those with new architectures) use 16KB page size instead of the traditional 4KB. To ensure forward compatibility, this library now builds with a maximum page size of 64KB.
Verify the build:
readelf -l libquickjs.so | grep LOAD
# Check that Align value is 0x10000 (64KB)This is a QuickJS binding for idiomatic Kotlin, inspired by Cash App's Zipline (previously Duktape Android) but with more flexibility.
There are a few QuickJS wrappers for Android already. Some written in Java are not Kotlin Multiplatform friendly, and some lack updates.
Zipline is great and KMP-friendly, but it focuses on running Kotlin/JS modules. Its API is limited to running arbitrary JavaScript code with platform bindings.
That's why I created this library, with some good features:
async and suspended. See #Async
Android, JVM and Kotlin/Native
In build.gradle.kts:
implementation("io.github.dokar3:quickjs-kt:<VERSION>")Or in libs.versions.toml:
quickjs-kt = { module = "io.github.dokar3:quickjs-kt", version = "<VERSION>" }with DSL (This is recommended if you don't need long-live instances):
coroutineScope.launch {
val result = quickJs {
evaluate<Int>("1 + 2")
}
}without DSL:
val quickJs = QuickJs.create(Dispatchers.Default)
coroutineScope.launch {
val result = quickJs.evaluate<Int>("1 + 2")
quickJs.close()
}Evaluate the compiled bytecode:
coroutineScope.launch {
quickJs {
val bytecode = compile("1 + 2")
val result = evaluate<Int>(bytecode)
}
}With DSL:
quickJs {
define("console") {
function("log") { args ->
println(args.joinToString(" "))
}
}
function("fetch") { args ->
someClient.request(args[0])
}
function<String, String>("greet") { "Hello, $it!" }
evaluate<Any?>(
"""
console.log("Hello from JavaScript!")
fetch("https://www.example.com")
greet("Jack")
""".trimIndent()
)
}With Reflection (JVM only):
class Console {
fun log(args: Array<Any?>) = TODO()
}
class Http {
fun fetch(url: String) = TODO()
}
quickJs {
define<Console>("console", Console())
define<Http>("http", Http())
evaluate<Any?>(
"""
console.log("Hello from JavaScript!")
http.fetch("https://www.example.com")
""".trimIndent()
)
}Binding classes need to be added to Android's ProGuard rules files.
-keep class com.example.Console { *; }
-keep class com.example.Http { *; }
This library gives you the ability to define async functions. Within the QuickJs instance, a coroutine scope is created to launch async jobs, a job Dispatcher can also be passed when creating the instance.
evaluate() and quickJs{} are suspend functions, which make your async jobs await in the caller scope. All pending jobs will be canceled when the caller scope is canceled or the instance is closed.
To define async functions, easily call asyncFunction():
quickJs {
define("http") {
asyncFunction("request") {
// Call suspend functions here
}
}
asyncFunction("fetch") {
// Call suspend functions here
}
}In JavaScript, you can use the top level await to easily get the result:
const resp = await http.request("https://www.example.com");
const next = await fetch("https://www.example.com");Or use Promise.all() to run your request concurrently!
const responses = await Promise.all([
fetch("https://www.example.com/0"),
fetch("https://www.example.com/1"),
fetch("https://www.example.com/2"),
])ES Modules are supported when evaluate() or compile() has the parameter asModule = true.
quickJs {
// ...
evaluate<String>(
"""
import * as hello from "hello";
// Use hello
""".trimIndent(),
asModule = true,
)
}Modules can be added using addModule() functions, both code and QuickJS bytecode are supported.
quickJs {
val helloModuleCode = """
export function greeting() {
return "Hi from the hello module!";
}
""".trimIndent()
addModule(name = "hello", code = helloModuleCode)
// OR
val bytecode = compile(
code = helloModuleCode,
filename = "hello",
asModule = true,
)
addModule(bytecode)
// ...
}When evaluating ES module code, no return values will be captured, you may need a function binding to receive the result.
quickJs {
// ...
var result: Any? = null
function("returns") { result = it.first() }
evaluate<Any?>(
"""
import * as hello from "hello";
// Pass the script result here
returns(hello.greeting());
""".trimIndent(),
asModule = true,
)
assertEquals("Hi from the hello module!", result)
}Want shorter DSL names?
quickJs {
def("console") {
prop("level") {
getter { "DEBUG" }
}
func("log") { }
}
func("fetch") { "Hello" }
asyncFunc("delay") { delay(1000) }
eval<Any?>("fetch()")
eval<Any?>(compile(code = "fetch()"))
}Use the DSL aliases then!
-import com.dokar.quickjs.binding.define
-import com.dokar.quickjs.binding.function
-import com.dokar.quickjs.binding.asyncFunction
-import com.dokar.quickjs.evaluate
+import com.dokar.quickjs.alias.def
+import com.dokar.quickjs.alias.func
+import com.dokar.quickjs.alias.asyncFunc
+import com.dokar.quickjs.alias.eval
+import com.dokar.quickjs.alias.propSome built-in types are mapped automatically between C and Kotlin, this table shows how they are mapped.
| JavaScript type | Kotlin type |
|---|---|
| null | null |
| undefined | null (1) |
| boolean | Boolean |
| Number | Long/Int/Short/Byte, Double/Float (2) |
| string | String |
| Array | List<Any?> |
| Set | Set<Any?> |
| Map | Map<Any?, Any?> |
| Error | Error |
| object | JsObject |
| Int8Array | ByteArray |
| UInt8Array | UByteArray |
(1) A Kotlin Unit will be mapped to a JavaScript undefined, conversely, JavaScript undefined won't be mapped to Kotlin Unit.
(2) When converting a JavaScript Number to Kotlin Int, Short, Byte or Float and the value is out of range, it will throw
TypeConverters are used to support mapping non-built-in types. You can implement your own type
converters:
data class FetchParams(val url: String, val method: String)
// interface JsObjectConverter<T : Any?> : TypeConverter<JsObject, T>
object FetchParamsConverter : JsObjectConverter<FetchParams> {
override val targetType: KType = typeOf<FetchParams>()
override fun convertToTarget(value: JsObject): FetchParams = FetchParams(
url = value["url"] as String,
method = value["method"] as String,
)
override fun convertToSource(value: FetchParams): JsObject =
mapOf("url" to value.url, "method" to value.method).toJsObject()
}
quickJs {
addTypeConverters(FetchParamsConverter)
asyncFunction<FetchParams, String>("fetch") {
// Use the typed fetch params
val (url, method) = it
TODO()
}
val result = evaluate<String>(
"""await fetch({ url: "https://example.com", method: "GET" })"""
)
}You can also use the converter from quickjs-kt-converter-ktxserialization
and quickjs-kt-convereter-moshi (JVM only).
Add the dependency
implementation("io.github.dokar3:quickjs-kt-converter-ktxserialization:<VERSION>")
// Or use the moshi converter
implementation("io.github.dokar3:quickjs-kt-converter-moshi:<VERSION>")Add the type converters of your classes
import com.dokar.quickjs.conveter.SerializableConverter
// For moshi
import com.dokar.quickjs.conveter.JsonClassConverter
@kotlinx.serialization.Serializable
// For moshi
@com.squareup.moshi.JsonClass(generateAdapter = true)
data class FetchParams(val url: String, val method: String)
quickJs {
addTypeConverters(SerializableConverter<FetchParams>())
// For moshi
addTypeConverters(JsonClassConverter<FetchParams>())
asyncFunction<FetchParams, String>("fetch") {
// Use the typed fetch params
val (url, method) = it
TODO()
}
val result = evaluate<String>(
"""await fetch({ url: "https://example.com", method: "GET" })"""
)
}[!NOTE] Functions with generic <T, R> require exactly 1 parameter on the JS side, it will throw if no parameter is passed or multiple parameters are passed.
Most of functions may throw:
IllegalStateException, if some function was called after calling close
evaluate() and compile() may throw:
QuickJsException, if a JavaScript error occurred or failed to map a type between JavaScript and KotlinIf you find other suspicious errors, please feel free to open an issue to report
js-eval but it has some Web API polyfills to run the bundled openai-node
# Install Zig compiler (for cross-compiling)
brew install zig
# Install CMake and Ninja (if not already installed)
brew install cmake ninja# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y cmake ninja-build
# Install Zig
# Download from https://ziglang.org/download/If you need to build for Android, install Android SDK and NDK.
The script supports different modes for local development and CI/CD environments:
# Local development (detailed output)
./publish-local.sh
# CI mode (minimal output)
./publish-local.sh --quiet
# or
./publish-local.sh --ci
# or
./publish-local.sh -q
# Skip clean step (faster rebuild)
./publish-local.sh --skip-clean
# Show help
./publish-local.sh --helpScript features:
# 1. Set execution permissions
chmod +x gradlew
chmod +x quickjs/native/cmake/*.sh
# 2. Configure local.properties (based on your platform)
# macOS ARM64
echo "JAVA_HOME_MACOS_AARCH64=/path/to/your/java/home" >> local.properties
# macOS x64
echo "JAVA_HOME_MACOS_X64=/path/to/your/java/home" >> local.properties
# Linux x64
echo "JAVA_HOME_LINUX_X64=/path/to/your/java/home" >> local.properties
# Linux ARM64
echo "JAVA_HOME_LINUX_AARCH64=/path/to/your/java/home" >> local.properties
# 3. Build and publish
./gradlew clean build publishToMavenLocalAfter successful publishing to ~/.m2/repository:
Add to your build.gradle.kts:
repositories {
mavenLocal() // Add local Maven repository
mavenCentral()
// ... other repositories
}
dependencies {
implementation("io.github.qdsfdhvh:quickjs-kt:<version>")
// Optional: Add converters
implementation("io.github.qdsfdhvh:quickjs-converter-ktxserialization:<version>")
// or
implementation("io.github.qdsfdhvh:quickjs-converter-moshi:<version>")
}# macOS
brew install zig
# Linux
# Download from https://ziglang.org/download/chmod +x gradlew
chmod +x quickjs/native/cmake/*.sh# macOS
export JAVA_HOME=$(/usr/libexec/java_home)
# Linux
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# Or find your Java installation pathEnsure CMake 3.10+ and Ninja are installed:
cmake --version
ninja --versionname: Build and Publish
on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Install Zig (macOS)
if: runner.os == 'macOS'
run: brew install zig
- name: Install Zig (Linux)
if: runner.os == 'Linux'
run: |
wget https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
tar -xf zig-linux-x86_64-0.11.0.tar.xz
echo "$PWD/zig-linux-x86_64-0.11.0" >> $GITHUB_PATH
- name: Publish to Maven Local
run: ./publish-local.sh --ci| Version | QuickJS Version | Date | Changes |
|---|---|---|---|
| 1.0.0-alpha14 | 2025-04-26 | 2025-11-26 | Update QuickJS to latest, add Android 64KB page support |
| 1.0.0-alpha13 | 2024-02-14 | 2024-xx-xx | Previous version |
Copyright 2024 dokar3
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.