Skip to content

Commit

Permalink
Merge pull request #3809 from jruby/ji-iface
Browse files Browse the repository at this point in the history
bind (abstract) methods when implementing interface
  • Loading branch information
kares committed Apr 27, 2016
2 parents 08e4b1b + 79bb8d1 commit e097afb
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 27 deletions.
27 changes: 23 additions & 4 deletions core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ public abstract class RealClassGenerator {

private static final int V_BC = V1_6; // version used for generated byte-code

public static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces, String[] superTypeNames) throws SecurityException {
Map<String, List<Method>> simpleToAll = new LinkedHashMap<String, List<Method>>();
//public static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces, String[] superTypeNames)
// throws SecurityException {
// return buildSimpleToAllMap(interfaces, superTypeNames, null);
//}

static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces, String[] superTypeNames, RubyClass implClass)
throws SecurityException {
final LinkedHashMap<String, List<Method>> simpleToAll = new LinkedHashMap<>();
// we're use the map's order to work-around bug when there's too getters for a property :
// getFoo and isFoo in which case we make sure getFoo will come after isFoo in the map
// so that the installed "foo" alias always triggers getFoo regardless of getMethods order
Expand All @@ -84,6 +90,11 @@ public static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces,
for ( Method method : interfaces[i].getMethods() ) {
final String name = method.getName();
if ( Modifier.isStatic(method.getModifiers()) ) continue;
if ( implClass != null ) { // only override default methods if present in implementing class
if ( ! Modifier.isAbstract(method.getModifiers()) && ! implClass.getMethods().containsKey(name) ) {
continue;
}
}
List<Method> methods = simpleToAll.get(name);
if (methods == null) {
simpleToAll.put(name, methods = new ArrayList<Method>(6));
Expand All @@ -103,18 +114,26 @@ public static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces,
return simpleToAll;
}

// NOTE: assuming this is only used for interface-impl generation from: Java.newInterfaceImpl
public static Class createOldStyleImplClass(Class[] superTypes, RubyClass rubyClass, Ruby ruby, String name, ClassDefiningClassLoader classLoader) {
String[] superTypeNames = new String[superTypes.length];
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(superTypes, superTypeNames);

// interfaces now do have a convention that they only override an interface default method
// if a Ruby method (stub) is present in the implementing Ruby class :
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(superTypes, superTypeNames, rubyClass);

Class newClass = defineOldStyleImplClass(ruby, name, superTypeNames, simpleToAll, classLoader);

return newClass;
}

// NOTE: only used for interface class generation from ... Java.generateRealClass
public static Class createRealImplClass(Class superClass, Class[] interfaces, RubyClass rubyClass, Ruby ruby, String name) {
String[] superTypeNames = new String[interfaces.length];
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(interfaces, superTypeNames);

// interfaces now do have a convention that they only override an interface default method
// if a Ruby method (stub) is present in the implementing Ruby class :
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(interfaces, superTypeNames, rubyClass);

Class newClass = defineRealImplClass(ruby, name, superClass, superTypeNames, simpleToAll);
if (!newClass.isAssignableFrom(interfaces[0])) {
Expand Down
118 changes: 108 additions & 10 deletions core/src/main/java/org/jruby/java/proxies/JavaInterfaceTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyModule;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.internal.runtime.methods.JavaMethod;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodN;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOne;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOneBlock;
Expand Down Expand Up @@ -258,6 +262,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
public static void addRealImplClassNew(final RubyClass clazz) {
clazz.setAllocator(new ObjectAllocator() {
private Constructor proxyConstructor;

public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
// if we haven't been here before, reify the class
Class reifiedClass = klazz.getReifiedClass();
Expand Down Expand Up @@ -342,33 +347,68 @@ public static IRubyObject op_aref(ThreadContext context, IRubyObject self, IRuby
return JavaProxy.op_aref(context, self, args);
}

@JRubyMethod(name = "impl", rest = true)
@JRubyMethod(name = "impl", rest = true) // impl(methods = true)
public static IRubyObject impl(ThreadContext context, IRubyObject self, IRubyObject[] args, final Block implBlock) {
final Ruby runtime = context.runtime;

if ( ! implBlock.isGiven() ) {
throw runtime.newArgumentError("block required to call #impl on a Java interface");
}

boolean allMethods = true;
final IRubyObject[] methodNames;
if ( args.length == 0 ) methodNames = null;
else if ( args.length == 1 && args[0] instanceof RubyBoolean ) {
allMethods = args[0].isTrue(); // impl(false) ... allMethods = false
methodNames = null;
}
else {
methodNames = args.clone();
Arrays.sort(methodNames); // binarySearch needs a sorted array
// RubySymbol implements a Java compareTo thus will always work
}

RubyClass implClass = RubyClass.newClass(runtime, runtime.getObject());
implClass.include(context, self);

final IRubyObject implObject = implClass.callMethod(context, "new");

implClass.addMethod("method_missing", new BlockInterfaceImpl(implClass, implBlock, methodNames));
RubyClass implClass = RubyClass.newClass(runtime, runtime.getObject()); // ImplClass = Class.new
implClass.include(context, self); // ImplClass.include Interface

final BlockInterfaceImpl ifaceImpl = new BlockInterfaceImpl(implClass, implBlock, methodNames);
implClass.addMethod("method_missing", ifaceImpl); // def ImplClass.method_missing ...

final Class<?> ifaceClass = JavaClass.getJavaClass(context, ((RubyModule) self));
if ( methodNames == null ) {
final BlockInterfaceImpl.ConcreteMethod implMethod = ifaceImpl.getConcreteMethod();
for ( Method method : ifaceClass.getMethods() ) {
if ( method.isBridge() || method.isSynthetic() ) continue;
if ( Modifier.isStatic( method.getModifiers() ) ) continue;
// override default methods (by default) - users should pass down method names or impl(false) { ... }
if ( ! allMethods && ! Modifier.isAbstract( method.getModifiers() ) ) continue;
implClass.addMethodInternal(method.getName(), implMethod); // might add twice - its fine
}
}
else {
final BlockInterfaceImpl.ConcreteMethod implMethod = ifaceImpl.getConcreteMethod();
final Method[] decMethods = ifaceClass.getDeclaredMethods();
loop: for ( IRubyObject methodName : methodNames ) {
final String name = methodName.toString();
for ( int i = 0; i < decMethods.length; i++ ) {
final Method method = decMethods[i];
if ( method.isBridge() || method.isSynthetic() ) continue;
if ( Modifier.isStatic( method.getModifiers() ) ) continue;
// add if its a declared method of the interface or its super-interfaces
if ( name.equals(decMethods[i].getName()) ) {
implClass.addMethodInternal(name, implMethod);
continue loop;
}
}
// did not continue (main) loop - passed method name not found in interface
runtime.getWarnings().warn("`" + name + "' is not a declared method in interface " + ifaceClass.getName());
}
}

return implObject;
return implClass.callMethod(context, "new"); // ImplClass.new
}

private static final class BlockInterfaceImpl extends org.jruby.internal.runtime.methods.JavaMethod {
private static final class BlockInterfaceImpl extends JavaMethod {

private final IRubyObject[] methodNames; // RubySymbol[]
private final Block implBlock;
Expand Down Expand Up @@ -398,6 +438,11 @@ else if ( Arrays.binarySearch(methodNames, args[0]) >= 0 ) {
return clazz.getSuperClass().callMethod(context, "method_missing", args, block);
}

@Override
public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
return callImpl(context, klazz, block); // avoids checkArgumentCount
}

@Override
public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
return callImpl(context, klazz, block, arg0); // avoids checkArgumentCount
Expand All @@ -413,7 +458,60 @@ public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModul
return callImpl(context, klazz, block, arg0, arg1, arg2); // avoids checkArgumentCount
}

//public DynamicMethod dup() { return this; }
public DynamicMethod dup() { return this; }

final ConcreteMethod getConcreteMethod() { return new ConcreteMethod(); }

private final class ConcreteMethod extends JavaMethod {

ConcreteMethod() {
super(BlockInterfaceImpl.this.implementationClass, Visibility.PUBLIC);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name) };
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0 };
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) {
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0, arg1 };
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) {
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0, arg1, arg2 };
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject[] args, Block block) {
switch (args.length) {
case 0:
return call(context, self, klazz, name, block);
case 1:
return call(context, self, klazz, name, args[0], block);
case 2:
return call(context, self, klazz, name, args[0], args[1], block);
case 3:
return call(context, self, klazz, name, args[0], args[1], args[2], block);
default:
final IRubyObject[] nargs = new IRubyObject[args.length + 1];
nargs[0] = context.runtime.newSymbol(name);
System.arraycopy(args, 0, nargs, 1, args.length);
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
}
}

}

}

Expand Down
53 changes: 46 additions & 7 deletions core/src/main/java/org/jruby/javasupport/Java.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
import org.jruby.javasupport.binding.Initializer;
import org.jruby.javasupport.proxy.JavaProxyClass;
import org.jruby.javasupport.proxy.JavaProxyConstructor;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.Library;
Expand Down Expand Up @@ -1002,18 +1003,14 @@ public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModul

}

final static class ProcToInterface extends org.jruby.internal.runtime.methods.DynamicMethod {
static final class ProcToInterface extends org.jruby.internal.runtime.methods.DynamicMethod {

ProcToInterface(final RubyClass singletonClass) {
super(singletonClass, PUBLIC);
}

@Override // method_missing impl :
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
if ( ! ( self instanceof RubyProc ) ) {
throw context.runtime.newTypeError("interface impl method_missing for block used with non-Proc object");
}
final RubyProc proc = (RubyProc) self;
final IRubyObject[] newArgs;
switch( args.length ) {
case 1 : newArgs = IRubyObject.NULL_ARRAY; break;
Expand All @@ -1022,14 +1019,56 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
default : newArgs = new IRubyObject[ args.length - 1 ];
System.arraycopy(args, 1, newArgs, 0, newArgs.length);
}
return proc.call(context, newArgs);
return callProc(context, self, newArgs);
}

private IRubyObject callProc(ThreadContext context, IRubyObject self, IRubyObject[] procArgs) {
if ( ! ( self instanceof RubyProc ) ) {
throw context.runtime.newTypeError("interface impl method_missing for block used with non-Proc object");
}
return ((RubyProc) self).call(context, procArgs);
}

@Override
public DynamicMethod dup() {
return this;
}

final ConcreteMethod getConcreteMethod() { return new ConcreteMethod(); }

final class ConcreteMethod extends org.jruby.internal.runtime.methods.JavaMethod {

ConcreteMethod() {
super(ProcToInterface.this.implementationClass, Visibility.PUBLIC);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
return ProcToInterface.this.callProc(context, self, IRubyObject.NULL_ARRAY);
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0});
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) {
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0, arg1});
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) {
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0, arg1, arg2});
}

@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject[] args, Block block) {
return ProcToInterface.this.callProc(context, self, args);
}

}

}

private static RubyModule getProxyUnderClass(final ThreadContext context,
Expand Down
16 changes: 14 additions & 2 deletions core/src/main/java/org/jruby/javasupport/JavaUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ReflectPermission;
import static java.lang.Character.isLetter;
import static java.lang.Character.isLowerCase;
import static java.lang.Character.isUpperCase;
import static java.lang.Character.isDigit;
import static java.lang.Character.toLowerCase;

import java.lang.reflect.ReflectPermission;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.AccessController;
Expand Down Expand Up @@ -243,7 +244,18 @@ public static <T> T convertProcToInterface(ThreadContext context, RubyBasicObjec
// Proc implementing an interface, pull in the catch-all code that lets the proc get invoked
// no matter what method is called on the interface
final RubyClass singletonClass = rubyObject.getSingletonClass();
singletonClass.addMethod("method_missing", new Java.ProcToInterface(singletonClass));
final Java.ProcToInterface procToIface = new Java.ProcToInterface(singletonClass);
singletonClass.addMethod("method_missing", procToIface);
// similar to Iface.impl { ... } - bind interface method(s) to avoid Java-Ruby conflicts
// ... e.g. calling a Ruby implemented Predicate#test should not dispatch to Kernel#test
final Java.ProcToInterface.ConcreteMethod implMethod = procToIface.getConcreteMethod();
// getMethods for interface returns all methods (including ones from super-interfaces)
for ( Method method : targetType.getMethods() ) {
if ( Modifier.isAbstract(method.getModifiers()) ) {
singletonClass.addMethodInternal(method.getName(), implMethod);
}
}

}
JavaObject javaObject = (JavaObject) Helpers.invoke(context, rubyObject, "__jcreate_meta!");
return (T) javaObject.getValue();
Expand Down
Loading

0 comments on commit e097afb

Please sign in to comment.