$30 off During Our Annual Pro Sale. View Details »

Bringing Ruby to the JVM: Making the Impossible Possible

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. Bringing Ruby to the JVM
    Making the Impossible Possible

    View Slide

  2. 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

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. 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

    View Slide

  8. 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

    View Slide

  9. 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

    View Slide

  10. View Slide

  11. 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

    View Slide

  12. # 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

    View Slide

  13. # 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();

    View Slide

  14. # 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();

    View Slide

  15. 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

    View Slide

  16. View Slide

  17. 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

    View Slide

  18. 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?

    View Slide

  19. View Slide

  20. 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

    View Slide

  21. 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?

    View Slide

  22. 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 😁

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. JRuby IRB

    View Slide

  28. 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!

    View Slide

  29. 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

    View Slide

  30. 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...

    View Slide

  31. $ 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)


    ...

    View Slide

  32. 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

    View Slide

  33. 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 🤯

    View Slide

  34. 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

    View Slide

  35. 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!)

    View Slide

  36. 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

    View Slide

  37. 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)

    View Slide

  38. 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

    View Slide

  39. //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);
    }
    }
    }

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

  47. 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

    View Slide

  48. $ 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)
    😩

    View Slide

  49. 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!

    View Slide

  50. View Slide

  51. View Slide

  52. $ jruby -J--enable-preview fiber_test.rb
    2.324123
    0.880373
    0.6916289999999999
    0.73703
    0.655856
    🤩

    View Slide

  53. 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

    View Slide

  54. 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!

    View Slide

  55. 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

    View Slide

  56. [] 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 "."
    ...

    View Slide

  57. [] 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
    ...

    View Slide

  58. [] 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

    View Slide

  59. View Slide

  60. View Slide

  61. 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

    View Slide

  62. View Slide

  63. 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

    View Slide

  64. 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! ☺

    View Slide

  65. 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

    View Slide

  66. 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

    View Slide

  67. #1 Challenge...

    View Slide

  68. 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!

    View Slide

  69. 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!

    View Slide

  70. 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?

    View Slide

  71. Thank you!
    • Charles Oliver Nutter


    • @headius(@mastodon.social)


    [email protected]

    View Slide