Skip to content

Commit e097afb

Browse files
committed
Merge pull request #3809 from jruby/ji-iface
bind (abstract) methods when implementing interface
2 parents 08e4b1b + 79bb8d1 commit e097afb

File tree

6 files changed

+287
-27
lines changed

6 files changed

+287
-27
lines changed

core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,14 @@ public abstract class RealClassGenerator {
7474

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

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

117+
// NOTE: assuming this is only used for interface-impl generation from: Java.newInterfaceImpl
106118
public static Class createOldStyleImplClass(Class[] superTypes, RubyClass rubyClass, Ruby ruby, String name, ClassDefiningClassLoader classLoader) {
107119
String[] superTypeNames = new String[superTypes.length];
108-
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(superTypes, superTypeNames);
120+
121+
// interfaces now do have a convention that they only override an interface default method
122+
// if a Ruby method (stub) is present in the implementing Ruby class :
123+
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(superTypes, superTypeNames, rubyClass);
109124

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

112127
return newClass;
113128
}
114129

130+
// NOTE: only used for interface class generation from ... Java.generateRealClass
115131
public static Class createRealImplClass(Class superClass, Class[] interfaces, RubyClass rubyClass, Ruby ruby, String name) {
116132
String[] superTypeNames = new String[interfaces.length];
117-
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(interfaces, superTypeNames);
133+
134+
// interfaces now do have a convention that they only override an interface default method
135+
// if a Ruby method (stub) is present in the implementing Ruby class :
136+
Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(interfaces, superTypeNames, rubyClass);
118137

119138
Class newClass = defineRealImplClass(ruby, name, superClass, superTypeNames, simpleToAll);
120139
if (!newClass.isAssignableFrom(interfaces[0])) {

core/src/main/java/org/jruby/java/proxies/JavaInterfaceTemplate.java

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22

33
import java.lang.reflect.Constructor;
44
import java.lang.reflect.Method;
5+
import java.lang.reflect.Modifier;
56
import java.util.Arrays;
7+
68
import org.jruby.Ruby;
79
import org.jruby.RubyArray;
10+
import org.jruby.RubyBoolean;
811
import org.jruby.RubyClass;
912
import org.jruby.RubyInstanceConfig;
1013
import org.jruby.RubyModule;
1114
import org.jruby.anno.JRubyMethod;
1215
import org.jruby.exceptions.RaiseException;
1316
import org.jruby.internal.runtime.methods.DynamicMethod;
17+
import org.jruby.internal.runtime.methods.JavaMethod;
1418
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodN;
1519
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOne;
1620
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOneBlock;
@@ -258,6 +262,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
258262
public static void addRealImplClassNew(final RubyClass clazz) {
259263
clazz.setAllocator(new ObjectAllocator() {
260264
private Constructor proxyConstructor;
265+
261266
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
262267
// if we haven't been here before, reify the class
263268
Class reifiedClass = klazz.getReifiedClass();
@@ -342,33 +347,68 @@ public static IRubyObject op_aref(ThreadContext context, IRubyObject self, IRuby
342347
return JavaProxy.op_aref(context, self, args);
343348
}
344349

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

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

358+
boolean allMethods = true;
353359
final IRubyObject[] methodNames;
354360
if ( args.length == 0 ) methodNames = null;
361+
else if ( args.length == 1 && args[0] instanceof RubyBoolean ) {
362+
allMethods = args[0].isTrue(); // impl(false) ... allMethods = false
363+
methodNames = null;
364+
}
355365
else {
356366
methodNames = args.clone();
357367
Arrays.sort(methodNames); // binarySearch needs a sorted array
358368
// RubySymbol implements a Java compareTo thus will always work
359369
}
360370

361-
RubyClass implClass = RubyClass.newClass(runtime, runtime.getObject());
362-
implClass.include(context, self);
363-
364-
final IRubyObject implObject = implClass.callMethod(context, "new");
365-
366-
implClass.addMethod("method_missing", new BlockInterfaceImpl(implClass, implBlock, methodNames));
371+
RubyClass implClass = RubyClass.newClass(runtime, runtime.getObject()); // ImplClass = Class.new
372+
implClass.include(context, self); // ImplClass.include Interface
373+
374+
final BlockInterfaceImpl ifaceImpl = new BlockInterfaceImpl(implClass, implBlock, methodNames);
375+
implClass.addMethod("method_missing", ifaceImpl); // def ImplClass.method_missing ...
376+
377+
final Class<?> ifaceClass = JavaClass.getJavaClass(context, ((RubyModule) self));
378+
if ( methodNames == null ) {
379+
final BlockInterfaceImpl.ConcreteMethod implMethod = ifaceImpl.getConcreteMethod();
380+
for ( Method method : ifaceClass.getMethods() ) {
381+
if ( method.isBridge() || method.isSynthetic() ) continue;
382+
if ( Modifier.isStatic( method.getModifiers() ) ) continue;
383+
// override default methods (by default) - users should pass down method names or impl(false) { ... }
384+
if ( ! allMethods && ! Modifier.isAbstract( method.getModifiers() ) ) continue;
385+
implClass.addMethodInternal(method.getName(), implMethod); // might add twice - its fine
386+
}
387+
}
388+
else {
389+
final BlockInterfaceImpl.ConcreteMethod implMethod = ifaceImpl.getConcreteMethod();
390+
final Method[] decMethods = ifaceClass.getDeclaredMethods();
391+
loop: for ( IRubyObject methodName : methodNames ) {
392+
final String name = methodName.toString();
393+
for ( int i = 0; i < decMethods.length; i++ ) {
394+
final Method method = decMethods[i];
395+
if ( method.isBridge() || method.isSynthetic() ) continue;
396+
if ( Modifier.isStatic( method.getModifiers() ) ) continue;
397+
// add if its a declared method of the interface or its super-interfaces
398+
if ( name.equals(decMethods[i].getName()) ) {
399+
implClass.addMethodInternal(name, implMethod);
400+
continue loop;
401+
}
402+
}
403+
// did not continue (main) loop - passed method name not found in interface
404+
runtime.getWarnings().warn("`" + name + "' is not a declared method in interface " + ifaceClass.getName());
405+
}
406+
}
367407

368-
return implObject;
408+
return implClass.callMethod(context, "new"); // ImplClass.new
369409
}
370410

371-
private static final class BlockInterfaceImpl extends org.jruby.internal.runtime.methods.JavaMethod {
411+
private static final class BlockInterfaceImpl extends JavaMethod {
372412

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

441+
@Override
442+
public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
443+
return callImpl(context, klazz, block); // avoids checkArgumentCount
444+
}
445+
401446
@Override
402447
public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
403448
return callImpl(context, klazz, block, arg0); // avoids checkArgumentCount
@@ -413,7 +458,60 @@ public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModul
413458
return callImpl(context, klazz, block, arg0, arg1, arg2); // avoids checkArgumentCount
414459
}
415460

416-
//public DynamicMethod dup() { return this; }
461+
public DynamicMethod dup() { return this; }
462+
463+
final ConcreteMethod getConcreteMethod() { return new ConcreteMethod(); }
464+
465+
private final class ConcreteMethod extends JavaMethod {
466+
467+
ConcreteMethod() {
468+
super(BlockInterfaceImpl.this.implementationClass, Visibility.PUBLIC);
469+
}
470+
471+
@Override
472+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
473+
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name) };
474+
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
475+
}
476+
477+
@Override
478+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
479+
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0 };
480+
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
481+
}
482+
483+
@Override
484+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) {
485+
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0, arg1 };
486+
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
487+
}
488+
489+
@Override
490+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) {
491+
final IRubyObject[] nargs = new IRubyObject[] { context.runtime.newSymbol(name), arg0, arg1, arg2 };
492+
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
493+
}
494+
495+
@Override
496+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject[] args, Block block) {
497+
switch (args.length) {
498+
case 0:
499+
return call(context, self, klazz, name, block);
500+
case 1:
501+
return call(context, self, klazz, name, args[0], block);
502+
case 2:
503+
return call(context, self, klazz, name, args[0], args[1], block);
504+
case 3:
505+
return call(context, self, klazz, name, args[0], args[1], args[2], block);
506+
default:
507+
final IRubyObject[] nargs = new IRubyObject[args.length + 1];
508+
nargs[0] = context.runtime.newSymbol(name);
509+
System.arraycopy(args, 0, nargs, 1, args.length);
510+
return BlockInterfaceImpl.this.callImpl(context, klazz, block, nargs);
511+
}
512+
}
513+
514+
}
417515

418516
}
419517

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

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@
6565
import org.jruby.javasupport.binding.Initializer;
6666
import org.jruby.javasupport.proxy.JavaProxyClass;
6767
import org.jruby.javasupport.proxy.JavaProxyConstructor;
68-
import org.jruby.runtime.Helpers;
6968
import org.jruby.runtime.Arity;
7069
import org.jruby.runtime.Block;
70+
import org.jruby.runtime.Helpers;
71+
import org.jruby.runtime.Visibility;
7172
import org.jruby.runtime.ThreadContext;
7273
import org.jruby.runtime.builtin.IRubyObject;
7374
import org.jruby.runtime.load.Library;
@@ -1002,18 +1003,14 @@ public final IRubyObject call(ThreadContext context, IRubyObject self, RubyModul
10021003

10031004
}
10041005

1005-
final static class ProcToInterface extends org.jruby.internal.runtime.methods.DynamicMethod {
1006+
static final class ProcToInterface extends org.jruby.internal.runtime.methods.DynamicMethod {
10061007

10071008
ProcToInterface(final RubyClass singletonClass) {
10081009
super(singletonClass, PUBLIC);
10091010
}
10101011

10111012
@Override // method_missing impl :
10121013
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
1013-
if ( ! ( self instanceof RubyProc ) ) {
1014-
throw context.runtime.newTypeError("interface impl method_missing for block used with non-Proc object");
1015-
}
1016-
final RubyProc proc = (RubyProc) self;
10171014
final IRubyObject[] newArgs;
10181015
switch( args.length ) {
10191016
case 1 : newArgs = IRubyObject.NULL_ARRAY; break;
@@ -1022,14 +1019,56 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
10221019
default : newArgs = new IRubyObject[ args.length - 1 ];
10231020
System.arraycopy(args, 1, newArgs, 0, newArgs.length);
10241021
}
1025-
return proc.call(context, newArgs);
1022+
return callProc(context, self, newArgs);
1023+
}
1024+
1025+
private IRubyObject callProc(ThreadContext context, IRubyObject self, IRubyObject[] procArgs) {
1026+
if ( ! ( self instanceof RubyProc ) ) {
1027+
throw context.runtime.newTypeError("interface impl method_missing for block used with non-Proc object");
1028+
}
1029+
return ((RubyProc) self).call(context, procArgs);
10261030
}
10271031

10281032
@Override
10291033
public DynamicMethod dup() {
10301034
return this;
10311035
}
10321036

1037+
final ConcreteMethod getConcreteMethod() { return new ConcreteMethod(); }
1038+
1039+
final class ConcreteMethod extends org.jruby.internal.runtime.methods.JavaMethod {
1040+
1041+
ConcreteMethod() {
1042+
super(ProcToInterface.this.implementationClass, Visibility.PUBLIC);
1043+
}
1044+
1045+
@Override
1046+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, Block block) {
1047+
return ProcToInterface.this.callProc(context, self, IRubyObject.NULL_ARRAY);
1048+
}
1049+
1050+
@Override
1051+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, Block block) {
1052+
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0});
1053+
}
1054+
1055+
@Override
1056+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) {
1057+
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0, arg1});
1058+
}
1059+
1060+
@Override
1061+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) {
1062+
return ProcToInterface.this.callProc(context, self, new IRubyObject[]{arg0, arg1, arg2});
1063+
}
1064+
1065+
@Override
1066+
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule klazz, String name, IRubyObject[] args, Block block) {
1067+
return ProcToInterface.this.callProc(context, self, args);
1068+
}
1069+
1070+
}
1071+
10331072
}
10341073

10351074
private static RubyModule getProxyUnderClass(final ThreadContext context,

core/src/main/java/org/jruby/javasupport/JavaUtil.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@
3838
import java.lang.reflect.Array;
3939
import java.lang.reflect.InvocationTargetException;
4040
import java.lang.reflect.Method;
41+
import java.lang.reflect.Modifier;
42+
import java.lang.reflect.ReflectPermission;
4143
import static java.lang.Character.isLetter;
4244
import static java.lang.Character.isLowerCase;
4345
import static java.lang.Character.isUpperCase;
4446
import static java.lang.Character.isDigit;
4547
import static java.lang.Character.toLowerCase;
4648

47-
import java.lang.reflect.ReflectPermission;
4849
import java.math.BigDecimal;
4950
import java.math.BigInteger;
5051
import java.security.AccessController;
@@ -243,7 +244,18 @@ public static <T> T convertProcToInterface(ThreadContext context, RubyBasicObjec
243244
// Proc implementing an interface, pull in the catch-all code that lets the proc get invoked
244245
// no matter what method is called on the interface
245246
final RubyClass singletonClass = rubyObject.getSingletonClass();
246-
singletonClass.addMethod("method_missing", new Java.ProcToInterface(singletonClass));
247+
final Java.ProcToInterface procToIface = new Java.ProcToInterface(singletonClass);
248+
singletonClass.addMethod("method_missing", procToIface);
249+
// similar to Iface.impl { ... } - bind interface method(s) to avoid Java-Ruby conflicts
250+
// ... e.g. calling a Ruby implemented Predicate#test should not dispatch to Kernel#test
251+
final Java.ProcToInterface.ConcreteMethod implMethod = procToIface.getConcreteMethod();
252+
// getMethods for interface returns all methods (including ones from super-interfaces)
253+
for ( Method method : targetType.getMethods() ) {
254+
if ( Modifier.isAbstract(method.getModifiers()) ) {
255+
singletonClass.addMethodInternal(method.getName(), implMethod);
256+
}
257+
}
258+
247259
}
248260
JavaObject javaObject = (JavaObject) Helpers.invoke(context, rubyObject, "__jcreate_meta!");
249261
return (T) javaObject.getValue();

0 commit comments

Comments
 (0)