diff --git a/src/IKVM.Java/local/ikvm/runtime/Util.java b/src/IKVM.Java/local/ikvm/runtime/Util.java index 4654b26052..fb7dc90c12 100644 --- a/src/IKVM.Java/local/ikvm/runtime/Util.java +++ b/src/IKVM.Java/local/ikvm/runtime/Util.java @@ -23,8 +23,14 @@ */ package ikvm.runtime; +import cli.System.Delegate; import cli.System.Type; import cli.System.RuntimeTypeHandle; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; import sun.misc.Unsafe; public final class Util @@ -48,8 +54,33 @@ private Util() public static native Type getRuntimeTypeFromClass(Class classObject); + /** + * Creates a delegate of the given type that invokes the given method handle. The handle is adapted to the + * signature of the delegate's Invoke method, which performs any conversions required; a handle that cannot be + * adapted results in a {@link java.lang.invoke.WrongMethodTypeException}. + */ + public static native Delegate getDelegateFromMethodHandle(Type delegateType, MethodHandle methodHandle); + + /** + * Creates a delegate of the given type that invokes the given method or constructor. Access is checked as it is + * for {@link MethodHandles.Lookup#unreflect}; call {@code setAccessible(true)} on the member first in order to + * create a delegate for an otherwise inaccessible one. + */ + public static Delegate getDelegateFromMethod(Type delegateType, Executable executable) throws IllegalAccessException + { + if (executable == null) + throw new NullPointerException("executable"); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle methodHandle = executable instanceof Constructor + ? lookup.unreflectConstructor((Constructor)executable) + : lookup.unreflect((Method)executable); + + return getDelegateFromMethodHandle(delegateType, methodHandle); + } + public static native Throwable mapException(Throwable x); - + public static native Throwable unmapException(Throwable x); public static void throwException(cli.System.Exception x) diff --git a/src/IKVM.Runtime/Java/Externs/ikvm/runtime/Util.cs b/src/IKVM.Runtime/Java/Externs/ikvm/runtime/Util.cs index 1fbc79ad5c..fa74c7bf27 100644 --- a/src/IKVM.Runtime/Java/Externs/ikvm/runtime/Util.cs +++ b/src/IKVM.Runtime/Java/Externs/ikvm/runtime/Util.cs @@ -185,6 +185,46 @@ public static Type getRuntimeTypeFromClass(global::java.lang.Class classObject) return wrapper.TypeAsBaseType; } + /// + /// Creates a delegate of the given type that invokes the given method handle. The handle is adapted to the + /// signature of the delegate's Invoke method, which performs any conversions required (boxing, primitive + /// widening, ghost wrapping, receiver binding); an incompatible handle results in a + /// . + /// + /// + /// + /// + public static Delegate getDelegateFromMethodHandle(Type delegateType, global::java.lang.invoke.MethodHandle methodHandle) + { +#if FIRST_PASS + throw new NotImplementedException(); +#else + if (delegateType == null) + throw new global::java.lang.NullPointerException("delegateType"); + if (methodHandle == null) + throw new global::java.lang.NullPointerException("methodHandle"); + + var invoke = delegateType.BaseType == typeof(MulticastDelegate) ? delegateType.GetMethod("Invoke") : null; + if (invoke == null) + throw new global::java.lang.IllegalArgumentException(delegateType.FullName + " is not a delegate type."); + + foreach (var parameter in invoke.GetParameters()) + if (parameter.ParameterType.IsByRef || parameter.ParameterType.IsPointer) + throw new global::java.lang.IllegalArgumentException(delegateType.FullName + " has a by-ref or pointer parameter."); + + // adapt the handle to the delegate's own signature; this is what performs the conversions, and is also + // what rejects a handle that cannot be called through this delegate + var methodType = JVM.Context.MethodHandleUtil.GetDelegateMethodType(delegateType); + methodHandle = methodHandle.asType(methodType).asFixedArity(); + + // the adapted handle is materialized as its canonical MH/MHV delegate, which by construction has exactly + // the signature of the requested delegate; binding its Invoke as the target closes over it + var inner = JVM.Context.MethodHandleUtil.GetDelegateForInvokeExact(methodHandle); + return Delegate.CreateDelegate(delegateType, inner, inner.GetType().GetMethod("Invoke"), false) ?? + throw new global::java.lang.IllegalArgumentException("Cannot create a " + delegateType.FullName + " for a method handle of type " + methodType + "."); +#endif + } + [HideFromJava] public static Exception mapException(Exception e) { diff --git a/src/IKVM.Runtime/MethodHandleUtil.jniexport.cs b/src/IKVM.Runtime/MethodHandleUtil.jniexport.cs index 0d4e614310..06bb39af2e 100644 --- a/src/IKVM.Runtime/MethodHandleUtil.jniexport.cs +++ b/src/IKVM.Runtime/MethodHandleUtil.jniexport.cs @@ -633,20 +633,32 @@ internal Type GetDelegateTypeForInvokeExact(global::java.lang.invoke.MethodType return type._invokeExactDelegateType; } - internal T GetDelegateForInvokeExact(global::java.lang.invoke.MethodHandle mh) - where T : class, Delegate + /// + /// Gets the canonical delegate that invokes the given method handle exactly. The type of the returned delegate + /// is the or instantiation that corresponds to the handle's method type, as + /// returned by . + /// + /// + /// + internal Delegate GetDelegateForInvokeExact(global::java.lang.invoke.MethodHandle mh) { var type = mh.type(); if (mh._invokeExactDelegate == null) { type._invokeExactDynamicMethod ??= DynamicMethodBuilder.CreateInvokeExact(context, type); mh._invokeExactDelegate = type._invokeExactDynamicMethod.CreateDelegate(GetDelegateTypeForInvokeExact(type), mh); - var del = mh._invokeExactDelegate as T; - if (del != null) - return del; } - throw java.lang.invoke.Invokers.newWrongMethodTypeException(GetDelegateMethodType(typeof(T)), type); + return (Delegate)mh._invokeExactDelegate; + } + + internal T GetDelegateForInvokeExact(global::java.lang.invoke.MethodHandle mh) + where T : class, Delegate + { + if (GetDelegateForInvokeExact(mh) is T del) + return del; + + throw java.lang.invoke.Invokers.newWrongMethodTypeException(GetDelegateMethodType(typeof(T)), mh.type()); } /// diff --git a/src/IKVM.Tests/Java/ikvm/runtime/UtilDelegateTests.cs b/src/IKVM.Tests/Java/ikvm/runtime/UtilDelegateTests.cs new file mode 100644 index 0000000000..467ce9fae0 --- /dev/null +++ b/src/IKVM.Tests/Java/ikvm/runtime/UtilDelegateTests.cs @@ -0,0 +1,157 @@ +using System; + +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace IKVM.Tests.Java.ikvm.runtime +{ + + [TestClass] + public class UtilDelegateTests + { + + static global::java.lang.Class Class(string name) => global::java.lang.Class.forName(name); + + static global::java.lang.reflect.Method Method(string className, string methodName, params string[] parameterTypeNames) + { + var parameterTypes = new global::java.lang.Class[parameterTypeNames.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + parameterTypes[i] = Class(parameterTypeNames[i]); + + return Class(className).getMethod(methodName, parameterTypes); + } + + [TestMethod] + public void CanCreateDelegateForInstanceMethod() + { + var m = Method("java.util.ArrayList", "size"); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), m); + + var list = new global::java.util.ArrayList(); + list.add("a"); + list.add("b"); + + d(list).Should().Be(2); + } + + [TestMethod] + public void CanCreateDelegateForInstanceMethodWithArgument() + { + var m = Method("java.lang.StringBuilder", "append", "java.lang.String"); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), m); + + var sb = new global::java.lang.StringBuilder("a"); + d(sb, "b").Should().BeSameAs(sb); + sb.toString().Should().Be("ab"); + } + + [TestMethod] + public void CanCreateDelegateForStaticMethod() + { + var m = Method("java.lang.Integer", "parseInt", "java.lang.String"); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), m); + + d("42").Should().Be(42); + } + + [TestMethod] + public void CanCreateDelegateForVoidMethod() + { + var m = Method("java.util.ArrayList", "clear"); + var d = (Action)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Action), m); + + var list = new global::java.util.ArrayList(); + list.add("a"); + d(list); + + list.size().Should().Be(0); + } + + /// + /// java.lang.Object is a remapped type, so its methods have no single backing MethodBase; the handle has to + /// route through the instance helper. + /// + [TestMethod] + public void CanCreateDelegateForMethodOnRemappedType() + { + var m = Method("java.lang.Object", "hashCode"); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), m); + + var o = new object(); + d(o).Should().Be(global::java.lang.System.identityHashCode(o)); + } + + [TestMethod] + public void CanCreateDelegateForConstructor() + { + var c = Class("java.util.ArrayList").getConstructor(new global::java.lang.Class[0]); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), c); + + d().Should().BeOfType(); + } + + /// + /// Access is checked as for Lookup.unreflect, so an inaccessible member is rejected unless it has been marked + /// accessible. java.lang.Runtime's constructor is private. + /// + [TestMethod] + public void ThrowsForInaccessibleMemberUntilMarkedAccessible() + { + var c = Class("java.lang.Runtime").getDeclaredConstructor(new global::java.lang.Class[0]); + + var act = () => global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), c); + act.Should().Throw(); + + c.setAccessible(true); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), c); + d().Should().BeOfType(); + } + + [TestMethod] + public void CanCreateDelegateForBoundMethodHandle() + { + var m = Method("java.util.ArrayList", "size"); + var list = new global::java.util.ArrayList(); + list.add("a"); + + var mh = global::java.lang.invoke.MethodHandles.publicLookup().unreflect(m).bindTo(list); + var d = (Func)global::ikvm.runtime.Util.getDelegateFromMethodHandle(typeof(Func), mh); + + d().Should().Be(1); + } + + [TestMethod] + public void ThrowsForTypeThatIsNotADelegate() + { + var m = Method("java.util.ArrayList", "size"); + + var act = () => global::ikvm.runtime.Util.getDelegateFromMethod(typeof(string), m); + act.Should().Throw(); + } + + [TestMethod] + public void ThrowsForIncompatibleDelegateSignature() + { + var m = Method("java.lang.Integer", "parseInt", "java.lang.String"); + + // parseInt takes one argument; this delegate supplies three + var act = () => global::ikvm.runtime.Util.getDelegateFromMethod(typeof(Func), m); + act.Should().Throw(); + } + + [TestMethod] + public void ThrowsForNullArguments() + { + var m = Method("java.util.ArrayList", "size"); + + var nullType = () => global::ikvm.runtime.Util.getDelegateFromMethodHandle(null, global::java.lang.invoke.MethodHandles.publicLookup().unreflect(m)); + nullType.Should().Throw(); + + var nullHandle = () => global::ikvm.runtime.Util.getDelegateFromMethodHandle(typeof(Func), null); + nullHandle.Should().Throw(); + } + + } + +}