A cross-platform C library that embeds a full Ruby interpreter for use in native applications, Android, iOS, and JVM applications. Execute Ruby scripts from your Kotlin, Java, or C code with a complete Ruby runtime environment and standard library embedded directly into your binary.
- π Full Ruby 3.1.0 Runtime - Complete Ruby interpreter with standard library
- π¦ Single Binary Distribution - No external Ruby installation required
- π Cross-Platform - Android, iOS, macOS, Linux, Windows, JVM Desktop
- π§ Kotlin Multiplatform API - Unified API across all platforms
- β‘ Native Performance - Direct C integration via JNI and cinterop
- π§΅ Thread-Safe - Isolated Ruby VM with async script execution
- π Comprehensive Logging - Capture Ruby stdout/stderr via callbacks
- π― Zero Dependencies - Self-contained with embedded assets
- β¨ Ergonomic APIs - Batch execution, builder pattern, structured results
- π Execution Metrics - Track duration, success rates, and performance
The project uses a three-layer architecture with hybrid static/dynamic library loading for maximum platform compatibility:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Kotlin Multiplatform (KMP) - Unified API β
β - commonMain: Shared interfaces β
β - androidMain/jvmMain: JNI implementations β
β - nativeMain: cinterop implementations β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Native Bridge Layer β
β - JNI: For Android & JVM Desktop β
β - cinterop + dlopen: For Native platforms β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β C Core Library - Embedded Ruby VM β
β - libembedded-ruby.so (dynamic) β
β - libassets.a (static) - Asset extraction β
β - Ruby VM integration, logging, threading β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The project supports both static and dynamic linking approaches through a unified API:
Dynamic Loading (Kotlin/Native, JNI):
libembedded-ruby.sois loaded at runtime viadlopen()- Ruby runtime (
libruby.so) is extracted by the asset system at first run - Dependency preloading via
.depsfile ensures all shared libraries are available - Uses
--whole-archivelinker flag to export symbols from static libraries
Static Loading (Optional, via -DRUBY_STATIC):
- Function pointers assigned directly to statically-linked functions
- Same
ruby_api_load()function signature for both approaches - Transparent switching via compile-time define
Key Design Benefits:
- Ruby runtime deployed at runtime, not hardcoded at build time
- Single unified API regardless of linking approach
- Clean separation between build-time and runtime dependencies
| Platform | Architecture | Bridge | Status |
|---|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | JNI | β Supported |
| JVM Desktop | x86_64, arm64 | JNI | β Supported |
| Linux Native | x86_64 | cinterop | β Supported |
| iOS | arm64, simulator | cinterop | |
| macOS | arm64, x86_64 | cinterop |
*iOS and macOS targets are currently disabled due to Kotlin/Native platform limitations but can be re-enabled on supported build hosts.
- Java Development Kit (JDK) 11 or higher
- CMake 3.19.2 or higher
- Gradle (wrapper included)
- Android NDK (for Android builds only)
WARNING: Please note that ideally (and while building on host directly is supported), ALL of your commands must be executed INSIDE the docker container!
That means you will have to prefix every exposed command down here with
docker exec <container_name>(the container name should beembedded-ruby-vm-dev).The docker container stack mounted using docker-compose.yml is using a named volume and not a bind mount. In order to sync the sources, each time you are doing a source code modification, you have to remove the
source-sync-in(docker-compose run --rm source-sync-in) container which will trigger a resync next build. Alternatively, you can also use thedocker-dev.shscript for convenient usage.
# Build for your current architecture
./gradlew build
# Build for specific architecture
./gradlew build -PtargetArch=x86_64
./gradlew build -PtargetArch=arm64
./gradlew build -PtargetArch=all
# Debug build
./gradlew build -PbuildType=DebugThat's it! Gradle automatically:
- Detects your OS and architecture
- Runs CMake to compile native libraries
- Compiles Kotlin code
- Packages everything into distributable artifacts
# Run the improved API example (recommended)
cd examples/kotlin-jvm
../../gradlew runExample
# Run original example
../../gradlew runExample -PexampleClass=JvmExample
# Run all examples
../../gradlew runAllExamplesimport com.scorbutics.rubyvm.*
// Create log listener
val listener = object : LogListener {
override fun onLog(message: String) = println("[Ruby] $message")
override fun onError(message: String) = System.err.println("[Ruby Error] $message")
}
// Create interpreter with automatic cleanup
RubyInterpreter.create(
appPath = ".",
rubyBaseDir = "./ruby",
nativeLibsDir = "./lib",
listener = listener
).use { interpreter ->
// Execute multiple scripts - no manual synchronization needed!
val exitCodes = interpreter.executeBatch(
scripts = listOf(
"puts 'Hello from Ruby!'",
"puts 'Ruby version: #{RUBY_VERSION}'",
"puts '2 + 2 = #{2 + 2}'"
),
timeoutSeconds = 30
)
println("All scripts completed: $exitCodes")
}RubyInterpreter.create(...).use { interpreter ->
val results = interpreter.batch()
.addScript("puts 'Initializing...'", name = "init")
.addScript("data = [1,2,3,4,5]; puts data.sum", name = "calculate")
.addScript("puts 'Done!'", name = "cleanup")
.timeout(60)
.onEachComplete { index, result ->
println("${result.name}: ${if (result.success) "β" else "β"} (${result.durationMs}ms)")
}
.execute()
// Get metrics
val metrics = results.toMetrics()
println("Success rate: ${metrics.successCount}/${metrics.totalScripts}")
}RubyInterpreter.create(...).use { interpreter ->
val exitCode = interpreter.executeSync(
scriptContent = "puts 'This blocks until completion'",
timeoutSeconds = 10
)
println("Exit code: $exitCode")
}import com.scorbutics.rubyvm.RubyScript
import java.util.concurrent.CountDownLatch
// For custom synchronization with external systems
val latch = CountDownLatch(2) // Ruby scripts + external tasks
val script = RubyScript.fromContent("puts 'Hello from Ruby!'")
interpreter.enqueue(script) { exitCode ->
println("Script completed: $exitCode")
script.close()
latch.countDown()
}
externalSystem.doWork { latch.countDown() }
latch.await() // Wait for both#include "ruby-interpreter.h"
// Create interpreter
RubyInterpreter* interpreter = ruby_interpreter_create(
".", // Working directory
"./ruby", // Ruby stdlib location
"./lib", // Native extensions path
log_listener // Logging callbacks
);
// Create script
RubyScript* script = ruby_script_create_from_content(
"puts 'Hello from Ruby!'",
strlen("puts 'Hello from Ruby!'")
);
// Execute script
ruby_interpreter_enqueue(interpreter, script, completion_callback);
// Cleanup
ruby_script_destroy(script);
ruby_interpreter_destroy(interpreter);import com.scorbutics.rubyvm.RubyVMNative;
import com.scorbutics.rubyvm.LogListener;
// Create interpreter
long interpreterPtr = RubyVMNative.createInterpreter(
".",
"./ruby",
"./lib",
new LogListener() {
public void onLog(String message) {
System.out.println("[Ruby] " + message);
}
public void onError(String message) {
System.err.println("[Ruby Error] " + message);
}
}
);
// Create and execute script
long scriptPtr = RubyVMNative.createScript("puts 'Hello from Ruby!'");
RubyVMNative.enqueueScript(interpreterPtr, scriptPtr, exitCode -> {
System.out.println("Script completed: " + exitCode);
});embedded-ruby-vm/
βββ core/ # C library core
β βββ ruby-vm/ # Ruby VM wrapper and communication
β βββ assets/ # Embedded Ruby stdlib and scripts
β βββ logging/ # Logging system
β βββ external/ # External dependencies (Ruby libs)
βββ jni/ # JNI bindings for Android/JVM
β βββ android/ # Android-specific logging
β βββ *.c/h # JNI bridge implementation
βββ kmp/ # Kotlin Multiplatform module
β βββ src/
β β βββ commonMain/ # Shared API definitions
β β βββ androidMain/ # Android implementation (JNI)
β β βββ desktopMain/ # JVM Desktop implementation (JNI)
β β βββ nativeMain/ # iOS/macOS/Linux (cinterop)
β β βββ desktopTest/ # KMP unit tests
β βββ build.gradle.kts # KMP build configuration
βββ tests/ # Test suites (organized by technology)
β βββ native/ # Native C tests
β β βββ core/ # Core library tests
β β βββ jni/ # JNI layer tests
β β βββ jni-android/ # Android logging tests
β βββ README.md # Test documentation
βββ examples/ # Usage examples (organized by language)
β βββ java/ # Java examples
β β βββ SimpleJavaExample.java
β β βββ run-java-example.sh
β βββ kotlin-jvm/ # Kotlin/JVM examples
β β βββ JvmExample.kt # Original example (manual latch)
β β βββ ImprovedApiExample.kt # Improved API example
β β βββ build.gradle.kts # Build configuration
β βββ kotlin-native/ # Kotlin/Native examples
β β βββ linux-x64/ # Linux x64 cinterop example
β βββ README.md # Examples documentation
βββ CMakeLists.txt # Root CMake configuration
βββ build.gradle.kts # Root Gradle configuration
βββ CLAUDE.md # Detailed technical documentation
βββ README.md # This file
# Quick iteration (arm64 only)
./gradlew :ruby-vm-kmp:assembleDebug -PtargetArch=arm64
# Production build (all ABIs)
./gradlew :ruby-vm-kmp:assembleRelease -PtargetArch=all# Build JAR with embedded native library
./gradlew :ruby-vm-kmp:desktopJar
# For specific architecture
./gradlew :ruby-vm-kmp:desktopJar -PtargetArch=x86_64# Build native executable (no JVM required)
./gradlew :ruby-vm-kmp:linuxX64MainBinaries
# Build native C library for Linux
./gradlew buildNativeLibsLinuxNote: This target uses Kotlin/Native with cinterop to directly call C functions, providing a JVM-free native Linux binary.
# Build for device
./gradlew buildNativeLibsIOS -PtargetArch=arm64
# Build for simulator
./gradlew buildNativeLibsIOS -PtargetArch=x86_64# All platforms
./gradlew buildAllNativeLibs
# Specific platform
./gradlew buildNativeLibsAndroid
./gradlew buildNativeLibsDesktop
./gradlew buildNativeLibsIOS
./gradlew buildNativeLibsMacOS
./gradlew buildNativeLibsLinux# Build and run all tests
./gradlew build
# Run specific test levels
cd build
./bin/test_core # Core library tests
./bin/test_jni # JNI layer tests
./bin/test_jni_android_log # Android logging testsThe Ruby standard library (11MB) is packaged as a zip file and embedded directly into the binary using CMake's object file generation. This means:
- β No external Ruby installation needed
- β Single binary distribution
- β Consistent stdlib version across platforms
- β Platform-specific builds (aarch64, x86_64)
The Ruby VM runs in a dedicated thread and communicates via Unix domain sockets:
- Protocol:
<length>\n<script_content>β Ruby executes β<exit_code>\n - Isolation: Ruby crashes don't affect the main application
- Async Execution: Scripts are enqueued and executed sequentially
- Output Capture: All Ruby stdout/stderr is captured and forwarded to callbacks
The JNI layer uses a weak symbol pattern for pluggable logging:
- Default: No-op logging (zero overhead)
- Android: Automatically uses
__android_log_write(logcat) - Custom: Provide your own
jni_log_impl()implementation
- Main VM Thread: Runs the Ruby interpreter
- Log Reader Thread: Reads stdout/stderr from Ruby
- Script Execution: Asynchronous with completion callbacks
- CLAUDE.md - Comprehensive technical documentation
- Architecture deep-dive
- Low-level API reference (C, JNI, Kotlin)
- Build system details
- Development guidelines
- Troubleshooting
Contributions are welcome! Please feel free to submit a Pull Request.
- Clone the repository
- Install prerequisites (JDK, CMake, Gradle)
- Build the project:
./gradlew build - Run tests:
./gradlew test
For low-level Ruby VM features (requires C changes):
- Extend C API in
core/ruby-vm/ruby-interpreter.h - Implement in
core/ruby-vm/*.c - Update JNI bindings in
jni/ruby_vm_jni.c(for JVM platforms) - Update Kotlin bindings in
kmp/src/ - Add tests in
tests/
For Kotlin convenience APIs (no C changes needed):
- Add common interface in
kmp/src/commonMain/kotlin/ - Implement for JVM in
kmp/src/jvmMain/kotlin/ - Implement for Native in
kmp/src/nativeMain/kotlin/ - Add examples in
examples/kotlin-jvm/
This project is licensed under the MIT License - see the LICENSE file for details.
- Ruby programming language and community
- Kotlin Multiplatform team
- minizip-ng for zip handling
For issues, questions, or contributions, please open an issue on the GitHub repository.