Layer 3: High-Level Kotlin (RubyInterpreterExtensions.kt, ScriptBatch.kt)
| Ergonomic APIs: executeBatch(), batch(), executeSync()
v
Layer 2: Low-Level Kotlin (RubyInterpreter.kt, RubyScript.kt)
| Direct wrappers over C API: enqueue(), execute(latch)
v
Layer 1: C Core (ruby-api-loader.h, ruby-interpreter.h, ruby-vm.c)
Ruby VM integration: ruby_api_load(), ruby_interpreter_enqueue()
Principle: "Easy things should be easy, hard things should be possible."
- Layer 3 handles 80% of use cases (batch execution, simple workflows)
- Layer 2 enables custom synchronization with external systems (e.g., coordinating Ruby scripts + external DB via shared CountDownLatch)
- Layer 1 stays minimal: platform-agnostic Ruby VM integration, thread management, IPC, asset loading
Don't flatten the layers. Each serves a purpose. Layer 2 costs ~15 lines to maintain and removing it would force ugly workarounds for edge cases.
The project supports both static and dynamic linking through a unified API in ruby-api-loader.h:
int ruby_api_load(const char* lib_path, RubyAPI* api);Used by Kotlin/Native and JNI platforms:
- Asset Extraction:
libassets.aextracts Ruby runtime to cache directory at first run - Dependency Preloading:
load_dependencies_from_file()readslibembedded-ruby.depsand preloadslibruby.sowithRTLD_GLOBAL - Main Library Loading:
ruby_api_load()usesdlopen()to loadlibembedded-ruby.so - Symbol Resolution: Function pointers populated via
dlsym()
Key files:
libembedded-ruby.so- Shared library with all Ruby VM functions exportedlibembedded-ruby.deps- Dependency list (generated by CMake)libassets.a- Static library for asset extractionruby-api-loader.h- API loader with integrated dependency preloading
CMake uses --whole-archive to export symbols from libruby-vm.a into libembedded-ruby.so:
if(UNIX AND NOT APPLE)
target_link_libraries(embedded-ruby PRIVATE
-Wl,--whole-archive logging ruby-vm -Wl,--no-whole-archive)
endif()Enabled by defining RUBY_STATIC at compile time. Same ruby_api_load() signature — function pointers are assigned directly instead of via dlsym. Use for pure C apps or platforms without dynamic loading.
- Ruby runtime (
libruby.so) must be deployed at runtime, not hardcoded at build time libembedded-ruby.socannot statically link Ruby (would create circular dependency)- Asset system handles runtime extraction of Ruby and its dependencies
C API stays minimal and focused on Ruby VM integration. Kotlin is better for high-level patterns (builders, sealed classes, extensions), easier to maintain/test, and avoids cross-platform C synchronization headaches.
- Runtime deployment requirement: Ruby runtime extracted at runtime
- Platform flexibility: JVM/Android uses JNI+dynamic, Kotlin/Native uses cinterop+dlopen, pure C can use static
- Transparent API: Same function works for both approaches
- Trade-off: dynamic loading adds small runtime overhead but enables runtime asset extraction
- JVM:
System.currentTimeMillis() - Native:
clock_gettime(CLOCK_MONOTONIC, ...)viaplatform.posix
- JVM:
java.io.File(path).readText() - Native:
kotlin.io.path.Path(path).readText()
- Use
actual/expectfor platform-specific implementations - Platform-specific code: JNI for JVM, cinterop for Native