jpype-project / jpype

JPype is cross language bridge to allow Python programs full access to Java class libraries.

Home Page:http://www.jpype.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Python lambda to FunctionalInterface conversion is too strict

astrelsky opened this issue · comments

Note the following line in the documentation for FunctionalInterface seen here:

"However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration."

Unfortunately, jpype does not allow conversion from a lambda to a functional interface if the Java interface does not have the @FunctionInterface annotation. However, this should not be the case and any functional interface should be convertable.

The below java and python code snippits provide a simple demonstration of the problem.

package tmp;

public class App {
    
    public static void printNonAnnotated(NonAnnotatedFunctionInterface messageGetter) {
        System.out.println(messageGetter.getMessage());
    }
    
    public static void printAnnotated(AnnotatedFunctionInterface messageGetter) {
        printNonAnnotated(messageGetter);
    }
    
    public static void main(String[] args) {
        printNonAnnotated(() -> "Hello from non annotated interface");
        printAnnotated(() -> "Hello from annotated interface");
    }
    
    public static interface NonAnnotatedFunctionInterface {
        
        public String getMessage();
    }
    
    @FunctionalInterface
    public static interface AnnotatedFunctionInterface extends NonAnnotatedFunctionInterface {
    }
}
import jpype
from jpype.imports import *

if __name__ == '__main__':
    jpype.startJVM(classpath="tmp.jar")
    from tmp import App
    App.main([])
    App.printAnnotated(lambda : "Hello from Python annotated interface")
    App.printNonAnnotated(lambda : "Hello from Python non annotated interface")

Running the above python code results in the following output:

Hello from non annotated interface
Hello from annotated interface
Hello from Python annotated interface
Traceback (most recent call last):
  File "C:\Users\astre\Documents\tmp\main.py", line 9, in <module>
    App.printNonAnnotated(lambda : "Hello from Python non annotated interface")
TypeError: No matching overloads found for *static* tmp.App.printNonAnnotated(function), options are:
        public static void tmp.App.printNonAnnotated(tmp.App$NonAnnotatedFunctionInterface)

I can try to figure out a pattern for identifying a SAM (single abstract method), but there isn't such a thing in the Java API so the only way to know currently is to check for the FunctionInterface annotation or to fully inspect all methods in the class at runtime. It is really the issue with Java that the compiler doesn't automatically annotation rather than a bug in JPype. As it currently stands we only identify a SAM by annotation.

I can try to figure out a pattern for identifying a SAM (single abstract method), but there isn't such a thing in the Java API so the only way to know currently is to check for the FunctionInterface annotation or to fully inspect all methods in the class at runtime. It is really the issue with Java that the compiler doesn't automatically annotation rather than a bug in JPype. As it currently stands we only identify a SAM by annotation.

I'm not familiar with jni, but I would expect equivalent functions to be available for the below Java code. This was the most trivial solution I could come up with which would short circuit on non functional interfaces. Worst case is for a function interface with numerous static and default methods.

public static Method getFunctionalInterfaceMethod(Class<?> cls) {
    // Not sure how someone would pass in an annotation, but may as well check to be oo the safe side
    if (!cls.isInterface() || cls.isAnnotation()) {
        return null;
    }
    Method result = null;
    // Class.getMethods() includes inherited methods and interfaces can only have public methods
    for (Method m : cls.getMethods()) {
        if (Modifier.isAbstract(m.getModifiers())) {
            if (result != null) {
                return null;
            }
            result = m;
        }
    }
    return result;
}

The good news is that the code for this is pure Java. The bad news is I am not sure off the side effects that are likely to happen...

The determination is made in native/java/org/jpype/manager/TypeManager.java on line 475

    if (this.functionalAnnotation != null
            && cls.getAnnotation(this.functionalAnnotation) != null)
      modifiers |= ModifierCode.FUNCTIONAL.value | ModifierCode.SPECIAL.value;

So that just triggers the logic to start the process. The next part has to figure out the name of the SAM.

This is in native/org/jpype/JPypeContext.java

  /**
   * Utility to probe functional interfaces.
   *
   * @param cls
   * @return
   */
  public String getFunctional(Class cls)
  {
    // If we don't find it to be a functional interface, then we won't return
    // the SAM.
    if (cls.getDeclaredAnnotation(FunctionalInterface.class) == null)
      return null;
    for (Method m : cls.getMethods())
    {
      if (Modifier.isAbstract(m.getModifiers()))
      {
        // This is a very odd construct.  Java allows for java.lang.Object
        // methods to declared in FunctionalInterfaces and they don't count
        // towards the single abstract method. So we have to probe the class
        // until we find something that fails.
        try
        {
          Object.class.getMethod(m.getName(), m.getParameterTypes());
        } catch (NoSuchMethodException | SecurityException ex)
        {
          return m.getName();
        }
      }
    }
    return null;
  }

This method shouldn't be used except on a SAM, because it will generate an exception for each method in the class (which is slow and costly).

You will need to test your code on a wide variety of cases, such as default methods, inherited interfaces that are SAM or not (from parent or from child).

public class Test
{
  @FunctionalInterface
  public interface A
  {
    void f(); // SAM 
    default void g() {}
  }
  
  @FunctionalInterface
  public interface B extends A
  {
    // f is still the SAM
    default void h() {}
  }
  
  @FunctionalInterface
  public interface C extends A
  {
    void h();  // NOT SAM!
  }
  
  @FunctionalInterface
  public interface D extends Serializable
  {
    void h();  // SAM
  } 
}
        // This is a very odd construct.  Java allows for java.lang.Object
        // methods to declared in FunctionalInterfaces and they don't count
        // towards the single abstract method. So we have to probe the class
        // until we find something that fails.

That's an annoying quirk. I've played around with this a bit more and think I've narrowed down exactly what is allowed and what isn't. I'm a bit tired so I'll need to re-look at this with a fresh set of eyes, but I may be able to do away with the Object.class.getMethod call. I'll probably open a pull request in a few days or over the weekend with something.

All good. I look forward to the PR.

As I mentioned at the start, it isn't that we can't support the full Java specification, it is simply that since Java lacks methods to determine what qualifies as a SAM, we end up having to write something ourselves and then try to find all the edge cases. The FunctionalInterface only served my needs but if we can hit the full specification that is great.