diff --git a/bench/java/bench_block_to_interface.rb b/bench/java/bench_block_to_interface.rb new file mode 100644 index 00000000000..36812634fb1 --- /dev/null +++ b/bench/java/bench_block_to_interface.rb @@ -0,0 +1,61 @@ +require 'java' +require 'benchmark' + +N = (ARGV[0] || 2 ** 21).to_i # default ~ 2_000_000 + +EMPTY = java.util.Optional.empty +PRESENT = java.util.Optional.of('x') +PRESENT_INT = java.util.OptionalInt.of(11) +EMPTY_DOUBLE = java.util.OptionalDouble.empty + +LIST = java.util.ArrayList.new +LIST.add(java.lang.Integer.new(0)) + +EMPTY_MAP = java.util.HashMap.new + +puts "----- single-threaded -----" +Benchmark.bmbm(36) do |bm| + bm.report("Supplier Optional#orElseGet") { N.times { EMPTY.orElseGet { 'y' } } } + bm.report("Function Optional#map") { N.times { PRESENT.map { |s| s } } } + bm.report("Predicate Optional#filter") { N.times { PRESENT.filter { |_| true } } } + bm.report("IntConsumer OptionalInt#ifPresent") { N.times { PRESENT_INT.ifPresent { |i| } } } + bm.report("DoubleSupplier OptionalDouble#orElseGet") { N.times { EMPTY_DOUBLE.orElseGet { 0.0 } } } + bm.report("BiFunction Map#compute") { N.times { EMPTY_MAP.compute(:key) { |k, v| nil } } } + bm.report("BiFunction Map#computeIfPresent") { N.times { EMPTY_MAP.computeIfPresent(1) { fail('never called') } } } + bm.report("ToLongFunction Comparator.comparingLong") { N.times { java.util.Comparator.comparingLong { |s| s.length } } } + bm.report("Comparator ArrayList#sort") { N.times { LIST.sort { |a, b| a <=> b } } } +end + +puts +puts "----- multi-threaded (4 submitter threads, N tasks each) -----" +Benchmark.bmbm(36) do |bm| + bm.report("Consumer Optional#ifPresent (4 threads)") do + 4.times.map { Thread.new { (N / 4).times { PRESENT.ifPresent { |_| } } } }.each(&:join) + end + bm.report("Consumer Optional#ifPresent (16 threads)") do + 16.times.map { Thread.new { (N / 16).times { PRESENT.ifPresent { |_| } } } }.each(&:join) + end + bm.report("Consumer Optional#ifPresent (128 threads)") do + 128.times.map { Thread.new { (N / 128).times { PRESENT.ifPresent { |_| } } } }.each(&:join) + end + + bm.report("IntConsumer OptionalInt#ifPresent (4 threads)") do + 4.times.map { Thread.new { (N / 4).times { PRESENT_INT.ifPresent { |i| } } } }.each(&:join) + end + bm.report("IntConsumer OptionalInt#ifPresent (128 threads)") do + 128.times.map { Thread.new { (N / 128).times { PRESENT_INT.ifPresent { |i| } } } }.each(&:join) + end + bm.report("IntConsumer OptionalInt#ifPresent (512 threads)") do + 512.times.map { Thread.new { (N / 512).times { PRESENT_INT.ifPresent { |i| } } } }.each(&:join) + end + + bm.report("Comparator List#stream.sorted (16 threads)") do + 16.times.map { Thread.new { (N / 16).times { LIST.stream.sorted { |a, b| a <=> b } } } }.each(&:join) + end + bm.report("Comparator List#stream.sorted (128 threads)") do + 128.times.map { Thread.new { (N / 128).times { LIST.stream.sorted { |a, b| a <=> b } } } }.each(&:join) + end + bm.report("Comparator List#stream.sorted (512 threads)") do + 512.times.map { Thread.new { (N / 512).times { LIST.stream.sorted { |a, b| a <=> b } } } }.each(&:join) + end +end diff --git a/core/src/main/java/org/jruby/RubyProc.java b/core/src/main/java/org/jruby/RubyProc.java index c1a5e4ba26a..b13917198b1 100644 --- a/core/src/main/java/org/jruby/RubyProc.java +++ b/core/src/main/java/org/jruby/RubyProc.java @@ -40,6 +40,8 @@ import org.jruby.ast.util.ArgsUtil; import org.jruby.ir.runtime.IRRuntimeHelpers; +import org.jruby.java.codegen.BlockInterfaceGenerator; +import org.jruby.javasupport.Java; import org.jruby.parser.StaticScope; import org.jruby.runtime.Binding; import org.jruby.runtime.Block; @@ -494,12 +496,23 @@ private boolean isFromMethod() { return getBlock().getBody() instanceof MethodBlockBody; } - //private boolean isProc() { - // return type.equals(Block.Type.PROC); - //} + @Override + public T toJava(final Class target) { + if (type == Block.Type.JAVA) { + var constructor = BlockInterfaceGenerator.fromCache(target); + if (constructor != null) { // fast cached path (we know it's a functional interface) + return Java.newBlockToInterfaceInstance(this, constructor); + } - private boolean isThread() { - return type.equals(Block.Type.THREAD); + if (Java.isFunctionalInterfaceType(target)) { + constructor = Java.getBlockToInterfaceConstructor(getRuntime(), target); + return Java.newBlockToInterfaceInstance(this, constructor); + } + + // NOTE: this dummy proc could end-up in user-land; thus use a proper clone: + return newProc(getRuntime(), block, block.type).defaultToJava(target); + } + return defaultToJava(target); } private static JavaSites.ProcSites sites(ThreadContext context) { diff --git a/core/src/main/java/org/jruby/java/codegen/BlockInterfaceGenerator.java b/core/src/main/java/org/jruby/java/codegen/BlockInterfaceGenerator.java new file mode 100644 index 00000000000..8745361df97 --- /dev/null +++ b/core/src/main/java/org/jruby/java/codegen/BlockInterfaceGenerator.java @@ -0,0 +1,248 @@ +package org.jruby.java.codegen; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentMap; + +import org.jruby.Ruby; +import org.jruby.RubyProc; +import org.jruby.compiler.impl.SkinnyMethodAdapter; +import org.jruby.java.proxies.BlockInterfaceTemplate; +import org.jruby.javasupport.Java; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ASM; +import org.jruby.util.ClassDefiningClassLoader; +import org.jruby.util.JRubyClassLoader; +import org.jruby.util.collections.ConcurrentWeakHashMap; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; + +import static org.jruby.util.CodegenUtils.ci; +import static org.jruby.util.CodegenUtils.getBoxType; +import static org.jruby.util.CodegenUtils.p; +import static org.jruby.util.CodegenUtils.sig; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_SUPER; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; +import static org.objectweb.asm.Opcodes.V11; + +/** + * Generates a concrete subclass of {@link BlockInterfaceTemplate} for a Java interface, where + * every abstract method delegates straight into one of the inherited {@code __ruby_call} helpers. + * + * Default methods on the interface are intentionally not overridden, matching the pre-existing + * {@code convertProcToInterface} behavior where only abstract methods were backed by the proc. + * + *

For an interface such as: + *

{@code
+ *   public interface MyIface {
+ *       int compute(int a, String b);
+ *       default int compute2(int a, String b) { return compute(a, b) * 2; }
+ *   }
+ * }
+ * the generated class is equivalent to: + *
{@code
+ *   public final class BlockInterfaceImpl$ extends BlockInterfaceTemplate implements MyIface {
+ *       public BlockInterfaceImpl$(RubyProc proc) { super(proc); }
+ *
+ *       public int compute(int a, String b) {
+ *           Object result = __ruby_call(Integer.TYPE, coerce(a), coerce(b));
+ *           return ((Number) result).intValue();
+ *       }
+ *       // compute2 is not overridden — the interface default is used
+ *   }
+ * }
+ */ +public final class BlockInterfaceGenerator { + + /** + * Cache of generated {@link BlockInterfaceTemplate} subclass constructors, weakly keyed by the interface. + * + *

+ * The generated class lives in the {@link JRubyClassLoader}, so the value never roots a classloader outside of + * JRuby itself; the only strong root held by a cache entry is the constructor (class pair) for the interface. + */ + static final ConcurrentMap, Constructor> CACHE = new ConcurrentWeakHashMap<>(); + + public static Constructor fromCache(final Class interfaceType) { + return CACHE.get(interfaceType); + } + + /** + * @return the constructor, which is usable with any Ruby block targeting the same interface + */ + @SuppressWarnings("unchecked") + public static Constructor getConstructor(final Ruby runtime, final Class interfaceType) + throws ReflectiveOperationException { + + var constructor = CACHE.get(interfaceType); + if (constructor != null) return constructor; + + assert interfaceType.isInterface(); + + final String implClassName = makeImplClassName(interfaceType); + final JRubyClassLoader loader = runtime.getJRubyClassLoader(); + + synchronized (loader) { + constructor = CACHE.get(interfaceType); + if (constructor != null) return constructor; + + Class implClass; + try { + implClass = Class.forName(implClassName, true, loader); + } catch (ClassNotFoundException e) { + assert Java.getFunctionalInterfaceMethod(interfaceType) != null : "not a functional-interface: " + interfaceType; + implClass = defineImplClass(loader, interfaceType, implClassName); + } + + constructor = (Constructor) implClass.getConstructor(RubyProc.class); + CACHE.put(interfaceType, constructor); + return constructor; + } + } + + private static String makeImplClassName(final Class interfaceType) { + return "org.jruby.gen.BlockInterfaceImpl$" + interfaceType.getSimpleName() + Math.abs(interfaceType.hashCode()); + } + + /** + * Emits the class header, the single {@code (RubyProc)} constructor, and one bridge method + * per abstract method collected from the interface. Equivalent to: + *

{@code
+     *   public final class 
+     *           extends BlockInterfaceTemplate
+     *           implements  { ... }
+     * }
+ */ + private static Class defineImplClass(final ClassDefiningClassLoader loader, + final Class interfaceType, + final String implClassName) { + final ClassWriter cw = ASM.newClassWriter((ClassLoader) loader); + final String pathName = implClassName.replace('.', '/'); + + cw.visit(V11, + ACC_PUBLIC | ACC_SUPER | ACC_SYNTHETIC, + pathName, + null, + p(BlockInterfaceTemplate.class), + new String[] { p(interfaceType) }); + cw.visitSource(pathName + ".gen", null); + + defineConstructor(cw); + Method implMethod = Java.getFunctionalInterfaceMethod(interfaceType); + if (implMethod != null) defineBridgeMethod(cw, implMethod); + + cw.visitEnd(); + + final byte[] bytecode = cw.toByteArray(); + return loader.defineClass(implClassName, bytecode); + } + + /** + *
{@code
+     *   public (RubyProc proc) { super(proc); }
+     * }
+ */ + private static void defineConstructor(final ClassWriter cw) { + final SkinnyMethodAdapter init = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "", + sig(void.class, RubyProc.class), null, null); + init.start(); + init.aload(0); + init.aload(1); + init.invokespecial(p(BlockInterfaceTemplate.class), "", sig(void.class, RubyProc.class)); + init.voidreturn(); + init.end(); + } + + /** + * Emits a bridge method that calls {@code __ruby_call(returnType, rubyArgs...)} on the inherited + * {@link BlockInterfaceTemplate} and unboxes/casts the result to interface method's declared return type. + * + *

For a one-arg method {@code R accept(A a)}: + *

{@code
+     *   public R accept(A a) {
+     *       IRubyObject result = __ruby_call(, coerce(a));
+     *       return (R) result.toJava();
+     *   }
+     * }
+ */ + private static void defineBridgeMethod(final ClassWriter cw, final Method method) { + final Class returnType = method.getReturnType(); + final Class[] paramTypes = method.getParameterTypes(); + final SkinnyMethodAdapter mv = new SkinnyMethodAdapter(cw, ACC_PUBLIC, method.getName(), + sig(returnType, paramTypes), null, null); + + final int rubyIndex = nextLocalIndex(paramTypes); + + mv.start(); + if (paramTypes.length > 0) { + mv.aload(0); + mv.getfield(p(BlockInterfaceTemplate.class), "runtime", ci(Ruby.class)); + mv.astore(rubyIndex); + } + + // `this.__ruby_call(...)` - push receiver + the return-type class literal for every arity + mv.aload(0); + pushClassLiteral(mv, returnType); + + switch (paramTypes.length) { + case 0: // __ruby_call() + mv.invokevirtual(p(BlockInterfaceTemplate.class), "__ruby_call", sig(IRubyObject.class, Class.class)); + break; + case 1: // __ruby_call(, coerce(arg0)) + RealClassGenerator.coerceArgumentToRuby(mv, paramTypes[0], 1, rubyIndex); + mv.invokevirtual(p(BlockInterfaceTemplate.class), "__ruby_call", sig(IRubyObject.class, Class.class, IRubyObject.class)); + break; + case 2: // __ruby_call(, coerce(arg0), coerce(arg1)) + int argIndex = RealClassGenerator.coerceArgumentToRuby(mv, paramTypes[0], 1, rubyIndex); + RealClassGenerator.coerceArgumentToRuby(mv, paramTypes[1], argIndex, rubyIndex); + mv.invokevirtual(p(BlockInterfaceTemplate.class), "__ruby_call", sig(IRubyObject.class, Class.class, IRubyObject.class, IRubyObject.class)); + break; + default: // __ruby_call(, new IRubyObject[] { coerce(arg0), coerce(arg1), ... }) + RealClassGenerator.coerceArgumentsToRuby(mv, paramTypes, rubyIndex); + mv.invokevirtual(p(BlockInterfaceTemplate.class), "__ruby_call", sig(IRubyObject.class, Class.class, IRubyObject[].class)); + break; + } + + emitReturn(mv, returnType); + mv.end(); + } + + private static int nextLocalIndex(Class[] paramTypes) { + int index = 1; + for (Class paramType : paramTypes) { + index += RealClassGenerator.paramSlotSize(paramType); + } + return index; + } + + /** + * Pushes the {@code Class} literal for {@code type} on the stack. + * + *
    + *
  • primitive (non-void) → {@code Integer.TYPE}, {@code Long.TYPE}, ... + *
  • {@code void} → {@code Void.TYPE} + *
  • reference → {@code ldc .class} + *
+ */ + private static void pushClassLiteral(SkinnyMethodAdapter mv, Class type) { + if (type.isPrimitive()) { + if (type == void.class) { + mv.getstatic(p(Void.class), "TYPE", ci(Class.class)); + } else { + mv.getstatic(p(getBoxType(type)), "TYPE", ci(Class.class)); + } + } else { + mv.ldc(Type.getType(type)); + } + } + + private static void emitReturn(SkinnyMethodAdapter mv, Class returnType) { + if (returnType == void.class) { + mv.pop(); + mv.voidreturn(); + } else { + RealClassGenerator.coerceResultAndReturn(mv, returnType); + } + } +} diff --git a/core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java b/core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java index e3d01c77014..bc7b0dd7ce2 100644 --- a/core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java +++ b/core/src/main/java/org/jruby/java/codegen/RealClassGenerator.java @@ -56,7 +56,6 @@ import java.util.Set; import org.jruby.Ruby; -import org.jruby.RubyArray; import org.jruby.RubyBasicObject; import org.jruby.RubyClass; import org.jruby.RubyClass.ConcreteJavaReifier; @@ -747,39 +746,12 @@ public static void coerceArgumentsToRuby(SkinnyMethodAdapter mv, Class[] paramTy mv.pushInt(paramTypes.length); mv.anewarray(p(IRubyObject.class)); + int argIndex = 1; // TODO: make this do specific-arity calling - for (int i = 0, argIndex = 1; i < paramTypes.length; i++) { - Class paramType = paramTypes[i]; + for (int i = 0; i < paramTypes.length; i++) { mv.dup(); mv.pushInt(i); - // convert to IRubyObject - if (paramTypes[i].isPrimitive()) { - mv.aload(rubyIndex); - if (paramType == byte.class || paramType == short.class || paramType == char.class || paramType == int.class) { - mv.iload(argIndex++); - mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, int.class)); - } else if (paramType == long.class) { - mv.lload(argIndex); - argIndex += 2; // up two slots, for long's two halves - mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, long.class)); - } else if (paramType == float.class) { - mv.fload(argIndex++); - mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, float.class)); - } else if (paramType == double.class) { - mv.dload(argIndex); - argIndex += 2; // up two slots, for long's two halves - mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, double.class)); - } else if (paramType == boolean.class) { - mv.iload(argIndex++); - mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, boolean.class)); - } - } else if (!IRubyObject.class.isAssignableFrom(paramType)) { - mv.aload(rubyIndex); - mv.aload(argIndex++); - mv.invokestatic(p(JavaUtil.class), "convertJavaToUsableRubyObject", sig(IRubyObject.class, Ruby.class, Object.class)); - } else { - mv.aload(argIndex++); - } + argIndex = coerceArgumentToRuby(mv, paramTypes[i], argIndex, rubyIndex); mv.aastore(); } } else { @@ -787,6 +759,42 @@ public static void coerceArgumentsToRuby(SkinnyMethodAdapter mv, Class[] paramTy } } + public static int coerceArgumentToRuby(SkinnyMethodAdapter mv, Class paramType, int argIndex, int rubyIndex) { + if (paramType.isPrimitive()) { + mv.aload(rubyIndex); + if (paramType == byte.class || paramType == short.class || paramType == char.class || paramType == int.class) { + mv.iload(argIndex); + mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, int.class)); + } else if (paramType == long.class) { + mv.lload(argIndex); + mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, long.class)); + } else if (paramType == float.class) { + mv.fload(argIndex); + mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, float.class)); + } else if (paramType == double.class) { + mv.dload(argIndex); + mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, double.class)); + } else if (paramType == boolean.class) { + mv.iload(argIndex); + mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, boolean.class)); + } + argIndex += paramSlotSize(paramType); + } else if (!IRubyObject.class.isAssignableFrom(paramType)) { + mv.aload(rubyIndex); + mv.aload(argIndex++); + mv.invokestatic(p(JavaUtil.class), "convertJavaToUsableRubyObject", sig(IRubyObject.class, Ruby.class, Object.class)); + } else { + mv.aload(argIndex++); + } + + return argIndex; + } + + public static int paramSlotSize(final Class paramType) { + // two slots, for double's and long's two halves + return paramType == long.class || paramType == double.class ? 2 : 1; + } + public static void coerceResultAndReturn(SkinnyMethodAdapter mv, Class returnType) { coerceResult(mv, returnType, true); } @@ -839,8 +847,7 @@ public static void coerceResult(SkinnyMethodAdapter mv, Class returnType, boolea // if the return type is not an IRubyObject implementer, coerce to that type before casting if (!IRubyObject.class.isAssignableFrom(returnType)) { mv.ldc(Type.getType(returnType)); - mv.invokeinterface( - p(IRubyObject.class), "toJava", sig(Object.class, Class.class)); + mv.invokeinterface(p(IRubyObject.class), "toJava", sig(Object.class, Class.class)); } mv.checkcast(p(returnType)); if (doReturn) mv.areturn(); diff --git a/core/src/main/java/org/jruby/java/invokers/ConstructorInvoker.java b/core/src/main/java/org/jruby/java/invokers/ConstructorInvoker.java index b7a6874018c..98df3a13310 100644 --- a/core/src/main/java/org/jruby/java/invokers/ConstructorInvoker.java +++ b/core/src/main/java/org/jruby/java/invokers/ConstructorInvoker.java @@ -123,7 +123,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz final int len = args.length; - IRubyObject[] newArgs = ArraySupport.newCopy(args, RubyProc.newProc(context.runtime, block, block.type)); + IRubyObject[] newArgs = ArraySupport.newCopy(args, Java.newBlockToInterfaceProc(context.runtime, block)); JavaConstructor constructor = (JavaConstructor) findCallable(self, name, newArgs, len + 1); final Class[] paramTypes = constructor.getParameterTypes(); @@ -144,7 +144,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz if (block.isGiven()) { JavaProxy proxy = castJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaConstructor constructor = (JavaConstructor) findCallableArityOne(self, name, proc); final Class[] paramTypes = constructor.getParameterTypes(); Object cArg0 = proc.toJava(paramTypes[0]); @@ -161,7 +161,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz if (block.isGiven()) { JavaProxy proxy = castJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaConstructor constructor = (JavaConstructor) findCallableArityTwo(self, name, arg0, proc); final Class[] paramTypes = constructor.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -179,7 +179,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz if (block.isGiven()) { JavaProxy proxy = castJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaConstructor constructor = (JavaConstructor) findCallableArityThree(self, name, arg0, arg1, proc); final Class[] paramTypes = constructor.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -198,7 +198,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz if (block.isGiven()) { JavaProxy proxy = castJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaConstructor constructor = (JavaConstructor) findCallableArityFour(self, name, arg0, arg1, arg2, proc); final Class[] paramTypes = constructor.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); diff --git a/core/src/main/java/org/jruby/java/invokers/InstanceMethodInvoker.java b/core/src/main/java/org/jruby/java/invokers/InstanceMethodInvoker.java index 26836f543e5..149a92dd7b7 100644 --- a/core/src/main/java/org/jruby/java/invokers/InstanceMethodInvoker.java +++ b/core/src/main/java/org/jruby/java/invokers/InstanceMethodInvoker.java @@ -6,6 +6,7 @@ import org.jruby.RubyModule; import org.jruby.RubyProc; import org.jruby.java.proxies.JavaProxy; +import org.jruby.javasupport.Java; import org.jruby.javasupport.JavaMethod; import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; @@ -71,7 +72,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz Object target = unwrapIfJavaProxy(self); final int len = args.length; // these extra arrays are really unfortunate; split some of these paths out to eliminate? - IRubyObject[] newArgs = ArraySupport.newCopy(args, RubyProc.newProc(context.runtime, block, block.type)); + IRubyObject[] newArgs = ArraySupport.newCopy(args, Java.newBlockToInterfaceProc(context.runtime, block)); JavaMethod method = (JavaMethod) findCallable(self, name, newArgs, len + 1); final Class[] paramTypes = method.getParameterTypes(); @@ -90,7 +91,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, Block block) { if (block.isGiven()) { Object target = unwrapIfJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityOne(self, name, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = proc.toJava(paramTypes[0]); @@ -103,7 +104,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, Block block) { if (block.isGiven()) { Object target = unwrapIfJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityTwo(self, name, arg0, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -117,7 +118,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) { if (block.isGiven()) { Object target = unwrapIfJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityThree(self, name, arg0, arg1, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -132,7 +133,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { if (block.isGiven()) { Object target = unwrapIfJavaProxy(self); - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod)findCallableArityFour(self, name, arg0, arg1, arg2, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); diff --git a/core/src/main/java/org/jruby/java/invokers/SingletonMethodInvoker.java b/core/src/main/java/org/jruby/java/invokers/SingletonMethodInvoker.java index 6bd43026cf0..ffabf8a8d28 100644 --- a/core/src/main/java/org/jruby/java/invokers/SingletonMethodInvoker.java +++ b/core/src/main/java/org/jruby/java/invokers/SingletonMethodInvoker.java @@ -7,6 +7,7 @@ import org.jruby.RubyClass; import org.jruby.RubyModule; import org.jruby.RubyProc; +import org.jruby.javasupport.Java; import org.jruby.javasupport.JavaMethod; import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; @@ -77,7 +78,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz Object[] convertedArgs = new Object[len + 1]; IRubyObject[] intermediate = new IRubyObject[len + 1]; System.arraycopy(args, 0, intermediate, 0, len); - intermediate[len] = RubyProc.newProc(context.runtime, block, block.type); + intermediate[len] = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallable(self, name, intermediate, len + 1); final Class[] paramTypes = method.getParameterTypes(); @@ -93,7 +94,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityOne(self, name, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = proc.toJava(paramTypes[0]); @@ -106,7 +107,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityTwo(self, name, arg0, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -120,7 +121,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityThree(self, name, arg0, arg1, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -135,7 +136,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityFour(self, name, arg0, arg1, arg2, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); diff --git a/core/src/main/java/org/jruby/java/invokers/StaticMethodInvoker.java b/core/src/main/java/org/jruby/java/invokers/StaticMethodInvoker.java index 80800af266a..d07153869f4 100644 --- a/core/src/main/java/org/jruby/java/invokers/StaticMethodInvoker.java +++ b/core/src/main/java/org/jruby/java/invokers/StaticMethodInvoker.java @@ -5,6 +5,7 @@ import org.jruby.RubyModule; import org.jruby.RubyProc; +import org.jruby.javasupport.Java; import org.jruby.javasupport.JavaMethod; import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; @@ -70,7 +71,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz if (block.isGiven()) { final int len = args.length; // too much array creation! - IRubyObject[] newArgs = ArraySupport.newCopy(args, RubyProc.newProc(context.runtime, block, block.type)); + IRubyObject[] newArgs = ArraySupport.newCopy(args, Java.newBlockToInterfaceProc(context.runtime, block)); JavaMethod method = (JavaMethod) findCallable(self, name, newArgs, len + 1); final Class[] paramTypes = method.getParameterTypes(); @@ -88,7 +89,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityOne(self, name, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = proc.toJava(paramTypes[0]); @@ -101,7 +102,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityTwo(self, name, arg0, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -115,7 +116,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityThree(self, name, arg0, arg1, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); @@ -131,7 +132,7 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz @Override public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, Block block) { if (block.isGiven()) { - RubyProc proc = RubyProc.newProc(context.runtime, block, block.type); + RubyProc proc = Java.newBlockToInterfaceProc(context.runtime, block); JavaMethod method = (JavaMethod) findCallableArityFour(self, name, arg0, arg1, arg2, proc); final Class[] paramTypes = method.getParameterTypes(); Object cArg0 = arg0.toJava(paramTypes[0]); diff --git a/core/src/main/java/org/jruby/java/proxies/BlockInterfaceTemplate.java b/core/src/main/java/org/jruby/java/proxies/BlockInterfaceTemplate.java new file mode 100644 index 00000000000..eb4a33534a8 --- /dev/null +++ b/core/src/main/java/org/jruby/java/proxies/BlockInterfaceTemplate.java @@ -0,0 +1,45 @@ +package org.jruby.java.proxies; + +import org.jruby.Ruby; +import org.jruby.RubyProc; +import org.jruby.ir.JIT; +import org.jruby.runtime.Block; +import org.jruby.runtime.builtin.IRubyObject; + +/** + * @see org.jruby.java.codegen.BlockInterfaceGenerator + */ +public abstract class BlockInterfaceTemplate { + public final Ruby runtime; + private final Block block; + + public BlockInterfaceTemplate(final RubyProc proc) { + assert proc != null; + this.runtime = proc.getRuntime(); + this.block = proc.getBlock(); + } + + @JIT + @SuppressWarnings("unused") + protected final IRubyObject __ruby_call(final Class returnType) { + return block.call(runtime.getCurrentContext()); + } + + @JIT + @SuppressWarnings("unused") + protected final IRubyObject __ruby_call(final Class returnType, IRubyObject arg0) { + return block.call(runtime.getCurrentContext(), arg0); + } + + @JIT + @SuppressWarnings("unused") + protected final IRubyObject __ruby_call(final Class returnType, IRubyObject arg0, IRubyObject arg1) { + return block.call(runtime.getCurrentContext(), arg0, arg1); + } + + @JIT + @SuppressWarnings("unused") + protected final IRubyObject __ruby_call(final Class returnType, IRubyObject[] args) { + return block.call(runtime.getCurrentContext(), args); + } +} diff --git a/core/src/main/java/org/jruby/javasupport/Java.java b/core/src/main/java/org/jruby/javasupport/Java.java index d75414cca1c..de022abe3d8 100644 --- a/core/src/main/java/org/jruby/javasupport/Java.java +++ b/core/src/main/java/org/jruby/javasupport/Java.java @@ -61,6 +61,7 @@ import org.jruby.api.Access; import org.jruby.exceptions.NameError; import org.jruby.exceptions.TypeError; +import org.jruby.java.proxies.BlockInterfaceTemplate; import org.jruby.javasupport.binding.Initializer; import org.jruby.javasupport.proxy.JavaProxyClass; import org.jruby.javasupport.proxy.JavaProxyConstructor; @@ -83,6 +84,7 @@ import org.jruby.java.addons.IOJavaAddons; import org.jruby.java.addons.KernelJavaAddons; import org.jruby.java.addons.StringJavaAddons; +import org.jruby.java.codegen.BlockInterfaceGenerator; import org.jruby.java.codegen.RealClassGenerator; import org.jruby.java.dispatch.CallableSelector; import org.jruby.java.dispatch.CallableSelector.CallableCache; @@ -1481,9 +1483,6 @@ public static Object newInterfaceImpl(ThreadContext context, final IRubyObject w Constructor proxyConstructor = proxyImplClass.getConstructor(IRubyObject.class); return proxyConstructor.newInstance(wrapper); } - catch (InvocationTargetException e) { - throw mapGeneratedProxyException(context.runtime, e); - } catch (ReflectiveOperationException e) { throw mapGeneratedProxyException(context.runtime, e); } @@ -1494,6 +1493,45 @@ private static Object newProxyInterfaceImpl(final IRubyObject wrapper, final Cla return Proxy.newProxyInstance(loader, interfaces, new InterfaceProxyHandler(wrapper, interfaces)); } + /** + * Fast path for converting a proc to a Java interface proxy, bypassing the singleton class setup. + *

+ * The standard {@link JavaUtil#convertProcToInterface} path creates a singleton class for each proc, includes the + * interface module, and installs method stubs (operations acquires the global {@code hierarchyLock}). + *

+ * + * @apiNote Meant to be used with {@code javaMethod { ... } } block variants implementing a functional interface; + * the proc passed is a mere dummy block wrapped NOT one originating from (or visible to) user .rb code + * @implNote Dispatches directly to {@code block.call()} without any Ruby method lookup. + */ + public static Constructor getBlockToInterfaceConstructor(final Ruby runtime, + final Class targetType) { + try { + return BlockInterfaceGenerator.getConstructor(runtime, targetType); + } catch (ReflectiveOperationException e) { + throw mapGeneratedProxyException(runtime, e); + } + } + + /** + * @see #getBlockToInterfaceConstructor(Ruby, Class) + */ + public static T newBlockToInterfaceInstance(final RubyProc proc, + final Constructor constructor) { + try { + return (T) constructor.newInstance(proc); + } catch (ReflectiveOperationException e) { + throw mapGeneratedProxyException(proc.getRuntime(), e); + } + } + + public static RubyProc newBlockToInterfaceProc(final Ruby runtime, final Block block) { + final var blockProc = block.getProcObject(); + final var proc = new RubyProc(runtime, runtime.getProc(), block, Block.Type.JAVA, false); + block.setProcObject(blockProc); // undo RubyProc: block.setProcObject(this); + return proc; + } + private static final class InterfaceProxyHandler implements InvocationHandler { final IRubyObject wrapper; @@ -1666,22 +1704,15 @@ public static IRubyObject constructProxy(Ruby runtime, Constructor type) { + return (type.isInterface() && getFunctionalInterfaceMethod(type) != null); + } + /** - * @param iface + * @param interfaceType * @return the sole un-implemented method for a functional-style interface or null - *

Note: This method is internal and might be subject to change, do not assume its part of JRuby's API!

+ * @apiNote This method is internal and might be subject to change, do not assume its part of JRuby's API */ - public static Method getFunctionalInterfaceMethod(final Class iface) { - assert iface.isInterface(); + public static Method getFunctionalInterfaceMethod(final Class interfaceType) { + assert interfaceType.isInterface(); + + final Method[] objectMethods = Object.class.getMethods(); + Method single = null; - for ( final Method method : iface.getMethods() ) { + for (final Method method : interfaceType.getMethods()) { final int mod = method.getModifiers(); - if ( Modifier.isStatic(mod) ) continue; - if ( Modifier.isAbstract(mod) ) { - try { // check if it's equals, hashCode etc. : - Object.class.getMethod(method.getName(), method.getParameterTypes()); - continue; // abstract but implemented by java.lang.Object - } - catch (NoSuchMethodException e) { /* fall-through */ } - catch (SecurityException e) { - // NOTE: we could try check for FunctionalInterface on Java 8 - } - } - else continue; // not-abstract ... default method - if ( single == null ) single = method; + if (Modifier.isStatic(mod) || !Modifier.isAbstract(mod)) continue; // skip static and default methods + if (method.getDeclaringClass() == Object.class || + isRedeclaredObjectMethod(objectMethods, method)) continue; // equals, hashCode etc. + + if (single == null) single = method; else return null; // not a functional iface } return single; } + private static boolean isRedeclaredObjectMethod(final Method[] objectMethods, final Method method) { + for (int i = 0; i < objectMethods.length; i++) { + final Method objectMethod = objectMethods[i]; + if (objectMethod.getName().equals(method.getName()) && + Arrays.equals(objectMethod.getParameterTypes(), method.getParameterTypes())) return true; + } + return false; + } + /** * Try to set the given member to be accessible, considering open modules and avoiding the actual setAccessible * call when it would produce a JPMS warning. All classes on Java 8 are considered open, allowing setAccessible diff --git a/core/src/main/java/org/jruby/runtime/Block.java b/core/src/main/java/org/jruby/runtime/Block.java index c5b1e4626c0..ff0432c1de5 100644 --- a/core/src/main/java/org/jruby/runtime/Block.java +++ b/core/src/main/java/org/jruby/runtime/Block.java @@ -59,7 +59,11 @@ */ public class Block implements FunctionOneOrTwoOrThree, RecursiveFunctionEx { public enum Type { - NORMAL(false), PROC(false), LAMBDA(true), THREAD(false); + NORMAL(false), + PROC(false), + LAMBDA(true), + THREAD(false), + JAVA(false); // special type used with Java block-to-interface conversion Type(boolean checkArity) { this.checkArity = checkArity; diff --git a/core/src/test/java/org/jruby/javasupport/TestJava.java b/core/src/test/java/org/jruby/javasupport/TestJava.java index 44cc884cc79..5907bba6e9b 100644 --- a/core/src/test/java/org/jruby/javasupport/TestJava.java +++ b/core/src/test/java/org/jruby/javasupport/TestJava.java @@ -40,11 +40,10 @@ public void testGetFunctionInterface() { method = Java.getFunctionalInterfaceMethod(java.io.Serializable.class); assertNull(method); - //if ( Java.JAVA8 ) { // compare and equals both abstract + // compare and equals both abstract method = Java.getFunctionalInterfaceMethod(java.util.Comparator.class); assertNotNull(method); assertEquals("compare", method.getName()); - //} method = Java.getFunctionalInterfaceMethod(java.lang.Comparable.class); assertNotNull(method); diff --git a/spec/java_integration/fixtures/iface/SingleMethodInterfaceWith3Args.java b/spec/java_integration/fixtures/iface/SingleMethodInterfaceWith3Args.java new file mode 100644 index 00000000000..fb2694c5252 --- /dev/null +++ b/spec/java_integration/fixtures/iface/SingleMethodInterfaceWith3Args.java @@ -0,0 +1,11 @@ +package java_integration.fixtures.iface; + +public interface SingleMethodInterfaceWith3Args { + public static class Caller { + public static Object call(SingleMethodInterfaceWith3Args iface) { + return iface.doIt("alpha", 1, true); + } + } + + Object doIt(final T arg1, Integer arg2, Boolean arg3); +} diff --git a/spec/java_integration/interfaces/implementation_spec.rb b/spec/java_integration/interfaces/implementation_spec.rb index 53a28200898..cb451a119a6 100644 --- a/spec/java_integration/interfaces/implementation_spec.rb +++ b/spec/java_integration/interfaces/implementation_spec.rb @@ -364,6 +364,281 @@ def pr.another_method; end end end +describe "Ruby block passed as a Java functional-interface" do + it "supports arity 0" do # SingleMethodInterface.callIt() + # Already exercised broadly in 'can be coerced from a block ...' tests. + expect(UsesSingleMethodInterface.callIt { 'zero' }).to eq('zero') + end + + it "supports arity 1" do + Java::java_integration.fixtures.iface.SingleMethodInterfaceWithArg::Caller.call do |arg| + expect(arg).to eq 42 + end + Java::java_integration.fixtures.iface.SingleMethodInterfaceWithArg::Caller.call('x') do |arg| + expect(arg).to eq 'x' + end + end + + it "supports arity 2 (java.io.FilenameFilter)" do + seen = [] + Java::java.io.File.new('.').list do |dir, name| + seen << [dir.class, name.class] + false # boolean accept(File, String) + end + expect(seen).not_to be_empty + seen.each { |dir_cls, name_cls| + expect(dir_cls).to eq java.io.File + expect(name_cls).to eq String + } + end + + it "supports arity 3" do # (mixed reference args + reference return) + result = Java::java_integration.fixtures.iface.SingleMethodInterfaceWith3Args::Caller.call do |a, b, c| + expect(a).to eq 'alpha' + expect(b).to eq 1 + expect(c).to eq true + [a, b, c] + end + expect(result).to eq ['alpha', 1, true] + end + + it "supports arity 4" do # (mixed reference + primitive args, Object[] return) + result = Java::java_integration.fixtures.iface.SingleMethodInterfaceWith4Args::Caller.call do |_, b, c, d| + expect(b).to eq 'hello' + expect(c).to eq 'world' + expect(d).to eq 42 + [b, c, d] + end + expect(result.to_a).to eq ['hello', 'world', 42] + end + + it "java.util.function.Consumer via Iterable#forEach (arity 1, void return)" do + seen = [] + list = java.util.ArrayList.new + [1, 2, 3].each { |i| list.add(i) } + list.forEach { |x| seen << x } + expect(seen).to eq [1, 2, 3] + end + + it "java.util.function.BiConsumer via Map#forEach (arity 2, void return)" do + pairs = [] + map = java.util.LinkedHashMap.new + map.put('a', 1); map.put('b', 2) + map.forEach { |k, v| pairs << [k, v] } + expect(pairs).to eq [['a', 1], ['b', 2]] + end + + it "java.util.function.BiFunction via Map#compute (arity 2, reference return)" do + map = java.util.HashMap.new + map.put('count', 1) + result = map.compute('count') { |k, v| v + 10 } + expect(result).to eq 11 + expect(map.get('count')).to eq 11 + end + + it "java.util.function.Function via Map#computeIfAbsent (arity 1, reference return)" do + map = java.util.HashMap.new + result = map.computeIfAbsent('k') { |key| "value-for-#{key}" } + expect(result).to eq 'value-for-k' + expect(map.get('k')).to eq 'value-for-k' + end + + it "java.util.function.Predicate via Collection#removeIf (arity 1, primitive boolean return)" do + list = java.util.ArrayList.new + [1, 2, 3, 4, 5].each { |i| list.add(i) } + list.removeIf { |i| i.even? } + expect(list.to_a).to eq [1, 3, 5] + end + + it "primitive consumers via Optional*#ifPresent (arity 1, primitive argument, void return)" do + seen = [] + java.util.OptionalInt.of(42).ifPresent { |i| seen << i } + java.util.OptionalLong.of(1_234_567_890_123).ifPresent { |l| seen << l } + java.util.OptionalDouble.of(1.25).ifPresent { |d| seen << d } + expect(seen).to eq [42, 1_234_567_890_123, 1.25] + end + + it "java.util.function.IntBinaryOperator via IntStream#reduce (arity 2, primitive arguments)" do + ints = [1, 2, 3].to_java(:int) + sum = java.util.stream.IntStream.of(ints).reduce(0) { |a, b| a + b } + expect(sum).to eq 6 + end + + it "IntSupplier via OptionalInt#orElseGet (arity 0, primitive int return)" do + expect(java.util.OptionalInt.empty.orElseGet { 7 }).to eq 7 + end + + it "LongSupplier via OptionalLong#orElseGet (arity 0, primitive long return)" do + expect(java.util.OptionalLong.empty.orElseGet { 1 << 40 }).to eq(1 << 40) + end + + it "DoubleSupplier via OptionalDouble#orElseGet (arity 0, primitive double return)" do + expect(java.util.OptionalDouble.empty.orElseGet { 3.14 }).to eq 3.14 + end + + it "IntUnaryOperator via IntStream#map (int -> int)" do + ints = [1, 2, 3].to_java(:int) + result = java.util.stream.IntStream.of(ints).map { |i| i * 10 }.toArray.to_a + expect(result).to eq [10, 20, 30] + end + + it "IntPredicate via IntStream#filter (int -> boolean)" do + ints = [1, 2, 3, 4].to_java(:int) + result = java.util.stream.IntStream.of(ints).filter { |i| i.even? }.toArray.to_a + expect(result).to eq [2, 4] + end + + it "IntFunction via IntStream#mapToObj (int -> reference)" do + ints = [1, 2, 3].to_java(:int) + result = java.util.stream.IntStream.of(ints).mapToObj { |i| "n=#{i}" }.toArray.to_a + expect(result).to eq ['n=1', 'n=2', 'n=3'] + end + + it "IntToLongFunction via IntStream#mapToLong (int -> long)" do + ints = [1, 2].to_java(:int) + result = java.util.stream.IntStream.of(ints).mapToLong { |i| i * (1 << 32) }.toArray.to_a + expect(result).to eq [1 * (1 << 32), 2 * (1 << 32)] + end + + it "IntToDoubleFunction via IntStream#mapToDouble (int -> double)" do + ints = [2, 4].to_java(:int) + result = java.util.stream.IntStream.of(ints).mapToDouble { |i| i + 0.5 }.toArray.to_a + expect(result).to eq [2.5, 4.5] + end + + it "LongUnaryOperator via LongStream#map (long -> long)" do + result = java.util.stream.LongStream.of([10, 20].to_java(:long)).map { |l| l + 1 }.toArray.to_a + expect(result).to eq [11, 21] + end + + it "LongPredicate via LongStream#filter (long -> boolean)" do + result = java.util.stream.LongStream.of([1, 2, 3, 4].to_java(:long)).filter { |l| l > 2 }.toArray.to_a + expect(result).to eq [3, 4] + end + + it "LongFunction via LongStream#mapToObj (long -> reference)" do + result = java.util.stream.LongStream.of([100, 200].to_java(:long)).mapToObj { |l| "L#{l}" }.toArray.to_a + expect(result).to eq ['L100', 'L200'] + end + + it "LongToIntFunction via LongStream#mapToInt (long -> int)" do + result = java.util.stream.LongStream.of([1000, 2000].to_java(:long)).mapToInt { |l| (l / 100).to_i }.toArray.to_a + expect(result).to eq [10, 20] + end + + it "LongToDoubleFunction via LongStream#mapToDouble (long -> double)" do + result = java.util.stream.LongStream.of([3, 7].to_java(:long)).mapToDouble { |l| l / 2.0 }.toArray.to_a + expect(result).to eq [1.5, 3.5] + end + + it "DoubleUnaryOperator via DoubleStream#map (double -> double)" do + result = java.util.stream.DoubleStream.of([1.5, 2.5].to_java(:double)).map { |d| d * 2 }.toArray.to_a + expect(result).to eq [3.0, 5.0] + end + + it "DoublePredicate via DoubleStream#filter (double -> boolean)" do + result = java.util.stream.DoubleStream.of([0.5, 1.5, 2.5].to_java(:double)).filter { |d| d > 1.0 }.toArray.to_a + expect(result).to eq [1.5, 2.5] + end + + it "DoubleFunction via DoubleStream#mapToObj (double -> reference)" do + result = java.util.stream.DoubleStream.of([1.5].to_java(:double)).mapToObj { |d| "d=#{d}" }.toArray.to_a + expect(result).to eq ['d=1.5'] + end + + it "DoubleToIntFunction via DoubleStream#mapToInt (double -> int)" do + result = java.util.stream.DoubleStream.of([3.7, 1.2].to_java(:double)).mapToInt { |d| d.floor }.toArray.to_a + expect(result).to eq [3, 1] + end + + it "DoubleToLongFunction via DoubleStream#mapToLong (double -> long)" do + result = java.util.stream.DoubleStream.of([9.9].to_java(:double)).mapToLong { |d| d.floor }.toArray.to_a + expect(result).to eq [9] + end + + it "ToIntFunction via Stream#mapToInt (reference -> int)" do + result = java.util.Arrays.stream(['ab', 'cdef'].to_java(:string)).mapToInt { |s| s.length }.toArray.to_a + expect(result).to eq [2, 4] + end + + it "ToLongFunction via Stream#mapToLong (reference -> long)" do + result = java.util.Arrays.stream(['a', 'bb'].to_java(:string)).mapToLong { |s| s.length }.toArray.to_a + expect(result).to eq [1, 2] + end + + it "ToDoubleFunction via Stream#mapToDouble (reference -> double)" do + result = java.util.Arrays.stream(['abc'].to_java(:string)).mapToDouble { |s| s.length * 1.0 }.toArray.to_a + expect(result).to eq [3.0] + end + + it "LongBinaryOperator via LongStream#reduce (long,long -> long)" do + sum = java.util.stream.LongStream.of([1, 2, 3].to_java(:long)).reduce(0) { |a, b| a + b } + expect(sum).to eq 6 + end + + it "DoubleBinaryOperator via DoubleStream#reduce (double,double -> double)" do + sum = java.util.stream.DoubleStream.of([1.5, 2.5].to_java(:double)).reduce(0.0) { |a, b| a + b } + expect(sum).to eq 4.0 + end + + it "java.util.function.UnaryOperator via List#replaceAll (arity 1, reference return)" do + list = java.util.ArrayList.new + ['a', 'b', 'c'].each { |s| list.add(s) } + list.replaceAll { |s| s.upcase } + expect(list.to_a).to eq ['A', 'B', 'C'] + end + + it "java.util.function.Supplier via Optional#orElseGet (arity 0, reference return)" do + result = java.util.Optional.empty.orElseGet { 'default' } + expect(result).to eq 'default' + result2 = java.util.Optional.of('x').orElseGet { 'default' } + expect(result2).to eq 'x' + end + + it "java.util.Comparator via List#sort (arity 2, primitive int return)" do + list = java.util.ArrayList.new + ['xx', 'a', 'bbb'].each { |s| list.add(s) } + list.sort { |a, b| a.length <=> b.length } + expect(list.to_a).to eq ['a', 'xx', 'bbb'] + end + + it "java.lang.Runnable via Optional#ifPresent — arity-0 Consumer with side-effect" do + ran = [] + java.util.Optional.empty.ifPresent { |_| ran << :should_not_run } + java.util.Optional.of('x').ifPresent { |x| ran << x } + expect(ran).to eq ['x'] + end + + context "across method dispatch" do + + it "static-method dispatch (StaticMethodInvoker)" do + expect(UsesSingleMethodInterface.callIt { 's0' }).to eq('s0') + expect(UsesSingleMethodInterface.callIt(nil) { 's1' }).to eq('s1') + expect(UsesSingleMethodInterface.callIt(nil, nil) { 's2' }).to eq('s2') + expect(UsesSingleMethodInterface.callIt(nil, nil, nil) { 's3' }).to eq('s3') + expect(UsesSingleMethodInterface.callIt(nil, nil, nil, nil) { 's4' }).to eq('s4') + end + + it "instance-method dispatch (InstanceMethodInvoker)" do + receiver = UsesSingleMethodInterface.new + expect(receiver.callIt2 { 'i0' }).to eq('i0') + expect(receiver.callIt2(nil) { 'i1' }).to eq('i1') + expect(receiver.callIt2(nil, nil) { 'i2' }).to eq('i2') + expect(receiver.callIt2(nil, nil, nil) { 'i3' }).to eq('i3') + expect(receiver.callIt2(nil, nil, nil, nil) { 'i4' }).to eq('i4') + end + + it "constructor dispatch (ConstructorInvoker)" do + expect(UsesSingleMethodInterface.new { 'c0' }.result).to eq('c0') + expect(UsesSingleMethodInterface.new(nil) { 'c1' }.result).to eq('c1') + expect(UsesSingleMethodInterface.new(nil, nil) { 'c2' }.result).to eq('c2') + expect(UsesSingleMethodInterface.new(nil, nil, nil) { 'c3' }.result).to eq('c3') + expect(UsesSingleMethodInterface.new(nil, nil, nil, nil) { 'c4' }.result).to eq('c4') + end + + end +end + describe "A bean-like Java interface" do it "allows implementation with attr* methods" do myimpl1 = Class.new do diff --git a/test/jruby/test_instantiating_interfaces.rb b/test/jruby/test_instantiating_interfaces.rb index 7bc204d5187..6fec8b983c5 100644 --- a/test/jruby/test_instantiating_interfaces.rb +++ b/test/jruby/test_instantiating_interfaces.rb @@ -1,9 +1,8 @@ require 'test/unit' +require 'java' class TestInstantiatingInterfaces < Test::Unit::TestCase - require 'java' - class NoRun include java.lang.Runnable end @@ -51,6 +50,305 @@ def cs.char_at; nil end assert_equal ' ', cs.charAt(2) end + # --- arity 0 ----------------------------------------------------------------- + + def test_proc_to_java_functional_interface + proc = proc { @ran = true } + + runnable = proc.to_java(java.lang.Runnable) + runnable.run + + assert_equal true, @ran + assert_same proc, runnable.__ruby_object + end + + def test_proc_to_java_supplier # arity 0, reference return -> __ruby_call(Class) + supplier = proc { 'forty-two' }.to_java(java.util.function.Supplier) + assert_equal 'forty-two', supplier.get + end + + def test_proc_to_java_callable # arity 0, reference return, can throw -> __ruby_call(Class) + callable = proc { 42 }.to_java(java.util.concurrent.Callable) + assert_equal 42, callable.call + end + + def test_proc_to_java_int_supplier # arity 0, primitive return -> __ruby_call(Class) + int_supplier = proc { 7 }.to_java(java.util.function.IntSupplier) + assert_equal 7, int_supplier.getAsInt + end + + def test_proc_to_java_long_supplier # arity 0, primitive long return + long_supplier = proc { 1 << 40 }.to_java(java.util.function.LongSupplier) + assert_equal 1 << 40, long_supplier.getAsLong + end + + def test_proc_to_java_double_supplier # arity 0, primitive double return + dbl_supplier = proc { 3.5 }.to_java(java.util.function.DoubleSupplier) + assert_equal 3.5, dbl_supplier.getAsDouble + end + + def test_proc_to_java_boolean_supplier # arity 0, primitive boolean return + bool_supplier = proc { true }.to_java(java.util.function.BooleanSupplier) + assert_equal true, bool_supplier.getAsBoolean + end + + # --- arity 1 ----------------------------------------------------------------- + + def test_proc_to_java_primitive_functional_interface # int -> int + doubler = proc { |value| value * 2 } + + operator = doubler.to_java(java.util.function.IntUnaryOperator) + + assert_equal 42, operator.applyAsInt(21) + end + + def test_proc_to_java_consumer # arity 1, void -> __ruby_call(Class, Object) + captured = [] + consumer = proc { |x| captured << x }.to_java(java.util.function.Consumer) + consumer.accept('a') + consumer.accept('b') + assert_equal ['a', 'b'], captured + end + + def test_proc_to_java_function # arity 1 reference -> reference + upcase = proc { |s| s.upcase }.to_java(java.util.function.Function) + assert_equal 'HELLO', upcase.apply('hello') + end + + def test_proc_to_java_predicate # arity 1, boolean return + even = proc { |n| n.even? }.to_java(java.util.function.Predicate) + assert_equal true, even.test(4) + assert_equal false, even.test(5) + end + + def test_proc_to_java_unary_operator # arity 1 reference -> reference + rev = proc { |s| s.reverse }.to_java(java.util.function.UnaryOperator) + assert_equal 'olleh', rev.apply('hello') + end + + def test_proc_to_java_to_int_function # arity 1 reference -> primitive int + len = proc { |s| s.length }.to_java(java.util.function.ToIntFunction) + assert_equal 5, len.applyAsInt('hello') + end + + def test_proc_to_java_int_function # arity 1 primitive int -> reference + str = proc { |i| "n=#{i}" }.to_java(java.util.function.IntFunction) + assert_equal 'n=3', str.apply(3) + end + + def test_proc_to_java_int_consumer # arity 1 primitive int -> void + seen = [] + consumer = proc { |i| seen << i }.to_java(java.util.function.IntConsumer) + consumer.accept(1); consumer.accept(2) + assert_equal [1, 2], seen + end + + def test_proc_to_java_int_predicate # arity 1 primitive int -> primitive boolean + pos = proc { |i| i > 0 }.to_java(java.util.function.IntPredicate) + assert_equal true, pos.test(1) + assert_equal false, pos.test(-1) + end + + def test_proc_to_java_long_unary_operator # arity 1 primitive long -> primitive long + plus_one = proc { |l| l + 1 }.to_java(java.util.function.LongUnaryOperator) + assert_equal 1 << 40 | 1 + 1, plus_one.applyAsLong(1 << 40 | 1) + end + + def test_proc_to_java_double_unary_operator # arity 1 primitive double -> primitive double + half = proc { |d| d / 2.0 }.to_java(java.util.function.DoubleUnaryOperator) + assert_equal 1.5, half.applyAsDouble(3.0) + end + + def test_proc_to_java_long_consumer # arity 1 primitive long -> void + seen = [] + consumer = proc { |l| seen << l }.to_java(java.util.function.LongConsumer) + consumer.accept(1 << 40); consumer.accept(-1) + assert_equal [1 << 40, -1], seen + end + + def test_proc_to_java_double_consumer # arity 1 primitive double -> void + seen = [] + consumer = proc { |d| seen << d }.to_java(java.util.function.DoubleConsumer) + consumer.accept(1.5); consumer.accept(-0.25) + assert_equal [1.5, -0.25], seen + end + + def test_proc_to_java_long_function # arity 1 primitive long -> reference + fn = proc { |l| "L=#{l}" }.to_java(java.util.function.LongFunction) + assert_equal 'L=99', fn.apply(99) + end + + def test_proc_to_java_double_function # arity 1 primitive double -> reference + fn = proc { |d| "D=#{d}" }.to_java(java.util.function.DoubleFunction) + assert_equal 'D=2.5', fn.apply(2.5) + end + + def test_proc_to_java_long_predicate # arity 1 primitive long -> primitive boolean + pos = proc { |l| l > 0 }.to_java(java.util.function.LongPredicate) + assert_equal true, pos.test(1) + assert_equal false, pos.test(-1) + end + + def test_proc_to_java_double_predicate # arity 1 primitive double -> primitive boolean + pos = proc { |d| d > 0 }.to_java(java.util.function.DoublePredicate) + assert_equal true, pos.test(0.1) + assert_equal false, pos.test(-0.1) + end + def test_proc_to_java_int_to_long_function # arity 1 primitive int -> primitive long + fn = proc { |i| i * (1 << 32) }.to_java(java.util.function.IntToLongFunction) + assert_equal 3 * (1 << 32), fn.applyAsLong(3) + end + + def test_proc_to_java_int_to_double_function # arity 1 primitive int -> primitive double + fn = proc { |i| i + 0.5 }.to_java(java.util.function.IntToDoubleFunction) + assert_equal 3.5, fn.applyAsDouble(3) + end + + def test_proc_to_java_long_to_int_function # arity 1 primitive long -> primitive int + fn = proc { |l| (l & 0xFF).to_i }.to_java(java.util.function.LongToIntFunction) + assert_equal 0xAB, fn.applyAsInt(0x7FFF_FFFF_FFFF_FFAB) + end + + def test_proc_to_java_long_to_double_function # arity 1 primitive long -> primitive double + fn = proc { |l| l.to_f / 2 }.to_java(java.util.function.LongToDoubleFunction) + assert_equal 5.0, fn.applyAsDouble(10) + end + + def test_proc_to_java_double_to_int_function # arity 1 primitive double -> primitive int + fn = proc { |d| d.floor }.to_java(java.util.function.DoubleToIntFunction) + assert_equal 3, fn.applyAsInt(3.7) + end + + def test_proc_to_java_double_to_long_function # arity 1 primitive double -> primitive long + fn = proc { |d| d.floor }.to_java(java.util.function.DoubleToLongFunction) + assert_equal 3, fn.applyAsLong(3.7) + end + + # --- arity 2 ----------------------------------------------------------------- + + def test_proc_to_java_two_arg_functional_interface + joiner = proc { |a, b| "#{a}+#{b}" } + + bi = joiner.to_java(java.util.function.BiFunction) + + assert_equal 'x+y', bi.apply('x', 'y') + end + + def test_proc_to_java_bi_consumer # arity 2, void -> __ruby_call(Class, Object, Object) + pairs = [] + bi = proc { |a, b| pairs << [a, b] }.to_java(java.util.function.BiConsumer) + bi.accept(:k, 1); bi.accept(:k2, 2) + assert_equal [[:k, 1], [:k2, 2]], pairs + end + + def test_proc_to_java_bi_predicate # arity 2, boolean return + pred = proc { |a, b| a == b }.to_java(java.util.function.BiPredicate) + assert_equal true, pred.test('x', 'x') + assert_equal false, pred.test('x', 'y') + end + + def test_proc_to_java_binary_operator # arity 2, generic same-type return + sum = proc { |a, b| a + b }.to_java(java.util.function.BinaryOperator) + assert_equal 'ab', sum.apply('a', 'b') + end + + def test_proc_to_java_int_binary_operator # arity 2, primitive int + add = proc { |a, b| a + b }.to_java(java.util.function.IntBinaryOperator) + assert_equal 5, add.applyAsInt(2, 3) + end + + def test_proc_to_java_double_binary_operator # arity 2, primitive double + mul = proc { |a, b| a * b }.to_java(java.util.function.DoubleBinaryOperator) + assert_equal 6.0, mul.applyAsDouble(2.0, 3.0) + end + + def test_proc_to_java_long_binary_operator # arity 2, primitive long args -> primitive long + add = proc { |a, b| a + b }.to_java(java.util.function.LongBinaryOperator) + assert_equal (1 << 40) + 1, add.applyAsLong(1 << 40, 1) + end + + def test_proc_to_java_to_int_bi_function # arity 2, reference args -> primitive int + bi = proc { |s, prefix| s.start_with?(prefix) ? 1 : 0 }.to_java(java.util.function.ToIntBiFunction) + assert_equal 1, bi.applyAsInt('hello world', 'hello') + assert_equal 0, bi.applyAsInt('hello world', 'bye') + end + + def test_proc_to_java_to_long_bi_function # arity 2, reference args -> primitive long + bi = proc { |s, n| s.length + n }.to_java(java.util.function.ToLongBiFunction) + assert_equal 8, bi.applyAsLong('hello', 3) + end + + def test_proc_to_java_to_double_bi_function # arity 2, reference args -> primitive double + bi = proc { |s, n| s.length * n }.to_java(java.util.function.ToDoubleBiFunction) + assert_equal 12.5, bi.applyAsDouble('hello', 2.5) + end + + def test_proc_to_java_obj_int_consumer # arity 2, (reference, primitive int) -> void + seen = [] + consumer = proc { |s, i| seen << [s, i] }.to_java(java.util.function.ObjIntConsumer) + consumer.accept('a', 1); consumer.accept('b', 2) + assert_equal [['a', 1], ['b', 2]], seen + end + + def test_proc_to_java_obj_long_consumer # arity 2, (reference, primitive long) -> void + seen = [] + consumer = proc { |s, l| seen << [s, l] }.to_java(java.util.function.ObjLongConsumer) + consumer.accept('x', 1 << 40) + assert_equal [['x', 1 << 40]], seen + end + + def test_proc_to_java_obj_double_consumer # arity 2, (reference, primitive double) -> void + seen = [] + consumer = proc { |s, d| seen << [s, d] }.to_java(java.util.function.ObjDoubleConsumer) + consumer.accept('pi', 3.14) + assert_equal [['pi', 3.14]], seen + end + + def test_proc_to_java_comparator # arity 2 -> primitive int + cmp = proc { |a, b| a.length <=> b.length }.to_java(java.util.Comparator) + list = java.util.ArrayList.new + list.add('aaa'); list.add('a'); list.add('aa') + java.util.Collections.sort(list, cmp) + assert_equal ['a', 'aa', 'aaa'], list.to_a + end + + def test_proc_to_java_void_return_ignores_block_value + # block returns a non-nil value; Runnable#run is void so the value is discarded. + runnable = proc { 42 }.to_java(java.lang.Runnable) + assert_nil runnable.run + end + + def test_proc_to_java_primitive_return_unboxing + # block returns a Ruby Integer; the generated bridge must unbox to primitive int. + op = proc { |s| s.length * 10 }.to_java(java.util.function.ToIntFunction) + assert_equal 50, op.applyAsInt('hello') + end + + def test_proc_to_java_internal__ruby_object + p0 = proc { 1 } + p1 = proc { |a| a } + p2 = proc { |a, b| [a, b] } + p3 = proc { |a, b, c| [a, b, c] } + + assert_same p0, p0.to_java(java.util.function.Supplier).__ruby_object + assert_same p1, p1.to_java(java.util.function.Function).__ruby_object + assert_same p2, p2.to_java(java.util.function.BiFunction).__ruby_object + assert_same p3, p3.to_java(java.lang.reflect.InvocationHandler).__ruby_object + end + + def test_lambda_to_java_functional_interface + fn = lambda { |a, b| a + b } + bi = fn.to_java(java.util.function.BiFunction) + assert_equal 5, bi.apply(2, 3) + assert_same fn, bi.__ruby_object + end + + def test_proc_to_java_repeated_invocation_is_stable + counter = java.util.concurrent.atomic.AtomicInteger.new(0) + consumer = proc { |_| counter.incrementAndGet }.to_java(java.util.function.Consumer) + 100.times { consumer.accept('x') } + assert_equal 100, counter.get + end end