Skip to content

Scorbutics/embedded-ruby-vm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

65 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Embedded Ruby VM

License: MIT Kotlin Ruby

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.

✨ Features

  • πŸš€ 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

πŸ—οΈ Architecture

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      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Library Loading Architecture

The project supports both static and dynamic linking approaches through a unified API:

Dynamic Loading (Kotlin/Native, JNI):

  • libembedded-ruby.so is loaded at runtime via dlopen()
  • Ruby runtime (libruby.so) is extracted by the asset system at first run
  • Dependency preloading via .deps file ensures all shared libraries are available
  • Uses --whole-archive linker 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

Supported Platforms

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 ⚠️ Disabled*
macOS arm64, x86_64 cinterop ⚠️ Disabled*

*iOS and macOS targets are currently disabled due to Kotlin/Native platform limitations but can be re-enabled on supported build hosts.

πŸš€ Quick Start

Prerequisites

  • 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 be embedded-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 the docker-dev.sh script for convenient usage.

Build Everything

# 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=Debug

That's it! Gradle automatically:

  1. Detects your OS and architecture
  2. Runs CMake to compile native libraries
  3. Compiles Kotlin code
  4. Packages everything into distributable artifacts

Run Examples

# Run the improved API example (recommended)
cd examples/kotlin-jvm
../../gradlew runExample

# Run original example
../../gradlew runExample -PexampleClass=JvmExample

# Run all examples
../../gradlew runAllExamples

πŸ“– Usage

Kotlin Multiplatform (Recommended)

Quick Start - Simple Batch Execution

import 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")
}

Advanced Usage - Builder Pattern with Named Scripts

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}")
}

Synchronous Execution (Blocking)

RubyInterpreter.create(...).use { interpreter ->
    val exitCode = interpreter.executeSync(
        scriptContent = "puts 'This blocks until completion'",
        timeoutSeconds = 10
    )
    println("Exit code: $exitCode")
}

Low-Level API (For Advanced Use Cases)

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

C API

#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);

Java/JNI API

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);
});

πŸ“‚ Project Structure

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

πŸ”§ Platform-Specific Builds

Android

# Quick iteration (arm64 only)
./gradlew :ruby-vm-kmp:assembleDebug -PtargetArch=arm64

# Production build (all ABIs)
./gradlew :ruby-vm-kmp:assembleRelease -PtargetArch=all

Desktop JVM

# Build JAR with embedded native library
./gradlew :ruby-vm-kmp:desktopJar

# For specific architecture
./gradlew :ruby-vm-kmp:desktopJar -PtargetArch=x86_64

Linux Native (cinterop - without JVM)

# Build native executable (no JVM required)
./gradlew :ruby-vm-kmp:linuxX64MainBinaries

# Build native C library for Linux
./gradlew buildNativeLibsLinux

Note: This target uses Kotlin/Native with cinterop to directly call C functions, providing a JVM-free native Linux binary.

iOS (when enabled)

# Build for device
./gradlew buildNativeLibsIOS -PtargetArch=arm64

# Build for simulator
./gradlew buildNativeLibsIOS -PtargetArch=x86_64

Native Libraries Only

# All platforms
./gradlew buildAllNativeLibs

# Specific platform
./gradlew buildNativeLibsAndroid
./gradlew buildNativeLibsDesktop
./gradlew buildNativeLibsIOS
./gradlew buildNativeLibsMacOS
./gradlew buildNativeLibsLinux

πŸ§ͺ Testing

# 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 tests

🎯 Key Features Explained

Embedded Assets

The 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)

Communication Architecture

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

Platform-Agnostic Logging

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

Threading Model

  • Main VM Thread: Runs the Ruby interpreter
  • Log Reader Thread: Reads stdout/stderr from Ruby
  • Script Execution: Asynchronous with completion callbacks

πŸ“š Documentation

  • CLAUDE.md - Comprehensive technical documentation
    • Architecture deep-dive
    • Low-level API reference (C, JNI, Kotlin)
    • Build system details
    • Development guidelines
    • Troubleshooting

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

  1. Clone the repository
  2. Install prerequisites (JDK, CMake, Gradle)
  3. Build the project: ./gradlew build
  4. Run tests: ./gradlew test

Adding Features

For low-level Ruby VM features (requires C changes):

  1. Extend C API in core/ruby-vm/ruby-interpreter.h
  2. Implement in core/ruby-vm/*.c
  3. Update JNI bindings in jni/ruby_vm_jni.c (for JVM platforms)
  4. Update Kotlin bindings in kmp/src/
  5. Add tests in tests/

For Kotlin convenience APIs (no C changes needed):

  1. Add common interface in kmp/src/commonMain/kotlin/
  2. Implement for JVM in kmp/src/jvmMain/kotlin/
  3. Implement for Native in kmp/src/nativeMain/kotlin/
  4. Add examples in examples/kotlin-jvm/

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Ruby programming language and community
  • Kotlin Multiplatform team
  • minizip-ng for zip handling

πŸ“ž Support

For issues, questions, or contributions, please open an issue on the GitHub repository.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •