|
| 1 | +# Architecture - Embedded Ruby VM |
| 2 | + |
| 3 | +## Three-Layer Design |
| 4 | + |
| 5 | +``` |
| 6 | +Layer 3: High-Level Kotlin (RubyInterpreterExtensions.kt, ScriptBatch.kt) |
| 7 | + | Ergonomic APIs: executeBatch(), batch(), executeSync() |
| 8 | + v |
| 9 | +Layer 2: Low-Level Kotlin (RubyInterpreter.kt, RubyScript.kt) |
| 10 | + | Direct wrappers over C API: enqueue(), execute(latch) |
| 11 | + v |
| 12 | +Layer 1: C Core (ruby-api-loader.h, ruby-interpreter.h, ruby-vm.c) |
| 13 | + Ruby VM integration: ruby_api_load(), ruby_interpreter_enqueue() |
| 14 | +``` |
| 15 | + |
| 16 | +**Principle:** "Easy things should be easy, hard things should be possible." |
| 17 | + |
| 18 | +- Layer 3 handles 80% of use cases (batch execution, simple workflows) |
| 19 | +- Layer 2 enables custom synchronization with external systems (e.g., coordinating Ruby scripts + external DB via shared CountDownLatch) |
| 20 | +- Layer 1 stays minimal: platform-agnostic Ruby VM integration, thread management, IPC, asset loading |
| 21 | + |
| 22 | +**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. |
| 23 | + |
| 24 | +## Library Loading Architecture |
| 25 | + |
| 26 | +The project supports both **static** and **dynamic** linking through a unified API in `ruby-api-loader.h`: |
| 27 | + |
| 28 | +```c |
| 29 | +int ruby_api_load(const char* lib_path, RubyAPI* api); |
| 30 | +``` |
| 31 | +
|
| 32 | +### Dynamic Loading (Default) |
| 33 | +
|
| 34 | +Used by Kotlin/Native and JNI platforms: |
| 35 | +
|
| 36 | +1. **Asset Extraction**: `libassets.a` extracts Ruby runtime to cache directory at first run |
| 37 | +2. **Dependency Preloading**: `load_dependencies_from_file()` reads `libembedded-ruby.deps` and preloads `libruby.so` with `RTLD_GLOBAL` |
| 38 | +3. **Main Library Loading**: `ruby_api_load()` uses `dlopen()` to load `libembedded-ruby.so` |
| 39 | +4. **Symbol Resolution**: Function pointers populated via `dlsym()` |
| 40 | +
|
| 41 | +Key files: |
| 42 | +- `libembedded-ruby.so` - Shared library with all Ruby VM functions exported |
| 43 | +- `libembedded-ruby.deps` - Dependency list (generated by CMake) |
| 44 | +- `libassets.a` - Static library for asset extraction |
| 45 | +- `ruby-api-loader.h` - API loader with integrated dependency preloading |
| 46 | +
|
| 47 | +CMake uses `--whole-archive` to export symbols from `libruby-vm.a` into `libembedded-ruby.so`: |
| 48 | +```cmake |
| 49 | +if(UNIX AND NOT APPLE) |
| 50 | + target_link_libraries(embedded-ruby PRIVATE |
| 51 | + -Wl,--whole-archive logging ruby-vm -Wl,--no-whole-archive) |
| 52 | +endif() |
| 53 | +``` |
| 54 | + |
| 55 | +### Static Loading (Optional) |
| 56 | + |
| 57 | +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. |
| 58 | + |
| 59 | +### Architecture Constraints |
| 60 | + |
| 61 | +- Ruby runtime (`libruby.so`) must be deployed at runtime, not hardcoded at build time |
| 62 | +- `libembedded-ruby.so` cannot statically link Ruby (would create circular dependency) |
| 63 | +- Asset system handles runtime extraction of Ruby and its dependencies |
| 64 | + |
| 65 | +## Design Decisions |
| 66 | + |
| 67 | +### Why Kotlin Wrappers Instead of C Extensions? |
| 68 | + |
| 69 | +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. |
| 70 | + |
| 71 | +### Why Hybrid Static/Dynamic Library Loading? |
| 72 | + |
| 73 | +- **Runtime deployment requirement**: Ruby runtime extracted at runtime |
| 74 | +- **Platform flexibility**: JVM/Android uses JNI+dynamic, Kotlin/Native uses cinterop+dlopen, pure C can use static |
| 75 | +- **Transparent API**: Same function works for both approaches |
| 76 | +- Trade-off: dynamic loading adds small runtime overhead but enables runtime asset extraction |
| 77 | + |
| 78 | +## Cross-Platform Patterns |
| 79 | + |
| 80 | +### Time APIs |
| 81 | +- **JVM**: `System.currentTimeMillis()` |
| 82 | +- **Native**: `clock_gettime(CLOCK_MONOTONIC, ...)` via `platform.posix` |
| 83 | + |
| 84 | +### File APIs |
| 85 | +- **JVM**: `java.io.File(path).readText()` |
| 86 | +- **Native**: `kotlin.io.path.Path(path).readText()` |
| 87 | + |
| 88 | +### Multi-platform APIs |
| 89 | +- Use `actual`/`expect` for platform-specific implementations |
| 90 | +- Platform-specific code: JNI for JVM, cinterop for Native |
0 commit comments