Skip to content

Latest commit

 

History

History
90 lines (64 loc) · 3.77 KB

File metadata and controls

90 lines (64 loc) · 3.77 KB

Architecture - Embedded Ruby VM

Three-Layer Design

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.

Library Loading Architecture

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

Dynamic Loading (Default)

Used by Kotlin/Native and JNI platforms:

  1. Asset Extraction: libassets.a extracts Ruby runtime to cache directory at first run
  2. Dependency Preloading: load_dependencies_from_file() reads libembedded-ruby.deps and preloads libruby.so with RTLD_GLOBAL
  3. Main Library Loading: ruby_api_load() uses dlopen() to load libembedded-ruby.so
  4. Symbol Resolution: Function pointers populated via dlsym()

Key files:

  • libembedded-ruby.so - Shared library with all Ruby VM functions exported
  • libembedded-ruby.deps - Dependency list (generated by CMake)
  • libassets.a - Static library for asset extraction
  • ruby-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()

Static Loading (Optional)

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.

Architecture Constraints

  • Ruby runtime (libruby.so) must be deployed at runtime, not hardcoded at build time
  • libembedded-ruby.so cannot statically link Ruby (would create circular dependency)
  • Asset system handles runtime extraction of Ruby and its dependencies

Design Decisions

Why Kotlin Wrappers Instead of C Extensions?

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.

Why Hybrid Static/Dynamic Library Loading?

  • 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

Cross-Platform Patterns

Time APIs

  • JVM: System.currentTimeMillis()
  • Native: clock_gettime(CLOCK_MONOTONIC, ...) via platform.posix

File APIs

  • JVM: java.io.File(path).readText()
  • Native: kotlin.io.path.Path(path).readText()

Multi-platform APIs

  • Use actual/expect for platform-specific implementations
  • Platform-specific code: JNI for JVM, cinterop for Native