From e6d6aa27fec116f48fb2eb60264c3cb11f7d45b4 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 8 Aug 2026 15:27:31 -0500 Subject: [PATCH] Add ikvm.runtime.Util.getDelegateFromMethodHandle/getDelegateFromMethod. Gives callers a supported way to obtain a .NET delegate for a Java method without reaching into IKVM.Runtime.ByteCodeHelper or the internal MH/MHV delegate family, neither of which is API. getDelegateFromMethodHandle adapts the handle to the requested delegate's signature with asType, which performs the boxing, primitive widening, ghost wrapping and receiver binding that a raw MethodBase from RuntimeJavaMethod.GetMethod() cannot express -- that method is one-to-many for remapped types and null for artificial ones. It then materializes the canonical invoke-exact delegate and binds its Invoke as the requested type. getDelegateFromMethod is a thin wrapper over Lookup.unreflect and unreflectConstructor. Access is checked as it is there, so callers must setAccessible an otherwise inaccessible member. Also factors the non-generic core out of MethodHandleUtil.GetDelegateForInvokeExact. The generic form previously threw unconditionally when the handle already had a cached delegate, because the type test sat inside the null check; it now returns the cached delegate when the type matches. Net behaviour is unchanged, since the sole caller in ByteCodeHelper performs that same test before calling in. --- src/IKVM.Java/local/ikvm/runtime/Util.java | 33 +++- .../Java/Externs/ikvm/runtime/Util.cs | 40 +++++ .../MethodHandleUtil.jniexport.cs | 24 ++- .../Java/ikvm/runtime/UtilDelegateTests.cs | 157 ++++++++++++++++++ 4 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 src/IKVM.Tests/Java/ikvm/runtime/UtilDelegateTests.cs 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(); + } + + } + +}