Ruby constructor without explicit super() call in reopened Java class returns broken object with uninitialized Java proxy in JRuby 9.3
While investigating https://github.com/jruby/jruby/issues/6966, I found what may be either a separate issue or a different symptom of the same underlying bug. It seems that adding an initialize method that does not explicitly call super to a reopened Java class can result in the construction of a broken object with an uninitialized Java proxy.
Specifically, the following spec (with associated Java class below) fails under JRuby 9.3.2.0 but passes under 9.2.19.0:
describe "JRuby Java inheritance with JRuby #{JRUBY_VERSION}" do
class Java::Test::JRubyInheritanceTestTwo
def initialize(*args)
raise "expected one argument, received #{args.count}" unless args.count == 1
# super(*args)
end
end
it 'calls Java constructor implicitly from Ruby constructor in reopened class' do
obj = Java::Test::JRubyInheritanceTestTwo.new('foobar')
expect { obj.to_java }.not_to raise_error
expect(obj.message).to eq('foobar')
end
end
package test;
public class JRubyInheritanceTestTwo {
private final String message;
public String getMessage() {
return message;
}
public JRubyInheritanceTestTwo(String s) {
message = s;
}
public JRubyInheritanceTestTwo() {
this("no args");
}
}
Output under JRuby 9.2.19.0:
JRuby Java inheritance with JRuby 9.2.19.0
calls Java constructor implicitly from Ruby constructor in reopened class
Finished in 0.02714 seconds (files took 0.37393 seconds to load)
1 example, 0 failures
Output under JRuby 9.3.2.0:
JRuby Java inheritance with JRuby 9.3.2.0
calls Java constructor implicitly from Ruby constructor in reopened class (FAILED - 1)
Failures:
1) JRuby Java inheritance with JRuby 9.3.2.0 calls Java constructor implicitly from Ruby constructor in reopened class
Failure/Error: expect { obj.to_java }.not_to raise_error
expected no Exception, got #<RuntimeError: Java proxy not initialized. Did you call super() yet?> with backtrace:
# ./spec/internals/jruby_inheritance2_spec.rb:11:in `block in <main>'
# ./spec/internals/jruby_inheritance2_spec.rb:11:in `block in <main>'
# ./spec/internals/jruby_inheritance2_spec.rb:11:in `block in <main>'
Finished in 0.061 seconds (files took 0.34418 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/internals/jruby_inheritance2_spec.rb:9 # JRuby Java inheritance with JRuby 9.3.2.0 calls Java constructor implicitly from Ruby constructor in reopened class
As a work-around, uncommenting the super(*args) line in the Ruby constructor makes the spec pass also on JRuby 9.3.
Linking this to #6968 since both are from the same feature (6968 may be a different issue related to the same feature)
A note for when this is looked at. The original feature for initialize and real subtypes was that nothing before the super can reference 'this' in a meaningful way. This is because we have to have an actual object defined and since we are really defining using Java construction one does not exist yet. I believe the raise here is depending on 'this' being available and it is not.
This makes me wonder how we solve this?
@enebo Maybe? But do note that the raise in the spec above is actually never executed — I just included it as an example of why one might want to override a Java constructor in Ruby like this. (In our actual codebase we're applying this hack to a third-party library class whose zero-arg constructor we don't want anyone to use in our code.) Also, the super call that makes it work is after the raise. (It does also work with super before raise, but then calling the constructor with no arguments will needlessly construct the Java object just to throw it away.)
@ikaronen-relex ok that is a little unexpected. It may become obvious once we start working on this one.
The constructing then raising is a bit academic since this should work but if you are raising constructing the Java object is probably a drop in the bucket (unless it is making some massive graph of objects or connecting to a data source). Exceptions are expensive.
We still need to fix this issue but here is an alternative way for you do this without the statement:
def foo(a, *b)
all_args = a, *b
p all_args
end
begin
foo()
rescue
p $!
end
foo(1)
foo(1, 2)
Oh I should point out the change would be:
def initialize(_, *)
super
end
It will give a nearly identical error message without adding any code.