Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Building JRuby: How We Implement Ruby on the JVM

headius
November 13, 2024

Building JRuby: How We Implement Ruby on the JVM

What does it take to build a language runtime on the JVM? This talk will show how we implement JRuby, from Java versions of core classes and native extensions to our parser, compiler, and JIT subsystems. You'll learn how to find and fix bugs, profile Ruby code, and integrate with the many libraries and features of the JVM.

Delivered at RubyConf 2024 in Chicago.

headius

November 13, 2024
Tweet

More Decks by headius

Other Decks in Programming

Transcript

  1. The JRuby Guys • Back together after 5 years! •

    Charles Oliver Nutter • [email protected], @headius • Co-founder Headius Enterprises • Thomas E. Enebo • [email protected] • @tom_enebo
  2. Pro Support for JRuby Users • JRuby Support by Headius

    Enterprises! • https://www.headius.com/ • Right-sized expert support for your applications • Ongoing support over email, chat, calls • Virtual team member for your JRuby project • Custom packages for migration to JRuby, upgrading, contract
  3. Stickers! • We have JRuby stickers! • Includes info about

    JRuby Support • Support JRuby, we'll support you!
  4. JRuby • Ruby for the JVM, and the JVM for

    Ruby! • JVM is a powerful platform for concurrency, high scale • Java ecosystem is enormous • Ruby dev experience on JRuby is pretty much the same • bundle your app, run with Puma, pro fi t!
  5. Roadmap JRuby 9.4 • Ruby 3.1.4 compatible • 9.4.9.0 released

    on 11/04 • Support ~1 year after JRuby 10 released
  6. JRuby 10 status • All language features are fi nished

    • Core APIs hold back release • 136 F/E in ruby/spec • 300+ in MRI test suite • Much less than it seems
  7. JRuby 10 status • All language features are fi nished

    • Core APIs hold back release • 136 F/E in ruby/spec • 300+ in MRI test suite • Much less than it seems
  8. Getting Started • Install a Java Development Kit (JDK) •

    Many vectors, system packages, just about all will work fi ne • git clone https://github.com/jruby/jruby.git • cd jruby • ./mvnw • Add JRUBY_DIR/bin to PATH
  9. Codebase Layout • /core - JRuby's core sources • /core/src/main/{java,ruby}

    - implementation code • /lib - JRuby's built executable, stdlib, gems • /test, /spec - Test and spec suites used to verify JRuby • /spec/ruby - Ruby spec suite • /test/mri - CRuby's test suite • /bin - JRuby's launcher and any installed gem executables
  10. Development Tools • Editor or IDE • We recommend IntelliJ

    IDEA • VS Code and other IDEs also work • JVM pro fi ling, monitoring tools • JConsole, VisualVM, JDK Mission Control
  11. Parsing • JRuby 10 has two parsers • Legacy -

    LALR grammar (going away) • Prism - hand-written parser (external OSS project)
  12. Internal Representation • Lists of instructions • Obvious: call, de

    fi ne_method, … • More abstract: branch_ne, jump, … • Operands • Fixnum, String, LocalVariable, TempVariable, etc… %v1 = call( fi x<1>, name: :+, fi x<2>)
  13. IR Building example % echo “hello world” | ruby -ne

    ‘print if /lo/‘ test body match if call: print regex: /lo/
  14. IR Builder public Operand buildMatch(Operand regexp) { Variable lastLine =

    addResultInstr(new GetGlobalVariableInstr(temp(), symbol("$_"))); return addResultInstr(new MatchInstr(scope, temp(), regexp, lastLine)); } %v_1 := get_global_var(:$_) %v_2 := match(/foo/, %v_1, name: :=~)
  15. IR Builder+ • Changed when…. • New language features come

    out • Performance enhancements • Lots of other pieces • Compiler passes/Interpreter • For people who want to learn about language implementations
  16. JRuby Compiler Pipeline Ruby (.rb) JIT Java Instructions (java bytecode)

    Ruby Instructions (IR) parse interpret interpreter interpret C1 compile native code better native code java bytecode interpreter execute C2 compile Java Virtual Machine JRuby Internals
  17. public class Float extends ImmutableLiteral { final public double value;

    public Float(double value) { super(); this.value = value; } @Override public Object createCacheObject(ThreadContext context) { return context.runtime.newFloat(value); } @Override public void visit(IRVisitor visitor) { visitor.Float(this); } org.jruby.ir.operands.Float
  18. org.jruby.ir.IRVisitor public void Fixnum(Fixnum fixnum) { error(fixnum); } public void

    FrozenString(FrozenString frozen) { error(frozen); } public void UnboxedFixnum(UnboxedFixnum fixnum) { error(fixnum); } public void Float(org.jruby.ir.operands.Float flote) { error(flote); } public void UnboxedFloat(org.jruby.ir.operands.UnboxedFloat flote) { error(flote); } public void GlobalVariable(GlobalVariable globalvariable) { error(globalvariable); }
  19. org.jruby.ir.targets.JVMVisitor @Override public void Float(org.jruby.ir.operands.Float flote) { jvmMethod() // current

    compile method .getValueCompiler() // compiler for values .pushFloat(flote.getValue()); // emit code for float }
  20. NormalValueCompiler and IndyValueCompiler public void pushFloat(final double d) { cacheValuePermanentlyLoadContext("float",

    RubyFloat.class, keyFor("float", Double.doubleToLongBits(d)), () -> { pushRuntime(); compiler.adapter.ldc(d); compiler.adapter.invokevirtual(p(Ruby.class), "newFloat", sig(RubyFloat.class, double.class)); }); } public void pushFloat(double d) { compiler.loadContext(); compiler.adapter.invokedynamic("flote", ...); }
  21. InvokeDynamic • JVM bytecode for dynamic binding • Methods or

    values • Allows us to "teach" JVM how to optimize Ruby • Not enabled on JRuby by default due to warmup time • Hopefully default in JRuby 10!
  22. Normal and Indy Bytecode ALOAD 0 INVOKEDYNAMIC flote(Lorg/jruby/runtime/ThreadContext;)Lorg/jruby/runtime/builtin/IRubyObject; [ //

    handle kind 0x6 : INVOKESTATIC org/jruby/ir/targets/indy/FloatObjectSite.bootstrap(...)Ljava/lang/invoke/CallSite; // arguments: 1.0D ] GETSTATIC DashE.float0 : Lorg/jruby/RubyFloat; DUP IFNONNULL L0 POP ALOAD 0 GETFIELD org/jruby/runtime/ThreadContext.runtime : Lorg/jruby/Ruby; LDC 1.0D INVOKEVIRTUAL org/jruby/Ruby.newFloat (D)Lorg/jruby/RubyFloat; DUP PUTSTATIC DashE.float0 : Lorg/jruby/RubyFloat;
  23. org.jruby.ir.targets.indy.FloatObjectSite public class FloatObjectSite extends LazyObjectSite { private final double

    value; public FloatObjectSite(MethodType type, double value) { super(type); this.value = value; } public IRubyObject construct(ThreadContext context) { return RubyFloat.newFloat(context.runtime, value); }
  24. public class Float extends ImmutableLiteral { final public double value;

    public Float(double value) { super(); this.value = value; } @Override public Object createCacheObject(ThreadContext context) { return context.runtime.newFloat(value); } @Override public void visit(IRVisitor visitor) { visitor.Float(this); } public class FloatObjectSite extends LazyObjectSite { private final double value; public FloatObjectSite(MethodType type, double value) { super(type); this.value = value; } public IRubyObject construct(ThreadContext context) { return RubyFloat.newFloat(context.runtime, value); }
  25. Review • Parser creates the abstract syntax tree • IR

    compiler turns AST into instructions (intermediate representation) • Interpreter executes IR instructions • JIT compiler turns hot methods into JVM bytecode • What about core classes like String, Array, Hash?
  26. Core Classes • String, Array, Hash => org.jruby.RubyString, RubyArray, RubyHash

    • Compatibility follows CRuby, so code is very similar • Sometimes almost a line-by-line port • JRuby uses Java method overloading, static types • Helps clarify the path calls will take
  27. array.c rb_define_method(rb_cArray, "[]", rb_ary_aref, -1); rb_define_method(rb_cArray, "slice", rb_ary_aref, -1); VALUE

    rb_ary_aref(int argc, const VALUE *argv, VALUE ary) { rb_check_arity(argc, 1, 2); if (argc == 2) { return rb_ary_aref2(ary, argv[0], argv[1]); } return rb_ary_aref1(ary, argv[0]); }
  28. array.c VALUE rb_ary_aref1(VALUE ary, VALUE arg) { long beg, len,

    step; /* special case - speeding up */ if (FIXNUM_P(arg)) { return rb_ary_entry(ary, FIX2LONG(arg)); } /* check if idx is Range or ArithmeticSequence */ switch (rb_arithmetic_sequence_beg_len_step(arg, &beg, &len, &step, RARRAY_LEN(ary), 0)) { case Qfalse: break; case Qnil: return Qnil; default: return rb_ary_subseq_step(ary, beg, len, step); } return rb_ary_entry(ary, NUM2LONG(arg)); }
  29. array.c static inline VALUE rb_ary_entry_internal(VALUE ary, long offset) { long

    len = RARRAY_LEN(ary); const VALUE *ptr = RARRAY_CONST_PTR(ary); if (len == 0) return Qnil; if (offset < 0) { offset += len; if (offset < 0) return Qnil; } else if (len <= offset) { return Qnil; } return ptr[offset]; }
  30. org.jruby.RubyArray @JRubyMethod(name = {"[]", "slice"}) public IRubyObject aref(ThreadContext context, IRubyObject

    arg0) { if (arg0 instanceof RubyArithmeticSequence) { return subseq_step(context, (RubyArithmeticSequence) arg0); } else { return arg0 instanceof RubyFixnum ? entry(((RubyFixnum) arg0).value) : arefCommon(context, arg0); } }
  31. org.jruby.RubyArray public final IRubyObject entry(long offset) { return (offset <

    0 ) ? elt(offset + realLength) : elt(offset); } public T eltInternal(int offset) { return (T) values[begin + offset]; } ...
  32. org.jruby.specialized.RubyArrayOneObject @Override public final IRubyObject eltInternal(int index) { if (!packed())

    return super.eltInternal(index); else if (index == 0) return value; throw new ArrayIndexOutOfBoundsException(index); }
  33. Finding Core Methods • Most classes are in org.jruby package

    (core/src/main/java/org/jruby) • Search for "@JRubyMethod" • Search for CRuby equivalent name like rb_ary_new • JRuby docs/tools to help translate to our API
  34. Native Extensions • No C API for JRuby extensions •

    Java/JRuby API should be mostly equivalent • Three options for porting: • Use pure-Ruby version... may be good enough! • Wrap C or JVM library with Ruby code (FFI or Java integration) • Port extension to JRuby API
  35. Java Integration • JVM libraries can be called from Ruby

    • Eliminates the need for extensions... just write Ruby! • JRuby ecosystem provides packaging tools • ruby-maven for building mixed projects • jar-dependencies for Maven libraries as gem deps
  36. Calling JVM Libraries • require the library's .jar fi le

    to load the classes in • java_import speci fi c class name, e.g. java.lang.ArrayList • Call methods as camelCase or underscore_case • getFoo, setFoo aliased like "def foo" and "def foo=" • See wiki.jruby.org for additional help
  37. Extension: Psych • Psych: YAML support for Ruby • CRuby

    uses libyaml, JRuby uses SnakeYAML • Code is very similar • Started out as a line-by-line port
  38. Psych Parser • Feed incoming YAML to the YAML library

    • Iterate over parser events • Check for errors • Convert to Ruby objects • Invoke Ruby parts of Psych
  39. psych_parser.c while(!done) { VALUE event_args[5]; VALUE start_line, start_column, end_line, end_column;

    if(parser->error || !yaml_parser_parse(parser, &event)) { VALUE exception; exception = make_exception(parser, path); yaml_parser_delete(parser); yaml_parser_initialize(parser); rb_exc_raise(exception); } start_line = SIZET2NUM(event.start_mark.line); start_column = SIZET2NUM(event.start_mark.column); end_line = SIZET2NUM(event.end_mark.line); end_column = SIZET2NUM(event.end_mark.column);
  40. psych_parser.c case YAML_ALIAS_EVENT: { VALUE args[2]; VALUE alias = Qnil;

    if(event.data.alias.anchor) { alias = rb_str_new2((const char *)event.data.alias.anchor); PSYCH_TRANSCODE(alias, encoding, internal_enc); } args[0] = handler; args[1] = alias; rb_protect(protected_alias, (VALUE)args, &state); } break;
  41. PsychParser.java while (parser.hasNext()) { event = parser.next(); Mark start =

    event.getStartMark().orElseThrow(RuntimeException::new); IRubyObject start_line = runtime.newFixnum(start.getLine()); IRubyObject start_column = runtime.newFixnum(start.getColumn()); Mark end = event.getEndMark().orElseThrow(RuntimeException::new); IRubyObject end_line = runtime.newFixnum(end.getLine()); IRubyObject end_column = runtime.newFixnum(end.getColumn());
  42. PsychParser.java case Alias: IRubyObject alias = stringOrNilForAnchor(context, ((AliasEvent) event).getAnchor()); sites.alias.call(context,

    this, handler, alias); break; private IRubyObject stringOrNilForAnchor(ThreadContext context, Optional<Anchor> value) { if (!value.isPresent()) return context.nil; return stringFor(context, value.get().getValue()); }
  43. API Problem De fi nition: Every public method in our

    code base is our API for native extensions : (
  44. API • “public” is not really public • Endless deprecations

    (1554!!!!) • Cannot break native extensions
  45. API Plan • De fi ne API enough for known

    native extensions to update (ex. nokogiri) • Support JRuby 9.4 and 10 • Encourage switch to API and version lock new gem to 9.4.?.0+
  46. RubyComparable#clamp @JRubyMethod(name = "clamp") public static IRubyObject clamp(ThreadContext context, C

    IRubyObject recv, IRubyObject arg) { var range = castAsRange(context, arg); var min = range.begin(context); var max = range.end(context); if (!max.isNil() && range.isExcludeEnd()) { throw argumentError(context, "cannot clamp with an exclusive range"); } return clamp(context, recv, min, max); } Cast or TypeError Make an ArgumentError Blessed API calls import static org.jruby.api.Convert.asBoolean; import static org.jruby.api.Convert.castAsRange; import static org.jruby.api.Error.argumentError;
  47. Blessed Methods @JRubyMethod @JRubyAPI public IRubyObject begin(ThreadContext context) { return

    begin; } @JRubyMethod @JRubyAPI public IRubyObject end(ThreadContext context) { return end; }
  48. API • Generate an API guide • @MRI suggestion annotation

    • rb_ary_new => Create#newArray • Easy way to contribute
  49. We Need Your Help! • Test your apps and libraries

    and report bugs • Help fi x bugs you fi nd or others have reported • Write in Ruby or Java, we'll optimize if needed later • Help add JRuby support to popular libraries • Port extension, wrap existing library, or pure Ruby
  50. Join the Community • JRuby contributors chat on JRuby channel

    on Matrix • https://matrix.to/#/#jruby:matrix.org • JRuby mailing list on Google Groups • https://groups.google.com/u/0/a/ruby-lang.org/g/jruby • Contact us directly!
  51. Thank You! • Charles Oliver Nutter, [email protected], @headius • Thomas

    E. Enebo, [email protected], @tom_enebo • https://www.jruby.org/ • https://github.com/jruby/jruby • https://www.headius.com/ • Hackfest tomorrow! Bring your project or library and hack with us!