Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Ruby One-Binary Tool, Enhanced with Kompo

ahogappa
April 18, 2025
21

The Ruby One-Binary Tool, Enhanced with Kompo

ahogappa

April 18, 2025
Tweet

Transcript

  1. About me • Sho Hirano(@ahogappa) • STORES, Inc. • I'm

    interested in `require`, executable binaries, and linkers. 2
  2. What is Kompo? • A tool that packs Ruby into

    a single binary • Combines all Ruby scripts and Gems into one binary • Based on the concept of “run easily, anywhere, and fast enough” • 梱包(こんぽう) 4
  3. Why do we want a one binary? • When distributing

    a Ruby project, it might not run because of version mismatches or missing libraries. • I want it to work by handing out just one file ◦ for example, when distributing a Ruby game. • Back in 2023, no one‑binary tool met my needs: ◦ easy to use ◦ cross‑platform ◦ compatible with the latest Ruby 5
  4. Kompo's goal, or its enhanced point as the tool •

    Can be turned into one binary easily • Supports multiple platforms • Requires no additional installation • Runs at a decent speed 6
  5. What is a one binary Ruby? 7 Ruby main.rb a.rb

    b.rb main.rb a.rb b.rb main.rb a.rb b.rb
  6. Kompo so far • Achieves this by overriding `require`: ◦ When

    a Ruby file is loaded, it is fetched from the embedded data instead of the filesystem • Statically links native extensions into the binary • For example, it could run programs built with Sinatra + SQLite ◦ Cannot yet run large Ruby projects like Rails 8
  7. Kompo so far 10 Ruby foo.rb bar.rb baz.rb main.rb Kernel#require

    Kernel#require_relative Kernel#autoload libkompo.a
  8. Problems with Kompo 14 def require(file) eval File.read(file) # =>

    uninitialized constant Foo end autoload :Foo, './foo.rb' p Foo class Foo; end
  9. Problems with Kompo 15 def require(file) Object.send(:remove_const, :Foo)  eval File.read(file)

    end autoload :Foo, './foo.rb' p Foo # => Foo class Foo; end Object.constants # => [..., :Foo, ...]
  10. Problems with Kompo 16 def require(file) Object.send(:remove_const, :Foo)  eval File.read(file)

    end autoload :Foo, './foo.rb' p Foo::Bar # => uninitialized constant Foo::Bar # => 🤯 class Foo autoload :Bar, './bar.rb' end class Foo class Bar; end end
  11. Problems with Kompo • The original design had fundamental flaws.

    • Users could not freely access files in one binary. ◦ for example, they could not use File.read. • Because it specialized only in require, it could embed only Ruby scripts, even though programs may also need YAML, CSV, and other files. • It could not handle directories, so iterating with `Dir.children` was impossible. 17
  12. How to solve this • Construct a virtual filesystem inside

    the one binary. • When Ruby calls functions such as read or open, allow it to choose whether to load data from the physical file system or from the embedded binary. 19
  13. Build a file system within the one binary • Retrieve

    the data embedded in the binary when libc's read or read is called. • Supporting only read‑only functions is sufficient; covering every feature is unnecessary. 20
  14. Methods considered • Using FUSE ◦ Requires additional installation on

    the user's side • Using SquashFS ◦ Also requires installation by the user 21
  15. Allow the user to choose the read source • When

    Ruby calls read, it must be able to choose whether to call libc's read or kompo's read. ◦ A straightforward implementation would lead to recursion. • I want to link libc's read while also linking a custom read that performs additional processing. ◦ In other words, we want to intercept the call. 22 CRuby code read(fd); kompo code ssize_t read(int fd); { … read(fd); … } Usually, libc's read is called. This read refers to itself, not to libc.
  16. Methods considered • Patch libc ◦ Hard to maintain •

    Use the linker option `--wrap` ◦ It is a GNU extension, so some linkers do not support it • Hook the system call ◦ Difficult to implement 23
  17. Kompo's solution • Build our own filesystem ◦ Read‑only is

    enough ◦ Simulate the filesystem • Dynamically getting a function address using `dlsym(RTLD_NEXT)` ◦ The implementation stays very simple ◦ Like `--wrap`, it is an extension, but easier to compile 24
  18. Usage in kompo 25 1:pub static READ_HANDLE: std::sync::LazyLock< 2: unsafe

    extern "C-unwind" fn( 3: fd: libc::c_int, 4: buf: *mut libc::c_void, 5: count: libc::size_t, 6: ) -> libc::ssize_t, 7:> = std::sync::LazyLock::new(|| unsafe { 8: let handle = libc::dlsym(libc::RTLD_NEXT, b"read\0".as_ptr() as _); 9: std::mem::transmute::< 10: *mut libc::c_void, 11: unsafe extern "C-unwind" fn( 12: fd: libc::c_int, 13: buf: *mut libc::c_void, 14: count: libc::size_t, 15: ) -> libc::ssize_t, 16: >(handle) 17:}); Store libc's `read` function in a static variable.
  19. 27 libc functions in ruby - open(2) - read(2) -

    stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb …
  20. in ruby script File.open “/real/b/c.rb” 28 libc functions in ruby

    - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb …
  21. 29 in ruby script require “/real/b/c.rb” physical file system -

    /real/b/c.rb - /real/b/d.rb … libc functions in ruby - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) …
  22. 30 libc functions in ruby - open(2) - read(2) -

    stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb …
  23. 31 libc functions in ruby - open(2) - read(2) -

    stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system. physical file system - /real/b/c.rb - /real/b/d.rb …
  24. 32 libc functions in ruby - open(2) - read(2) -

    stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system. physical file system - /real/b/c.rb - /real/b/d.rb … virtual file system - /kompo/b/c.rb - /kompo/b/d.rb …
  25. 33 in ruby script File.open “/kompo/b/c.rb” libc functions in ruby

    - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb … virtual file system - /kompo/b/c.rb - /kompo/b/d.rb … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system.
  26. 34 in ruby script File.open “./c.rb” /real/b current directory libc

    functions in ruby - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb … virtual file system - /kompo/b/c.rb - /kompo/b/d.rb … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system.
  27. 35 in ruby script Dir.chdir “/kompo/b” /kompo/b current directory libc

    functions in ruby - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb … virtual file system - /kompo/b/c.rb - /kompo/b/d.rb … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system.
  28. 36 in ruby script File.open “./c.rb” /kompo/b libc functions in

    ruby - open(2) - read(2) - stat(2) - opendir(2) - readdir(2) - chdir(2) - getcwd(2) … physical file system - /real/b/c.rb - /real/b/d.rb … virtual file system - /kompo/b/c.rb - /kompo/b/d.rb … kompo wrap - Access binary data under these conditions: - Absolute path: Check the embedded file system. - Relative path: Consider the current directory and check if the file exists in the binary or file system. current directory
  29. Difference from Kompo’s goal • Can be turned into one

    binary easily ◦ Support coming soon • Cross-platform support ◦ Not yet available • Requires no additional installation ◦ Already achieved • Runs at a decent speed ◦ Needs refactoring 37
  30. Conclusion • The previous kompo approach sometimes failed to create

    a one binary. • I tried various implementations and found a workable method. • With this implementation, I managed to make Rails run as one binary. • From now on, I want to focus on turning it into a gem and adding cross-platform support. 38