Slide 1

Slide 1 text

Building JRuby Charles Oliver Nutter Thomas E. Enebo

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Stickers! • We have JRuby stickers! • Includes info about JRuby Support • Support JRuby, we'll support you!

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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!

Slide 7

Slide 7 text

Roadmap Update

Slide 8

Slide 8 text

Roadmap JRuby 9.4 • Ruby 3.1.4 compatible • 9.4.9.0 released on 11/04 • Support ~1 year after JRuby 10 released

Slide 9

Slide 9 text

Roadmap JRuby 10 • Ruby 3.4 • Anticipate release early in 2025 • Java 8 -> Java 21

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

JRuby Walkthrough

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

sloccount Totals grouped by language (dominant language first): java: 293119 (97.26%) ruby: 4640 (1.54%) yacc: 3617 (1.20%)

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

IR Compiler Core API Extensions Parser

Slide 18

Slide 18 text

Parser IR Compiler Core API Extensions

Slide 19

Slide 19 text

Parsing • JRuby 10 has two parsers • Legacy - LALR grammar (going away) • Prism - hand-written parser (external OSS project)

Slide 20

Slide 20 text

Parser IR Compiler Core API Extensions

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

IR Building example % echo “hello world” | ruby -ne ‘print if /lo/‘ test body match if call: print regex: /lo/

Slide 23

Slide 23 text

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: :=~)

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Extensions Parser IR Compiler Core API

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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); }

Slide 29

Slide 29 text

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 }

Slide 30

Slide 30 text

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", ...); }

Slide 31

Slide 31 text

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!

Slide 32

Slide 32 text

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;

Slide 33

Slide 33 text

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); }

Slide 34

Slide 34 text

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); }

Slide 35

Slide 35 text

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?

Slide 36

Slide 36 text

Parser IR Compiler Core API Extensions

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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]); }

Slide 39

Slide 39 text

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)); }

Slide 40

Slide 40 text

array.c VALUE rb_ary_entry(VALUE ary, long offset) { return rb_ary_entry_internal(ary, offset); }

Slide 41

Slide 41 text

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]; }

Slide 42

Slide 42 text

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); } }

Slide 43

Slide 43 text

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]; } ...

Slide 44

Slide 44 text

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); }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Parser IR Compiler Core API Extensions

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

JRuby IRB

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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);

Slide 54

Slide 54 text

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;

Slide 55

Slide 55 text

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());

Slide 56

Slide 56 text

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 value) { if (!value.isPresent()) return context.nil; return stringFor(context, value.get().getValue()); }

Slide 57

Slide 57 text

Extensions Parser IR Compiler Core API

Slide 58

Slide 58 text

API Problem De fi nition: Every public method in our code base is our API for native extensions : (

Slide 59

Slide 59 text

API • “public” is not really public • Endless deprecations (1554!!!!) • Cannot break native extensions

Slide 60

Slide 60 text

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+

Slide 61

Slide 61 text

org.jruby.api.* Create Construct new core objects Error Create exceptions Convert Ruby to Ruby Java <=> Ruby

Slide 62

Slide 62 text

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;

Slide 63

Slide 63 text

Blessed Methods @JRubyMethod @JRubyAPI public IRubyObject begin(ThreadContext context) { return begin; } @JRubyMethod @JRubyAPI public IRubyObject end(ThreadContext context) { return end; }

Slide 64

Slide 64 text

API • Generate an API guide • @MRI suggestion annotation • rb_ary_new => Create#newArray • Easy way to contribute

Slide 65

Slide 65 text

Contributing to JRuby

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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!

Slide 68

Slide 68 text

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!