Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Bringing Ruby to the JVM: Making the Impossible...

headius
August 19, 2023

Bringing Ruby to the JVM: Making the Impossible Possible

Over the past 18 years, the JRuby team has worked to bring Ruby to the JVM, defying impossible challenges and building the most successful alternative Ruby implementation in the world. Today, JRuby is actively developed and deployed at thousands of Ruby and Java shops around the world... but how did we get here? This talk provides a retrospective of the JRuby project, with examples of modern JRuby usage along the way.

Talk presented on August 19, 2023 at the Carolina Code Conference

headius

August 19, 2023
Tweet

More Decks by headius

Other Decks in Programming

Transcript

  1. About me • Charles Oliver Nutter (@headius) • Born and

    raised in Minnesota • Professional developer since 1996 • Currently at Red Hat (IBM) • Programming languages and natural languages
  2. 40 Years of Programming • Early 80s: Atari 400 (Atari

    BASIC), IBM PC XT (IBM BASIC) • Late 80s: K&R C, some x86 assembly • Mid-90s: Visual Basic, Delphi, C++ for Win32 development • 1995: AppleScript and Perl for University of Minnesota webapps • 1996: Java starting with v1.0.2, applets mostly
  3. Languages are Tools • Good developers can get work done

    using any language • Good languages encourage good developers • You need to know your tools and know them well • You need more than one tool
  4. Java Finds Its Way • Java before 1.3 was too

    slow, too big for desktop or scripting • Long-running server applications fi t better • 1996-1998: U of MN and IBM collaboration on "Student Server" • Predated most app server tech we know today • Taught me how overcomplicated software can be • 1999-2006: Java Web and Enterprise Application Architect
  5. Ruby • Born the same year as Java (1995) •

    In fl uences: Perl, Lisp, Smalltalk, Python, POSIX and C • Heavily dynamic: method calls, fi elds, constants, class structure • Prettier than Perl, less arcane than LISP, designed for happiness
  6. # The Greeter class class Greeter def initialize(name) @name =

    name.capitalize end def salute puts "Hello #{@name}!" end end # Create a new object g = Greeter.new("world") # Output "Hello World!" g.salute
  7. # The Greeter class class Greeter def initialize(name) @name =

    name.capitalize end def salute puts "Hello #{@name}!" end end # Create a new object g = Greeter.new("world") # Output "Hello World!" g.salute # The Greeter class public class Greeter { public Greeter(String name) { this.name = name.capitalize(); } public void salute() System.out.println("Hello " + name + "!"); end end # Create a new object Greeter g = new Greeter("world"); # Output "Hello World!" g.salute();
  8. # The Greeter class class Greeter def initialize(name) @name =

    name.capitalize end def salute puts "Hello #{@name}!" end end # Create a new object g = Greeter.new("world") # Output "Hello World!" g.salute # The Greeter class public class Greeter { public Greeter(String name) { this.name = name.capitalize(); } public void salute() System.out.println("Hello " + name + "!"); end end # Create a new object Greeter g = new Greeter("world"); # Output "Hello World!" g.salute();
  9. RubyConf 2004 • My fi rst Ruby conference • Koichi

    Sasada presented Yet Another Ruby VM (YARV) • Largely the same VM in CRuby today • Hallway hacking produced early versions of key Ruby tools • A total of 65 attendees present
  10. Ruby Finds Its Way • Ruby was for "scripting", but

    Python, Perl were still more popular • 2004: Ruby on Rails announced • 2005: Rails takes off, changing web development forever • Convention over con fi guration: instant applications • Many other frameworks started to pay attention
  11. Can I use Ruby? • Fully committed to the Java

    ecosystem • Huge corpus of libraries and tools • Hotspot, JRockit JITs solved performance issues • Parallel, concurrent GCs improved memory overhead • What if we could make a sort of "J-Ruby" on the JVM?
  12. JRuby is (Re)born • 2001-2004: Early work on JRuby •

    Jan Arne Petersen started by porting the parser • Basic core classes added, simple scripting use cases • By 2004, only a few active developers working • Most active was Tom Enebo, my U of MN co-worker! • I rolled up my sleeves and got to work
  13. JRuby Finds Its Way • Ruby on Rails sparked sudden

    interest in Ruby web apps • Java platform is great for web app hosting • The obvious path: JRuby on Rails • Could it be done? • Should it be done?
  14. The Road to Rails • Almost nothing worked in JRuby

    ca. 2005 • We started with basic tools and fi lled in the blanks • Minimal compliance tests available so we ran everything we could • And we quickly ran into challenges adapting Ruby to JVM • Now the fun starts 😁
  15. IRB: Interactive Ruby • First step: run the standard REPL

    • Heavy work on language compatibility • "eval" behavior: parse and run code • Console behavior: stdio and readline • And then one day in early 2006, it worked! $ irb irb(main):001:0> 1 + 2 => 3 irb(main):002:1* class Foo irb(main):003:2* def foo irb(main):004:2* puts 1 irb(main):005:1* end irb(main):006:0> end => :foo irb(main):007:0> Foo.new.foo 1 => nil
  16. Rake and RubyGems • After RubyConf 2004 and 2005, tools

    started to solidify • Rake for build tooling (Ruby's equivalent to make) • File IO, fi lesystem manipulation needed lots of work • RubyGems for reusable packaging, dependency management • Network IO for fetching, zlib/tar for archives, YAML for descriptors
  17. But it's JRuby • Ruby and Java must be able

    to call each other • Primary goal: make Java classes, methods feel like Ruby • Stretch goal: implement JRuby and libraries in Ruby calling Java • Best Java integration experience of any dynamic languge
  18. Ruby/Java Integration (JI) • Each Java class gets a mirror

    Ruby class • With full mutability: add methods and Ruby code will see them • Java objects wrapped in an IRubyObject wrapper • Numerics and strings auto-convert across the boundary • Common idioms made Ruby-like: x.setFoo(...) becomes x.foo = ... • Interface impl and class extension
  19. Putting It All Together • Spring 2006: JRuby on Rails

    routes its fi rst request • It was so slow! 😂 • ...because we were still in development mode! 🤦 • JavaOne 2006: JRuby on Rails presented for the fi rst time • Sun Microsystems, creator of Java, brings us on board • Full-time work on JRuby!
  20. Reimplementing the World • Quickly became obvious how different Java

    and Ruby are • String and IO handling • Dynamic everything • Heavy native library use • One by one we reimplemented key parts of Java for JRuby
  21. Regular Expressions • Java's Regex crashes on large alternations (think

    /a*|b*/) • Like one use in Rails to parse image binaries 😬 • We tried every Java regex engine we could fi nd • JRegex was the best at the time • Eventually a user would port CRuby's regex engine...
  22. $ jruby -w -e 'java.util.regex.Pattern.matches("(a|b)*", "a" * 10000 + "b"

    * 10000)' Error: Your application used more stack memory than the safety cap of 2048K. Specify -J-Xss####k to increase it (#### = cap size in KB). java.lang.StackOverflowError at java.util.regex.Pattern$Loop.match(java/util/regex/Pattern.java:5074) at java.util.regex.Pattern$GroupTail.match(java/util/regex/Pattern.java:5000) at java.util.regex.Pattern$BranchConn.match(java/util/regex/Pattern.java:4878) at java.util.regex.Pattern$BmpCharProperty.match(java/util/regex/Pattern.java:4134) at java.util.regex.Pattern$Branch.match(java/util/regex/Pattern.java:4914) at java.util.regex.Pattern$GroupHead.match(java/util/regex/Pattern.java:4969) at java.util.regex.Pattern$Loop.match(java/util/regex/Pattern.java:5078) at java.util.regex.Pattern$GroupTail.match(java/util/regex/Pattern.java:5000) at java.util.regex.Pattern$BranchConn.match(java/util/regex/Pattern.java:4878) at java.util.regex.Pattern$BmpCharProperty.match(java/util/regex/Pattern.java:4134) at java.util.regex.Pattern$Branch.match(java/util/regex/Pattern.java:4914) at java.util.regex.Pattern$GroupHead.match(java/util/regex/Pattern.java:4969) at java.util.regex.Pattern$Loop.match(java/util/regex/Pattern.java:5078) at java.util.regex.Pattern$GroupTail.match(java/util/regex/Pattern.java:5000) at java.util.regex.Pattern$BranchConn.match(java/util/regex/Pattern.java:4878) at java.util.regex.Pattern$BmpCharProperty.match(java/util/regex/Pattern.java:4134) ...
  23. Joni • Java Oniguruma ported from C by Marcin Mielżyński

    • Bytecode-based register machine, no stack issues • byte[] matching (also ported to char[] for Nashorn) • Pluggable character encodings, regex grammars • https://github.com/jruby/joni
  24. A String is not just a String • Ruby's String

    is a byte[] plus an Encoding • ...and mutable by default • Multiple differently-encoded strings in memory • "Binary" is considered an encoding (ASCII-8BIT) • Emulating this with java.lang.String was impossible • Implemented our own String and Encoding logic 🤯
  25. JCodings • byte[]-based character encoding and transcoding framework • Ported

    from CRuby's own "multilingualization" backend (M17N) • All major encodings supported, plus most weird ones • I have not ported EBCDIC logic yet... • https://github.com/jruby/jcodings
  26. Secure Sockets • SSL in CRuby provided by OpenSSL •

    Large, complicated wrapper extension • JRuby-OpenSSL was needed to bridge the gap • Ola Bini worked tirelessly on "OpenSSL for JVM" • Still in use today (we need help updating or replacing it!)
  27. Native Interop • CRuby's extension API is too invasive, exposes

    VM guts • Biggest thing holding back CRuby VM development • JRuby introduces the Java Native Runtime (JNR) • Tools for binding and calling native libraries • https://github.com/jnr • Later, ported Ruby FFI from Rubinius for JRuby and CRuby
  28. Ruby FFI class Timeval < FFI::Struct layout :tv_sec => :ulong,

    :tv_usec => :ulong end module LibC extend FFI::Library ffi_lib FFI::Library::LIBC attach_function :gettimeofday, [ :pointer, :pointer ], :int end t = Timeval.new LibC.gettimeofday(t.pointer, nil)
  29. Project Panama • Foreign function API • With JIT help

    to make direct calls • Foreign memory API • JVM-assisted access, lifecycle • API extraction from C/++ headers • Save time setting up bindings
  30. //point.h struct Point2d { double x; double y; }; double

    distance(struct Point2d); jextract --source -t org.jextract point.h import java.lang.foreign.*; import static org.jextract.point_h.*; import org.jextract.Point2d; class TestPoint { public static void main(String[] args) { try (var session = MemorySession.openCon fi ned()) { MemorySegment point = MemorySegment.allocateNative(Point2d.$LAYOUT(), session); Point2d.x$set(point, 3d); Point2d.y$set(point, 4d); distance(point); } } }
  31. JIT Compiler • Interpretation worked, but was much too slow

    • All state on heap, no inlining of code, JVM could not optimize • 2007-2008: fi rst ever bytecode JIT on JVM (as far as I know) • Which also made this the fi rst native JIT for Ruby • After N calls, translate AST to bytecode • JVM picks it up and does its thing
  32. Catch up, JVM! • More and more of JRuby leaving

    standard Java behind • New string/regexp, new IO, native calls, bytecode JIT • 2007: InvokeDynamic is born • Bytecode and API for teaching the JVM new tricks • Dynamic call sites + method pointers + adapters • JRuby integrates "indy" immediately, helps design features
  33. InvokeDynamic Performance Times faster than JRuby Java 8 no indy

    0 1.25 2.5 3.75 5 Mandelbrot Red/Black 4.05x 3.92x 3.74x 3.68x 3.72x 1.97x Java 8 indy Java 11 indy Java 17 indy 3.28x CRuby JIT 1.86x CRuby JIT
  34. Indy + Graal JIT? Times faster than JRuby Java 8

    no indy 0 4 8 12 16 Mandelbrot Red/Black 3.13x 15.7x 4.05x 3.92x 3.74x 3.68x 3.72x 1.97x Java 8 indy Java 11 indy Java 17 indy Graal CE indy Escape analysis But not always better
  35. New Compiler Design • 2009-2015: Subramaniam Sastry helps us write

    a new compiler • Background in C++ optimizing compilers • New Intermediate Representation (IR) based on traditional design • Basic blocks, operands, control and data fl ow analysis • Register machine rather than all stack • Bytecode JIT got simpler, IR did most of the work
  36. Virtual Threads • 2007: Ruby introduces Fiber • Single native

    thread drives several fi bers • Context switch is voluntary and explicit • Thousands of microthreads potentially • No fi bers on JVM, so we used native threads 😕 • Big, heavy, slow, and can't run very many
  37. Thread 2 Fiber 2 Fiber 1 hands o ff to

    Fiber 2, which runs immediately etc Execution fl ow Fiber 1 Wait for IO Accept socket Wait for IO Accept socket Handle request Send response etc Thread 1 Wait for IO Accept socket Thread gets descheduled from CPU but still waiting for IO Handle request Send response
  38. 5.times do t = Time.now # create 100k fibers ary

    = 100_000.times.map { Fiber.new { } } # resume and complete 100k fibers ary.each(&:resume) p Time.now - t end
  39. $ jruby fiber_test.rb [7.603s][warning][os,thread] Attempt to protect stack guard pages

    failed (0x00007fc240a00000-0x00007fc240a04000). # # A fatal error has been detected by the Java Runtime Environment: # Native memory allocation (mprotect) failed to protect 16384 bytes for # memory to guard stack pages # # An error report file with more information is saved as: # /home/headius/work/jruby/hs_err_pid75149.log # # If you would like to submit a bug report, please visit: # https://bugreport.java.com/bugreport/crash.jsp # Aborted (core dumped) 😩
  40. Project Loom • "Virtual threads" for JVM • User-mode threading

    • JVM handles scheduling • Thread does not have to pause • Perfect analog for Ruby's Fibers • Previews available in Java 19, 20 • Final is on track for 21!
  41. JRuby Today • Production deployments since 2007 • Thousands of

    apps around the world • Web, desktop, mobile, POS, fi nance, medicine, astronomy, government, ... aircraft refueling? 😬 • Solid performance, industry-leading in many areas
  42. JRuby on Rails • It's just like Ruby on Rails

    • Generate app, populate database, deploy! • But with some amazing extras! • Concurrent threading for many users (CRuby uses processes) • Deploy on any Java server as a web archive • Everything else JVM has to offer!
  43. JRuby Install • Install a JDK • Java 11+ recommended,

    there's many distributions out there • Java 8 supported for 9.4 and lower • Install JRuby • Recommended: system package, Ruby installer, Docker image • Download tarball/zip or Windows installer
  44. [] jruby $ gem install rails Fetching loofah-2.21.3.gem Fetching rails-html-sanitizer-1.6.0.gem

    Fetching rack-2.2.8.gem Fetching activesupport-7.0.7.gem Fetching nokogiri-1.15.4-java.gem Fetching actionview-7.0.7.gem Fetching actionpack-7.0.7.gem Fetching railties-7.0.7.gem Fetching mini_mime-1.1.5.gem Fetching activemodel-7.0.7.gem Fetching activerecord-7.0.7.gem Fetching activejob-7.0.7.gem Fetching activestorage-7.0.7.gem Fetching actiontext-7.0.7.gem Fetching actionmailer-7.0.7.gem Fetching actionmailbox-7.0.7.gem Fetching actioncable-7.0.7.gem Fetching rails-7.0.7.gem ... [] jruby $ rails new carolina_app create create README.md create Rakefile create .ruby-version create config.ru create .gitignore create .gitattributes create Gemfile run git init from "." ...
  45. [] carolina_app $ bin/rails generate scaffold post title:string body:text published:boolean

    invoke active_record create db/migrate/20230819122544_create_posts.rb create app/models/post.rb invoke test_unit create test/models/post_test.rb create test/fixtures/posts.yml invoke resource_route route resources :posts invoke scaffold_controller create app/controllers/posts_controller.rb invoke erb create app/views/posts create app/views/posts/index.html.erb create app/views/posts/edit.html.erb create app/views/posts/show.html.erb create app/views/posts/new.html.erb ...
  46. [] carolina_app $ rake db:migrate == 20230819122544 CreatePosts: migrating ============

    -- create_table(:posts) -> 0.0088s -> 0 rows == 20230819122544 CreatePosts: migrated (0.0097s) === [] carolina_app $ rails server => Booting Puma => Rails 7.0.7 application starting in development => Run `bin/rails server --help` for more startup options Puma starting in single mode... * Puma version: 5.6.7 (jruby 9.4.4.0-SNAPSHOT - ruby 3.1.4) ("Birdie's Version") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 22710 * Listening on http://[::1]:3000 * Listening on http://127.0.0.1:3000 Use Ctrl-C to stop
  47. Ruboto: JRuby on Android • Actively used for commercial projects

    today • Build interface with GUI builder, wire it up with Ruby code • Neglected a bit but being updated for JRuby 9.4 now! • ruboto.org
  48. Trying Ruboto IRB • Original was pulled from store because

    it asked for ALL privileges! • Duh, it was a tech demo and REPL • We will republish soon, but search for two packages to download: • Ruboto Core: JRuby and Ruboto framework as a shared package • Ruboto IRB: REPL, editor, example scripts
  49. The Future of JRuby • Compatibility, performance are pretty solid

    right now • Best support for Ruby language, core classes of any impl • Challenges remain • ...but they keep this project fun! ☺
  50. Startup and Warmup • Number one complaint from users •

    CRuby is basically designed to start up fast • Preloaders: Nailgun, Drip, Theine work "ok" • Native AOT: GraalVM breaks all dynamic features ☹ • Project CRaC, Project Leyden offer hope for JRuby
  51. Memory Usage • We use every trick in the book

    to pack memory • With real threads, we can beat CRuby at 5 concurrent users • JVM is just BIG • Simple objects are 16 bytes minimum • Project Lilliput aims to halve that • New GCs pause less, use less memory, and free unused memory
  52. Maintenance • Ruby is a big world and JRuby is

    a big project • Only two of us full-time currently • Maybe 6-12 regular contributors • JRuby supports Ruby 3.1 but 3.2 is out and 3.3 in December • Optimization on hold trying to keep up!
  53. HELP WANTED • Seeking new developers to help improve JRuby

    • Applicants must have knowledge of: • Parsers, compilers, bytecode generation, regular expressions, string encoding, cryptography, virtual threading, native interop, memory pro fi ling, code optimization, Ruby and Java and JVM • Or we'll just teach you what you need to know 😀 • We need you!
  54. Wrapping Up • My journey to JRuby has been fun

    and exciting • We've created dozens of innovations for JVM and Ruby • I knew how to do none of this when I started • With persistence and curiousity, nothing is impossible • Why not give JRuby a try and help us move it forward?