Skip to content
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

Add support for covariant return types #1575

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

rPraml
Copy link
Contributor

@rPraml rPraml commented Aug 22, 2024

A few days ago, we discovered also a strange behaviour in the js<->java liveconnect code.

Normally, rhino will detect getters/setters in java classes and allows you, to access them as properties.

e.g. if you have a class like

public class MyBean {
  public String getValue() {return null;}
  public void setValue(String value) {...}
} 

you can read/write the value with myBean.value = "foo" in javascript. This works if there are matching getters/setters found. (If there is an incompatible setter like setValue(Integer value) it will no longer work, as Integer is not an instance of String.
BTW: It will work if there is a setValue(Object value), as this setter accepts also strings.

If you now inherit and narrow the return type, you will get multiple methods with covariant return types, e.g.

public interface ValueHolder {
   Object getValue();
}
public class MyBean implements ValueHolder {...}

will result in the following bytecode:

public getValue()Ljava/lang/String;
public synthetic bridge getValue()Ljava/lang/Object;

Note: You get the same method with same signature, but different return types. This is not allowed by the JLS, but at VM/bytecode level and it is explicitly mentioned in https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getMethods()

What happens in Rhino now:

  • Rhino takes the FIRST method of each signature (signature = method name + parameters)
  • So if there are covariant methods, it will either take getValue()Ljava/lang/String or getValue()Ljava/lang/Object
  • After that, it tries to find a compatible setter.
    -- If getValue()Ljava/lang/String was found, it will find also the setValue(String) setter and generates a String-BeanProperty
    -- If getValue()Ljava/lang/Object was found, it will the setValue(String) setter, but cannot use it, because rhino thinks the property is of type Object and the setter is incompatible
  • The main problem is, that Class.getMethods() does not return the methods in a particular order. (What happened: In our unit tests, the correct method was preferred, in production the wrong method was preferred)

This PR changes the method discovery in a way that if a method with same signature was found, but a "better" return type, this method is used.

@gbrail
Copy link
Collaborator

gbrail commented Aug 23, 2024

Looks like a good quality thing to fix to me but I don't work with the LiveConnect stuff very much.

Does anyone else who uses this stuff extensively want to have a look?

@rbri
Copy link
Collaborator

rbri commented Aug 23, 2024

Does anyone else who uses this stuff extensively want to have a look?

sorry i do not use it

@p-bakker
Copy link
Collaborator

@andreabergia care to have a look at this one? Seems like your team also uses Java interop a fair bit

@p-bakker
Copy link
Collaborator

Don't use it either to the extend that we would run into this, but reading the explanation and the linked Java succes, I think the PR makes sense

@p-bakker p-bakker added the Java Interop Issues related to the interaction between Java and JavaScript label Aug 24, 2024
if (existing == null) {
map.put(sig, method);
} else if (existing.getReturnType().isAssignableFrom(method.getReturnType())) {
// if there are multiple methods with same name (which is allowed in bytecode, but not
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. if the existing method has a return type of Object but new method will return String, we will use this method.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on a Map efficiency kick this week, so I apologize in advance -- but is there a way that you could replace the "map.get" / "map.put" pair with "putIfAbsent" or something like that? I'm not 100% sure but I think that if it works you could have fewer hash map traversals.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Map calls are actually ok the way that they are. The default implementation of putIfAbsent does exactly the same thing, and this way he already has a reference to the existing value for the else condition without needing to get it again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore my last comment. HashMap does not use the default Map implementation, so there may be a slight improvement. @gbrail, would it need to look something like this instead?

Method putResult = map.putIfAbsent(sig, method);
if (putResult != method && putResult.getReturnType().isAssignableFrom(method.getReturnType())) {
    map.put(sig, method);
}

MethodSignature sig = new MethodSignature(method);
if (!map.containsKey(sig)) map.put(sig, method);
}
discoverPublicMethods(clazz, map);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in discoverPublicMethods:

    void discoverPublicMethods(Class<?> clazz, Map<MethodSignature, Method> map) {
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            registerMethod(map, method);
        }
    }

@gbrail
Copy link
Collaborator

gbrail commented Sep 8, 2024

I would like to move forward with this but I'd like to know if there is a way to optimize the map usage like I commented above. Thanks!

@gbrail
Copy link
Collaborator

gbrail commented Sep 12, 2024 via email

@rPraml
Copy link
Contributor Author

rPraml commented Sep 18, 2024

Can take a look at the PR next week or so

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Java Interop Issues related to the interaction between Java and JavaScript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants