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

Concrete JRuby Implementations of Abstract Java Classes Can't be Constructed With Reflection #5270

Open
ScottHaney opened this Issue Aug 2, 2018 · 8 comments

Comments

Projects
None yet
3 participants
@ScottHaney

ScottHaney commented Aug 2, 2018

Environment

  • JRuby: jruby 9.2.0.0 (2.5.0) 2018-05-24 81156a8 Java HotSpot(TM) 64-Bit Server VM 25.181-b13 on 1.8.0_181-b13 +jit [linux-x86_64]
  • Operating system and platform: Linux shoes 4.15.0-23-generic #25-Ubuntu SMP Wed May 23 18:02:16 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Expected Behavior

Creating a JRuby class from an abstract Java class should have a constructor that can be called through reflection. This means that the following code should work JRubyConcreteClass.java_class.constructor.new_instance when JRubyConcreteClass derives from an abstract Java class.

Actual Behavior

It complains that it can't instantiate the concrete class because it can't make an instance of the abstract base class. The error says "TypeError: can't make instance of AbstractClass". I have created a repository that reproduces the issue. Also overriding abstract java methods in JRuby doesn't seem to work either which is something that would be helpful to have.

Potential Work Around for now

As a work around I am going to create concrete derived classes that directly implement the abstract methods and then add new setter type methods that JRuby code can then call to set data on. The data populated through these setters will then be used inside of the overridden abstract methods to get the desired behavior. This is a hack, but I think it will work for now.

I am also open to helping out with some development depending on what might be involved in fixing improving the behavior of implementations of abstract Java classes.

Thank you for your time and help!

@headius

This comment has been minimized.

Show comment
Hide comment
@headius

headius Aug 2, 2018

Member

Ok the usual way we improve these features is by getting some new specs in place. Class extension has needed an overhaul for years but the specs are very weak. Can you folks collab to figure out both what we do now and what we want to do? The new impl is not terribly hard if we have good specs and can throw away the current impl.

Member

headius commented Aug 2, 2018

Ok the usual way we improve these features is by getting some new specs in place. Class extension has needed an overhaul for years but the specs are very weak. Can you folks collab to figure out both what we do now and what we want to do? The new impl is not terribly hard if we have good specs and can throw away the current impl.

@monkstone

This comment has been minimized.

Show comment
Hide comment
@monkstone

monkstone Aug 2, 2018

Contributor

OK just to clarify works for me as expected, but may'be that's my peculiar/particular case, I'd be happy to collaborate on a spec. My Simple test case also works as expected:-

package monkstone.reflect;

public abstract class SimpleAbstract {
    public abstract void javaMethod();    
}
require 'java'
require '/home/tux/reflect/reflect/target/reflect-1.0-SNAPSHOT.jar'
class SimpleRubyClass < Java::MonkstoneReflect::SimpleAbstract
  def java_method
    puts 'java_method from ruby'
  end
end

reflect = SimpleRubyClass.new

reflect.java_method

Now definitely that's just too simple for a test case, but put together with my real world example that does involve java reflection I think that current impl does work, but perhaps not as ScottHanley wants.

Contributor

monkstone commented Aug 2, 2018

OK just to clarify works for me as expected, but may'be that's my peculiar/particular case, I'd be happy to collaborate on a spec. My Simple test case also works as expected:-

package monkstone.reflect;

public abstract class SimpleAbstract {
    public abstract void javaMethod();    
}
require 'java'
require '/home/tux/reflect/reflect/target/reflect-1.0-SNAPSHOT.jar'
class SimpleRubyClass < Java::MonkstoneReflect::SimpleAbstract
  def java_method
    puts 'java_method from ruby'
  end
end

reflect = SimpleRubyClass.new

reflect.java_method

Now definitely that's just too simple for a test case, but put together with my real world example that does involve java reflection I think that current impl does work, but perhaps not as ScottHanley wants.

@ScottHaney

This comment has been minimized.

Show comment
Hide comment
@ScottHaney

ScottHaney Aug 3, 2018

@monkstone - If I am looking over the examples that you provided it seems like you are focusing more on overriding an abstract method from a Java class in a Jruby class. That situation seems to work fine for me in testing as well. I am concerned with the .constructor.new_instance call chain specifically.

The issue does look like a bug, because if I call puts JrubyConcreteClass.java_class.constructor from the example repository that I linked in my original post I get back "public com.almondbranchsoftware.jruby_abstract_class.SimpleAbstractClass()". So it looks like calling .constructor for the concrete class is giving me back the constructor from its parent class rather than its own constructor for some reason. The error message that I am seeing about not being able to create an abstract class looks like it is because of getting the abstract parents constructor by mistake.

@headius - I don't mind putting some effort towards it. From looking at the code it seems like the current spec is jruby/spec/java_integration/types/extension_spec.rb. Are there more specs that would need to be updated as well or just this one?

ScottHaney commented Aug 3, 2018

@monkstone - If I am looking over the examples that you provided it seems like you are focusing more on overriding an abstract method from a Java class in a Jruby class. That situation seems to work fine for me in testing as well. I am concerned with the .constructor.new_instance call chain specifically.

The issue does look like a bug, because if I call puts JrubyConcreteClass.java_class.constructor from the example repository that I linked in my original post I get back "public com.almondbranchsoftware.jruby_abstract_class.SimpleAbstractClass()". So it looks like calling .constructor for the concrete class is giving me back the constructor from its parent class rather than its own constructor for some reason. The error message that I am seeing about not being able to create an abstract class looks like it is because of getting the abstract parents constructor by mistake.

@headius - I don't mind putting some effort towards it. From looking at the code it seems like the current spec is jruby/spec/java_integration/types/extension_spec.rb. Are there more specs that would need to be updated as well or just this one?

@monkstone

This comment has been minimized.

Show comment
Hide comment
@monkstone

monkstone Aug 3, 2018

Contributor

@ScottHaney So long as my use case keeps working I'm happy. I create a custom abstract class, with the sole purpose to to gain access (in ruby) to java methods (on processing PApplet class) that have a reflective access via java. I only posted just in case you had a similar problem. As far as I knew the use case for the reflective constructor in jruby is to cope with different java class constructor signatures.

Contributor

monkstone commented Aug 3, 2018

@ScottHaney So long as my use case keeps working I'm happy. I create a custom abstract class, with the sole purpose to to gain access (in ruby) to java methods (on processing PApplet class) that have a reflective access via java. I only posted just in case you had a similar problem. As far as I knew the use case for the reflective constructor in jruby is to cope with different java class constructor signatures.

@headius

This comment has been minimized.

Show comment
Hide comment
@headius

headius Aug 16, 2018

Member

So I'm back to work after some vacation...and I think what you are asking for here is that the Java-facing concrete class have a no-argument constructor, yes?

It's not impossible to do (and maybe not difficult) but this is the sort of thing we want to get right once. Currently the concrete class created does not have an easily-usable constructor. This logic was written many years ago and we have resisted rewriting it due to its complexity.

I believe the current logic would allow you to construct the object via a more complex constructor signature, perhaps one that takes the JRuby "Ruby" runtime object and the RubyClass object associated with the new class, but that may not be convenient for what you are doing...

Member

headius commented Aug 16, 2018

So I'm back to work after some vacation...and I think what you are asking for here is that the Java-facing concrete class have a no-argument constructor, yes?

It's not impossible to do (and maybe not difficult) but this is the sort of thing we want to get right once. Currently the concrete class created does not have an easily-usable constructor. This logic was written many years ago and we have resisted rewriting it due to its complexity.

I believe the current logic would allow you to construct the object via a more complex constructor signature, perhaps one that takes the JRuby "Ruby" runtime object and the RubyClass object associated with the new class, but that may not be convenient for what you are doing...

@ScottHaney

This comment has been minimized.

Show comment
Hide comment
@ScottHaney

ScottHaney Aug 17, 2018

@headius I believe that you have it right. The java facing class of the class that I create in ruby code needs to have a default constructor in order for the third party java code that I am interacting with to work.

To make sure that I understand the scope right the spec that would be affected by the changes would be jruby/spec/java_integration/types/extension_spec.rb correct? I am new to the code base and haven't looked through it a lot, but from glancing, the code files that look like they have code related to this issue are:

core/src/main/java/org/jruby/RubyClass.java
core/src/main/java/org/jruby/javasupport/JavaClass.java
core/src/main/java/org/jruby/javasupport/Java.java

Is this the right area? Also if you could give more specific details on the scope of the changes that you are looking for I could get a better idea of what to include in the updated spec.

As far as the workaround you mentioned at the end of your last post, unfortunately the creation of the object is deep inside a third party Java library. So I can't change it to use the workaround that you suggested unfortunately.

ScottHaney commented Aug 17, 2018

@headius I believe that you have it right. The java facing class of the class that I create in ruby code needs to have a default constructor in order for the third party java code that I am interacting with to work.

To make sure that I understand the scope right the spec that would be affected by the changes would be jruby/spec/java_integration/types/extension_spec.rb correct? I am new to the code base and haven't looked through it a lot, but from glancing, the code files that look like they have code related to this issue are:

core/src/main/java/org/jruby/RubyClass.java
core/src/main/java/org/jruby/javasupport/JavaClass.java
core/src/main/java/org/jruby/javasupport/Java.java

Is this the right area? Also if you could give more specific details on the scope of the changes that you are looking for I could get a better idea of what to include in the updated spec.

As far as the workaround you mentioned at the end of your last post, unfortunately the creation of the object is deep inside a third party Java library. So I can't change it to use the workaround that you suggested unfortunately.

@ScottHaney

This comment has been minimized.

Show comment
Hide comment
@ScottHaney

ScottHaney Sep 1, 2018

@headius I spent some time looking over the source code and I think I have a better understanding of your recommendation to use the JRuby "Ruby" runtime object.

I tried the following:

runtime = org.jruby.Ruby.getGlobalRuntime
names = java.util.HashSet.new
names.add('methodName1')

proxy = org.jruby.javasupport.proxy.JavaProxyClass.newProxyClass(runtime, org.jruby.javasupport.JavaClass.for_name(ruby, 'org.companyname.classname'), nil, names)

This creates a java object that has the methods that I need on it and also inherits from the java class that I want it to, however, the proxy constructor has an extra argument of type org.jruby.javasupport.proxy.JavaProxyInvocationHandler. Since I need the constructor to have zero arguments this extra parameter makes the proxy unusable, even though it is close to usable.

I also found the become_java! method, but this seems to not create anything for ruby classes that inherit from java classes. If I run it using a pure ruby class it works, but inheriting from a java class causes it to fail. I am thinking that the java proxy is the way to go, but it doesn't seem like there is a way to get rid of that extra constructor parameter.

Alternatively I could add another constructor if it were possible, I did see an extend_proxy method but wasn't sure how to call it on the proxy class. Would there be a way to add a no argument constructor to the proxy class using extend_proxy somehow?

ScottHaney commented Sep 1, 2018

@headius I spent some time looking over the source code and I think I have a better understanding of your recommendation to use the JRuby "Ruby" runtime object.

I tried the following:

runtime = org.jruby.Ruby.getGlobalRuntime
names = java.util.HashSet.new
names.add('methodName1')

proxy = org.jruby.javasupport.proxy.JavaProxyClass.newProxyClass(runtime, org.jruby.javasupport.JavaClass.for_name(ruby, 'org.companyname.classname'), nil, names)

This creates a java object that has the methods that I need on it and also inherits from the java class that I want it to, however, the proxy constructor has an extra argument of type org.jruby.javasupport.proxy.JavaProxyInvocationHandler. Since I need the constructor to have zero arguments this extra parameter makes the proxy unusable, even though it is close to usable.

I also found the become_java! method, but this seems to not create anything for ruby classes that inherit from java classes. If I run it using a pure ruby class it works, but inheriting from a java class causes it to fail. I am thinking that the java proxy is the way to go, but it doesn't seem like there is a way to get rid of that extra constructor parameter.

Alternatively I could add another constructor if it were possible, I did see an extend_proxy method but wasn't sure how to call it on the proxy class. Would there be a way to add a no argument constructor to the proxy class using extend_proxy somehow?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment