New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Java-constructable Concrete Ruby Classes #6422
Conversation
I'm now moving the concrete extension code into the reification code. So far I have the following pattern implement: public class NormalReify2 extends RubyObject implements Reified { // this is unchanged, and we are a IRubyObject
private static Ruby ruby;
private static RubyClass rubyClass;
public static void clinit(Ruby var0, RubyClass var1) {
ruby = var0;
rubyClass = var1;
}
public NormalReify2(Ruby var1, RubyClass var2) { //initialized via the RubyAlllocator, which is a change from before to allow java/init calls to not collide
super(var1, var2);
// ruby called us, we will be initialized later
}
public NormalReify2() { //java-called
super(ruby, rubyClass);
this.callMethod("initialize"); // initialize as java called us
}
public int mymethod(String var1) {
Ruby var2 = ruby;
return ((Number)this.callMethod("mymethod", new IRubyObject[]{JavaUtil.convertJavaToUsableRubyObject(var2, var1)}).toJava(Integer.TYPE)).intValue();
} Concrete Extension: public class RubyTest extends AbstractTest implements Reified { // Unsure of which interfaces should be implemented
private static Ruby ruby;
private static RubyClass rubyClass;
private RubyBasicObject rubyObject; // in theory should be IRubyObject, but not worrying about the threadcontext is nice :-)
public static void clinit(Ruby var0, RubyClass var1) { // to be replaced by static{} block
ruby = var0;
rubyClass = var1;
}
public RubyTest(Ruby var1, RubyClass var2) { // ruby facing ctor
if (this.rubyObject == null) {
this.rubyObject = new ConcreteJavaProxy(var1, var2, this); //note this pulls allocation out of an Allocator object, is that fine? what references, doc should there be?
}
// no init, as ruby will call it later. this is part of the alloc
}
public RubyTest() { // java-facing ctor
if (this.rubyObject == null) { // eh, lazy codegen that should be cleaned up
(this.rubyObject = new ConcreteJavaProxy(ruby, rubyClass, this)).callMethod("initialize"); // init here, as java called us
}
}
public List myAbstractFunc() { // an overridden function
if (this.rubyObject == null) { // I think this could use some optimization, but this is here in case the parent calls methods in our super
this.rubyObject = new ConcreteJavaProxy(ruby, rubyClass, this);
}
Ruby var1 = ruby;
return (List)this.rubyObject.callMethod("myAbstractFunc", IRubyObject.NULL_ARRAY).toJava(List.class); // Should the method lookup be there every time?
}
public static IRubyObject __allocate__(Ruby var0, RubyClass var1) { //uses StaticRubyAllocator
return (new RubyTest(var0, var1)).rubyObject; // Note: rubyObject has a ref to `this`, so no GC worries
} Ruby API: class MyClass
configure_java_class methods: :explicit/:all, call_init: true/false, java_constructable: true/false
# sets JavaClassConfiguration
end |
…w configurable from ruby-land
Of note, given this java class: class MyClass {
MyClass(String s){
doAbstract();
}
protected abstract doAbstract();
} and this JRuby class: class MyRuby < MyClass
def initialize(s)
puts "in init"
end
def doAbstract
puts "in abstract"
end
end the resulting output, with my current code is: in abstract
in init which is weird. Also, super() is a no-op in ctors on concrete classes now, which also strikes me as weird |
I have three sets of questions remaining: Architecture, Design, and Implementation, in order of importance ArchitectureJava Init Sequence == Ruby Init Sequence?As of the way I've implemented it right now, a ruby init sequence is ReifiedClass.<init>(*args)
super(*args)
this.rubyObject = new ConcreteJavaProxy(this)
rubyObject#initialize(*args)
# super() calls are *SILENTLY* no-ops Alloc no longer exists [1] I debated making super NOT a no-op, but that would involve a lot of code gen + super-splitting that I wanted to ask about before I went down that rabbit hole. I also didn't like only making it a no-op in java init sequences, as it seemed too context sensitive and prone to confusion. Note this is a behavioral change, and I don't like it. In theory, we could separate args == 0, as super is a no-op there (probably[2]), but I feel like it could lead to a confusing mental model of args == 0 being different from args > 0. 1: I have no idea if this is enforced/rumps are still around Super-splittingThis is an idea I had, but am unsure of how to go about this or if it's an ok idea. The general format of generated code will turn into: ReifiedClass.<init>(*args)
this.rubyObject = new ConcreteJavaProxy()
superargs, continuation = rubyObject#initialize_with_split_super(*args)
switch(guessSignatureFromArgs(*superargs))
when V -> super()
when LString -> super(*convert(superargs))
when LString;Z -> super(*convert(superargs))
etc...
end
rubyObject.setJavaObject(this)
continuation.call There are 3 ways I though of implementing this:
I think #3 is out due to overhead, though it would work today and is the most minimally invasive. Howerver def initialize(*args)
variable = OtherClass.call_static(*args)
super(variable, keep_me = args[0] + 5)
self.to_java.java_method(keep_me, variable) # values retained
end
def initialize_transformed(*args) # No need to change the return type, as initialize doesn't return values
variable = OtherClass.call_static(*args)
return [ [variable, keep_me = args[0] + 5], lambda {
self.to_java.java_method(keep_me, variable) # values retained
}
end New Method names => new generated classThe old concrete extension code (now deleted in this PR) used to regenerate when a new method was added. The reify code doesn't appear to do this. Why the difference? DesignReify optionsRight now things are configured as such: class MyClass
configure_java_class methods: :explicit/:all, call_init: true/false, java_constructable: true/false
end I was also thinking of supporting the same options (to override the ones set there) in Where should the options be configured? Also, are those reasonable options? (for all: https://github.com/jruby/jruby/pull/6422/files#diff-fa6783cb1e5bbf966f341330a90265a3aa04682625c103500fe61d5e8602ef96 and for what they do (this area of the code): https://github.com/jruby/jruby/pull/6422/files#diff-4823dd97065ce1b80e235b191b03497a85a35a0acd1e8089e09eef1365b699ecR1564-R2165) Implementation
|
…rifying AST rewriting to de-super()
Added super-splitting proposal code-gen, and some tests on how option 1 holds up. Answer: super sketchy with it being enforced to be just like java for that method to work (no super in if's, etc). I though about maybe an alternative to fibers, but that would require us to implement a continuation in super, precluding java calls on the stack frame between and the super call. Super-splitter constructors will be rather large (decompiled from bytecode): public TestClass(int var1, String var2) {
Ruby var3 = ruby;
IRubyObject[] var7;
this.$rubyInitArgs = var7 = new IRubyObject[]{JavaUtil.convertJavaToRuby(var3, var1), JavaUtil.convertJavaToUsableRubyObject(var3, var2)};
// returns [[super args], [lambda]]
RubyArray var10001 = (this.$rubyObject = new ConcreteJavaProxy(var3, rubyClass)).callMethod("j_initialize", var7).convertToArray();
IRubyObject var6 = var10001.entry(1);
IRubyObject var8 = var10001.entry(0);
RubyArray var5;
switch(JCreateMethod.forTypes($rubyCtors, var8)) {
case -1:
super(var1, var2); // super (no args) called
break;
case 0: // disambiguate the ctors for this class for super (with args)
var5 = var8.convertToArray();
super((String)var5.entry(0).toJava(String.class), (Boolean)var5.entry(1).toJava(Boolean.TYPE));
break;
case 1:
var5 = var8.convertToArray();
super(((Number)var5.entry(0).toJava(Integer.TYPE)).intValue(), (String)var5.entry(1).toJava(String.class));
break;
// case n, as appropriate
default:
throw new IllegalStateException("No available superconstructors match that type signature");
}
this.$rubyInitArgs = null;
((ConcreteJavaProxy)this.$rubyObject).setObject(this);
var6.callMethod(var3.getCurrentContext(), "call"); // complete the lambda/initialize
} |
A summary of matrix conversation with @headius:
Of note this:
|
… fallback. Also some misc cleanup
Things to do before this is finished, roughly in order:
|
From todays chat:
|
As in other places we call through Java code, we should allow all exceptions to proceed unwrapped. This should restore the Ruby trace for your first example. We used to post-process Java exceptions coming from Java integration calls but it required us to be catching every one and rewriting it, which then broke Java consumers of those exceptions further up the stack. |
Here are the Windows GHA failures as of this moment:
I will try to confirm these should be passing. Not sure why Windows would have any differing behavior here. |
Running JI specs have three failures for me locally:
This is MacOS, Java 11. |
…, fix negative super looping
Fixes vararg dispatch
What remains to be done?
|
Some more cleanup required, but jruby/jruby#6422 has been merged to close jruby/jruby#449 which means that finally, we can use the native FXMLLoader support by generating classes at runtime based on the fxml files that the loader knows how to deal with, and we can finally drop all the garbage to work around this. Yay!
* master: add --dev stage, fix JRUBY_OPTS Fix method visibility for define method doclint stinks getsockopt crash with a --dev flag Minor style tweaks after landing of jruby#6422 Update for next development cycle Update for 9.2.18.0 Update for newest jruby-launcher
Work on #449, duplicate #5270, and related #2369. See #449 for latest discussion.
Missing: Working code, Tests, and a good design.
Right now this is very hacky.
Notes to watch out for:
https://github.com/jruby/jruby/wiki/FAQs#when-i-implement-a-java-interface-and-provide-my-own-initialize-method-i-cant-construct-it-anymore
interface methods must be in :methods (vs all)