From 99bd68e19aa8b6763f7f208dab7f7984b7a389d7 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Sun, 25 Oct 2026 22:13:33 +0200 Subject: [PATCH 1/6] Fix DispatchProxy pointer parameters and returns The generated proxy method stored each argument in the object[] it passes to DispatchProxy.Invoke with Convert(paramType -> object), which for a pointer or function pointer fell through to `castclass object` on the raw pointer. A non-null pointer argument made the call throw NullReferenceException on CoreCLR before Invoke ran (or pass the pointee to Invoke as an object when its first word was a MethodTable pointer), and Mono rejected the method with InvalidProgramException on every call. Pointer returns and by-ref pointer write-backs had the same problem in the other direction (`castclass` to the pointer type). Box and unbox pointers in the forms MethodInfo.Invoke uses for pointer arguments and returns: a pointer argument reaches Invoke as a System.Reflection.Pointer of the parameter's type and a function pointer as an IntPtr; a pointer returned or written back by Invoke may be a Pointer (pointers only), an IntPtr or null (a null pointer), and anything else throws InvalidCastException. By-ref pointers are read and written with ldind.i/stind.i. The IL for every non-pointer parameter and return is unchanged. Fix #136451 Co-Authored-By: Claude Opus 5.5 --- .../Reflection/DispatchProxyGenerator.cs | 71 +++++++ .../tests/DispatchProxyTests.cs | 192 ++++++++++++++++++ ...stem.Reflection.DispatchProxy.Tests.csproj | 1 + .../tests/TestTypes.cs | 37 ++++ 4 files changed, 301 insertions(+) diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index a4a2a239203c4a..c45174c76f89b4 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -51,6 +51,8 @@ internal static class DispatchProxyGenerator private static readonly MethodInfo s_dispatchProxyInvokeMethod = typeof(DispatchProxy).GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance)!; private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; private static readonly MethodInfo s_makeGenericMethodMethod = GetGenericMethodMethodInfo(); + private static readonly MethodInfo s_pointerBoxMethod = typeof(Pointer).GetMethod(nameof(Pointer.Box))!; + private static readonly MethodInfo s_pointerUnboxMethod = typeof(Pointer).GetMethod(nameof(Pointer.Unbox))!; [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "MakeGenericMethod is safe here because the user code invoking the generic method will reference " + @@ -648,6 +650,18 @@ private static void Convert(ILGenerator il, Type source, Type target, bool isAdd Convert(il, argType, target, isAddress); return; } + if (source.IsPointer || source.IsFunctionPointer) + { + Debug.Assert(target == typeof(object)); + BoxPointer(il, source); + return; + } + if (target.IsPointer || target.IsFunctionPointer) + { + Debug.Assert(source == typeof(object)); + UnboxPointer(il, target); + return; + } if (target.IsValueType) { if (source.IsValueType) @@ -687,8 +701,59 @@ private static void Convert(ILGenerator il, Type source, Type target, bool isAdd } } + // Boxes a pointer the way MethodInfo.Invoke returns one: a pointer as a System.Reflection.Pointer, + // a function pointer as an IntPtr. + private static void BoxPointer(ILGenerator il, Type type) + { + if (type.IsFunctionPointer) + { + il.Emit(OpCodes.Box, typeof(IntPtr)); + } + else + { + il.Emit(OpCodes.Ldtoken, type); + il.Emit(OpCodes.Call, s_getTypeFromHandleMethod); + il.Emit(OpCodes.Call, s_pointerBoxMethod); + } + } + + // Unboxes a pointer from what MethodInfo.Invoke returns for one (a System.Reflection.Pointer for a pointer, + // an IntPtr for a function pointer), from an IntPtr, or from null as a null pointer. + // Anything else throws InvalidCastException. + private static void UnboxPointer(ILGenerator il, Type type) + { + Label done = il.DefineLabel(); + if (type.IsPointer) + { + Label notPointer = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Isinst, typeof(Pointer)); + il.Emit(OpCodes.Brfalse_S, notPointer); + il.Emit(OpCodes.Call, s_pointerUnboxMethod); + il.Emit(OpCodes.Br_S, done); + il.MarkLabel(notPointer); + } + + Label notNull = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue_S, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Br_S, done); + il.MarkLabel(notNull); + il.Emit(OpCodes.Unbox_Any, typeof(IntPtr)); + il.MarkLabel(done); + } + private static void Ldind(ILGenerator il, Type type) { + if (type.IsPointer || type.IsFunctionPointer) + { + il.Emit(OpCodes.Ldind_I); + return; + } + OpCode opCode = s_ldindOpCodes[GetTypeCode(type)]; if (!opCode.Equals(OpCodes.Nop)) { @@ -702,6 +767,12 @@ private static void Ldind(ILGenerator il, Type type) private static void Stind(ILGenerator il, Type type) { + if (type.IsPointer || type.IsFunctionPointer) + { + il.Emit(OpCodes.Stind_I); + return; + } + OpCode opCode = s_stindOpCodes[GetTypeCode(type)]; if (!opCode.Equals(OpCodes.Nop)) { diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index 3cf8746aa9eee1..238a0709a695dc 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -999,6 +999,198 @@ private static void TestRefOutInInvocation(Action invocation, Assert.Equal(expected, result); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Pointer_Parameter_Receives_Pointer(bool useGenericCreate) + { + int value = 42; + var proxy = CreateHelper(useGenericCreate); + + object received = null; + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => + { + received = args[0]; + return null; + }; + + proxy.Echo(&value); + Assert.IsType(received); + Assert.Equal((nint)(&value), (nint)Pointer.Unbox(received)); + + proxy.Echo(null); + Assert.IsType(received); + Assert.Equal(0, (nint)Pointer.Unbox(received)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Pointer_Return_Accepts_Pointer_IntPtr_And_Null(bool useGenericCreate) + { + int value = 42; + int* p = &value; + var proxy = CreateHelper(useGenericCreate); + + object result = null; + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => result; + + result = Pointer.Box(p, typeof(int*)); + Assert.Equal((nint)p, (nint)proxy.Echo(null)); + + result = (IntPtr)p; + Assert.Equal((nint)p, (nint)proxy.Echo(null)); + + result = null; + Assert.Equal(0, (nint)proxy.Echo(null)); + + // The pointer type of a returned Pointer is not checked. + result = Pointer.Box((long*)p, typeof(long*)); + Assert.Equal((nint)p, (nint)proxy.Echo(null)); + + result = 42; + Assert.Throws(() => { proxy.Echo(null); }); + + result = "42"; + Assert.Throws(() => { proxy.Echo(null); }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate) + { + int* values = stackalloc int[3]; + var proxy = CreateHelper(useGenericCreate); + + object received = "Failed"; + object written = null; + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => + { + received = args[0]; + args[0] = written; + return null; + }; + + int* p = values; + written = Pointer.Box(values + 1, typeof(int*)); + proxy.Ref(ref p); + Assert.Equal((nint)values, (nint)Pointer.Unbox(received)); + Assert.Equal((nint)(values + 1), (nint)p); + + written = (IntPtr)(values + 2); + proxy.Ref(ref p); + Assert.Equal((nint)(values + 1), (nint)Pointer.Unbox(received)); + Assert.Equal((nint)(values + 2), (nint)p); + + written = null; + proxy.Ref(ref p); + Assert.Equal(0, (nint)p); + + written = Pointer.Box(values, typeof(int*)); + proxy.Out(out p); + Assert.Null(received); + Assert.Equal((nint)values, (nint)p); + + written = null; + proxy.Out(out p); + Assert.Equal(0, (nint)p); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Pointer_Method_Forwarded_To_MethodInfo_Invoke(bool useGenericCreate) + { + int* ints = stackalloc int[2]; + long* longs = stackalloc long[2]; + var target = new TestType_PointerService(); + var proxy = CreateHelper(useGenericCreate); + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => method.Invoke(target, args); + + Assert.Equal((nint)(ints + 1), (nint)proxy.Echo(ints)); + Assert.Equal((nint)(longs + 1), (nint)proxy.EchoGeneric(longs)); + Assert.Equal(sizeof(int), (nint)proxy.Echo(null)); + Assert.Equal((nint)((byte*)ints + 1), (nint)proxy.EchoVoid(ints)); + Assert.Equal((nint)(&ints + 1), (nint)proxy.EchoPointerPointer(&ints)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Void_Pointer_Double_Pointer_And_In_Pointer(bool useGenericCreate) + { + int value = 42; + int* p = &value; + var proxy = CreateHelper(useGenericCreate); + + object received = null; + object result = null; + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => + { + received = args[0]; + return result; + }; + + result = Pointer.Box(p, typeof(void*)); + Assert.Equal((nint)p, (nint)proxy.EchoVoid(p)); + Assert.Equal((nint)p, (nint)Pointer.Unbox(Assert.IsType(received))); + + result = Pointer.Box(&p, typeof(int**)); + Assert.Equal((nint)(&p), (nint)proxy.EchoPointerPointer(&p)); + Assert.Equal((nint)(&p), (nint)Pointer.Unbox(Assert.IsType(received))); + + result = null; + proxy.In(in p); + Assert.Equal((nint)(&value), (nint)Pointer.Unbox(Assert.IsType(received))); + Assert.Equal((nint)(&value), (nint)p); + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/100020", TestRuntimes.CoreCLR)] + [Theory] + [InlineData(false)] + [InlineData(true)] + public static unsafe void Invoke_Function_Pointer_Parameter_And_Return(bool useGenericCreate) + { + delegate* twice = &TestType_FunctionPointerService.Twice; + var proxy = CreateHelper(useGenericCreate); + + object received = null; + object result = null; + object written = null; + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => + { + received = args[0]; + args[0] = written; + return result; + }; + + result = (IntPtr)twice; + Assert.Equal((nint)twice, (nint)proxy.Echo(twice)); + Assert.Equal((nint)twice, Assert.IsType(received)); + + result = null; + Assert.Equal(0, (nint)proxy.Echo(null)); + Assert.Equal(0, Assert.IsType(received)); + + result = Pointer.Box((void*)twice, typeof(void*)); + Assert.Throws(() => { proxy.Echo(twice); }); + + delegate* f = twice; + written = null; + proxy.Ref(ref f); + Assert.Equal((nint)twice, Assert.IsType(received)); + Assert.Equal(0, (nint)f); + + written = (IntPtr)twice; + proxy.Ref(ref f); + Assert.Equal((nint)twice, (nint)f); + + var target = new TestType_FunctionPointerService(); + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => method.Invoke(target, args); + Assert.Equal(42, proxy.Echo(twice)(21)); + } + private static TestType_IHelloService CreateTestHelloProxy(bool useGenericCreate) => CreateHelper(useGenericCreate); diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/System.Reflection.DispatchProxy.Tests.csproj b/src/libraries/System.Reflection.DispatchProxy/tests/System.Reflection.DispatchProxy.Tests.csproj index 941967a335138e..f35e37e6069ed7 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/System.Reflection.DispatchProxy.Tests.csproj +++ b/src/libraries/System.Reflection.DispatchProxy/tests/System.Reflection.DispatchProxy.Tests.csproj @@ -2,6 +2,7 @@ $(NetCoreAppCurrent) + true false false diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs index 6982e81fa726e7..93615cd64b78cf 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs @@ -142,6 +142,43 @@ public interface TypeType_GenericMethod T Echo(T messages); } +// Demonstrates proxies can be made for methods with pointer parameters and returns. +public unsafe interface TestType_IPointerService +{ + int* Echo(int* p); + void Ref(ref int* p); + void Out(out int* p); + T* EchoGeneric(T* p) where T : unmanaged; + void* EchoVoid(void* p); + int** EchoPointerPointer(int** p); + void In(in int* p); +} + +public unsafe class TestType_PointerService : TestType_IPointerService +{ + public int* Echo(int* p) => p + 1; + public void Ref(ref int* p) => p++; + public void Out(out int* p) => p = null; + public T* EchoGeneric(T* p) where T : unmanaged => p + 1; + public void* EchoVoid(void* p) => (byte*)p + 1; + public int** EchoPointerPointer(int** p) => p + 1; + public void In(in int* p) { } +} + +// Demonstrates proxies can be made for methods with function pointer parameters and returns. +public unsafe interface TestType_IFunctionPointerService +{ + delegate* Echo(delegate* f); + void Ref(ref delegate* f); +} + +public unsafe class TestType_FunctionPointerService : TestType_IFunctionPointerService +{ + public static int Twice(int x) => 2 * x; + public delegate* Echo(delegate* f) => f; + public void Ref(ref delegate* f) => f = &Twice; +} + // Negative -- demonstrates trying to use a class for the interface type for the proxy public class TestType_ConcreteClass { From 8322ba2b1291ad8b65b6a7b6a1e97a09e9048793 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Mon, 26 Oct 2026 20:05:52 +0200 Subject: [PATCH 2/6] Pass null pointers as null; convert pointers in C# Leave function pointer parameters and returns as before: DispatchProxy.Create still throws for them on CoreCLR, where TypeBuilder can't encode function pointer types. A null pointer argument now reaches Invoke as null, as it did on CoreCLR before, instead of as a Pointer holding null. Convert pointers in two C# methods instead of inline IL. Generated proxies can't call the library's internal methods, so they call them through delegates held in static fields of a PointerHelpers type, defined once per proxy assembly. Co-Authored-By: Claude Opus 5.5 --- .../Reflection/DispatchProxyGenerator.cs | 163 ++++++++++++------ .../tests/DispatchProxyTests.cs | 82 +++++---- .../tests/TestTypes.cs | 14 +- .../tests/TrimmingTests/DispatchProxyTest.cs | 12 +- 4 files changed, 165 insertions(+), 106 deletions(-) diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index c45174c76f89b4..be960feb2a3a22 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -51,8 +51,30 @@ internal static class DispatchProxyGenerator private static readonly MethodInfo s_dispatchProxyInvokeMethod = typeof(DispatchProxy).GetMethod("Invoke", BindingFlags.NonPublic | BindingFlags.Instance)!; private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; private static readonly MethodInfo s_makeGenericMethodMethod = GetGenericMethodMethodInfo(); - private static readonly MethodInfo s_pointerBoxMethod = typeof(Pointer).GetMethod(nameof(Pointer.Box))!; - private static readonly MethodInfo s_pointerUnboxMethod = typeof(Pointer).GetMethod(nameof(Pointer.Unbox))!; + + // Generated proxies call BoxPointer and UnboxPointer through delegates held in static fields of + // their assembly (ProxyAssembly.DefinePointerHelpers). The delegates take and return pointers as IntPtr. + private static MethodInfo? s_boxPointerInvokeMethod; + private static MethodInfo? s_unboxPointerInvokeMethod; + + private static MethodInfo BoxPointerInvokeMethod => + s_boxPointerInvokeMethod ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + + private static MethodInfo UnboxPointerInvokeMethod => + s_unboxPointerInvokeMethod ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + + // Passes a pointer argument to Invoke as a System.Reflection.Pointer of the parameter's type, + // and a null pointer as null. + private static unsafe object? BoxPointer(IntPtr pointer, Type type) => + pointer == IntPtr.Zero ? null : Pointer.Box((void*)pointer, type); + + // Takes a pointer returned or written back by Invoke as a System.Reflection.Pointer, an IntPtr, + // or null as a null pointer. The pointer type stored in a Pointer is not checked. + // Anything else is unboxed as an IntPtr. + private static unsafe IntPtr UnboxPointer(object? value) => + value is Pointer pointer ? (IntPtr)Pointer.Unbox(pointer) : + value is null ? IntPtr.Zero : + (IntPtr)value; [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "MakeGenericMethod is safe here because the user code invoking the generic method will reference " + @@ -117,6 +139,9 @@ private sealed class ProxyAssembly private readonly HashSet _ignoresAccessAssemblyNames = new HashSet(); private ConstructorInfo? _ignoresAccessChecksToAttributeConstructor; + private FieldInfo? _boxPointerField; + private FieldInfo? _unboxPointerField; + [RequiresDynamicCode("Defining a dynamic assembly requires generating code at runtime")] public ProxyAssembly(AssemblyLoadContext alc) { @@ -260,6 +285,46 @@ internal void EnsureTypeIsVisible(Type type) } } } + + // The PointerHelpers fields holding the Func and Func + // delegates that generated code calls. + internal FieldInfo BoxPointerField + { + get + { + if (_boxPointerField is null) + DefinePointerHelpers(); + return _boxPointerField!; + } + } + + internal FieldInfo UnboxPointerField + { + get + { + if (_unboxPointerField is null) + DefinePointerHelpers(); + return _unboxPointerField!; + } + } + + // Defines and creates the type holding the delegate fields and sets them, once per proxy assembly, + // when the first pointer conversion is emitted, so before any proxy type that uses them is created. + // Runs under the lock taken by GetProxyType. + private void DefinePointerHelpers() + { + TypeBuilder tb = _mb.DefineType("PointerHelpers", TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed); + FieldBuilder box = tb.DefineField(nameof(BoxPointer), typeof(Func), FieldAttributes.Assembly | FieldAttributes.Static); + FieldBuilder unbox = tb.DefineField(nameof(UnboxPointer), typeof(Func), FieldAttributes.Assembly | FieldAttributes.Static); + Type helpers = tb.CreateType(); + + const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static; + helpers.GetField(nameof(BoxPointer), Flags)!.SetValue(null, new Func(BoxPointer)); + helpers.GetField(nameof(UnboxPointer), Flags)!.SetValue(null, new Func(UnboxPointer)); + + _boxPointerField = box; + _unboxPointerField = unbox; + } } private sealed class ProxyBuilder @@ -479,11 +544,11 @@ private MethodBuilder AddMethodImpl(MethodInfo mi, int methodInfoIndex) return mdb; } - ParametersArray args = new ParametersArray(il, paramTypes); + ParametersArray args = new ParametersArray(_assembly, il, paramTypes); // object[] args = new object[paramCount]; il.Emit(OpCodes.Nop); - GenericArray argsArr = new GenericArray(il, parameters.Length); + GenericArray argsArr = new GenericArray(_assembly, il, parameters.Length); for (int i = 0; i < parameters.Length; i++) { @@ -512,7 +577,7 @@ private MethodBuilder AddMethodImpl(MethodInfo mi, int methodInfoIndex) il.Emit(OpCodes.Ldloc, methodInfoLocal); Type[] genericTypes = mi.GetGenericArguments(); - GenericArray typeArr = new GenericArray(il, genericTypes.Length); + GenericArray typeArr = new GenericArray(_assembly, il, genericTypes.Length); for (int i = 0; i < genericTypes.Length; ++i) { typeArr.BeginSet(i); @@ -559,7 +624,7 @@ private MethodBuilder AddMethodImpl(MethodInfo mi, int methodInfoIndex) { // return (mi.ReturnType)result; il.Emit(OpCodes.Ldloc, resultLocal); - Convert(il, typeof(object), mi.ReturnType, false); + Convert(_assembly, il, typeof(object), mi.ReturnType, false); } il.Emit(OpCodes.Ret); @@ -636,7 +701,7 @@ private MethodBuilder AddMethodImpl(MethodInfo mi, int methodInfoIndex) OpCodes.Stind_Ref, //String = 18, }; - private static void Convert(ILGenerator il, Type source, Type target, bool isAddress) + private static void Convert(ProxyAssembly assembly, ILGenerator il, Type source, Type target, bool isAddress) { Debug.Assert(!target.IsByRef); if (target == source) @@ -647,19 +712,19 @@ private static void Convert(ILGenerator il, Type source, Type target, bool isAdd Debug.Assert(!isAddress); Type argType = source.GetElementType()!; Ldind(il, argType); - Convert(il, argType, target, isAddress); + Convert(assembly, il, argType, target, isAddress); return; } - if (source.IsPointer || source.IsFunctionPointer) + if (source.IsPointer) { Debug.Assert(target == typeof(object)); - BoxPointer(il, source); + EmitBoxPointer(assembly, il, source); return; } - if (target.IsPointer || target.IsFunctionPointer) + if (target.IsPointer) { Debug.Assert(source == typeof(object)); - UnboxPointer(il, target); + EmitUnboxPointer(assembly, il); return; } if (target.IsValueType) @@ -701,54 +766,34 @@ private static void Convert(ILGenerator il, Type source, Type target, bool isAdd } } - // Boxes a pointer the way MethodInfo.Invoke returns one: a pointer as a System.Reflection.Pointer, - // a function pointer as an IntPtr. - private static void BoxPointer(ILGenerator il, Type type) + // The delegate comes before the value on the stack, so the value is held in a local + // while the delegate is loaded under it. + + // object? DispatchProxyGenerator.BoxPointer(IntPtr pointer, Type type) + private static void EmitBoxPointer(ProxyAssembly assembly, ILGenerator il, Type type) { - if (type.IsFunctionPointer) - { - il.Emit(OpCodes.Box, typeof(IntPtr)); - } - else - { - il.Emit(OpCodes.Ldtoken, type); - il.Emit(OpCodes.Call, s_getTypeFromHandleMethod); - il.Emit(OpCodes.Call, s_pointerBoxMethod); - } + LocalBuilder pointer = il.DeclareLocal(typeof(IntPtr)); + il.Emit(OpCodes.Stloc, pointer); + il.Emit(OpCodes.Ldsfld, assembly.BoxPointerField); + il.Emit(OpCodes.Ldloc, pointer); + il.Emit(OpCodes.Ldtoken, type); + il.Emit(OpCodes.Call, s_getTypeFromHandleMethod); + il.Emit(OpCodes.Callvirt, BoxPointerInvokeMethod); } - // Unboxes a pointer from what MethodInfo.Invoke returns for one (a System.Reflection.Pointer for a pointer, - // an IntPtr for a function pointer), from an IntPtr, or from null as a null pointer. - // Anything else throws InvalidCastException. - private static void UnboxPointer(ILGenerator il, Type type) + // IntPtr DispatchProxyGenerator.UnboxPointer(object? value) + private static void EmitUnboxPointer(ProxyAssembly assembly, ILGenerator il) { - Label done = il.DefineLabel(); - if (type.IsPointer) - { - Label notPointer = il.DefineLabel(); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Isinst, typeof(Pointer)); - il.Emit(OpCodes.Brfalse_S, notPointer); - il.Emit(OpCodes.Call, s_pointerUnboxMethod); - il.Emit(OpCodes.Br_S, done); - il.MarkLabel(notPointer); - } - - Label notNull = il.DefineLabel(); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brtrue_S, notNull); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Conv_U); - il.Emit(OpCodes.Br_S, done); - il.MarkLabel(notNull); - il.Emit(OpCodes.Unbox_Any, typeof(IntPtr)); - il.MarkLabel(done); + LocalBuilder value = il.DeclareLocal(typeof(object)); + il.Emit(OpCodes.Stloc, value); + il.Emit(OpCodes.Ldsfld, assembly.UnboxPointerField); + il.Emit(OpCodes.Ldloc, value); + il.Emit(OpCodes.Callvirt, UnboxPointerInvokeMethod); } private static void Ldind(ILGenerator il, Type type) { - if (type.IsPointer || type.IsFunctionPointer) + if (type.IsPointer) { il.Emit(OpCodes.Ldind_I); return; @@ -767,7 +812,7 @@ private static void Ldind(ILGenerator il, Type type) private static void Stind(ILGenerator il, Type type) { - if (type.IsPointer || type.IsFunctionPointer) + if (type.IsPointer) { il.Emit(OpCodes.Stind_I); return; @@ -786,10 +831,12 @@ private static void Stind(ILGenerator il, Type type) private sealed class ParametersArray { + private readonly ProxyAssembly _assembly; private readonly ILGenerator _il; private readonly Type[] _paramTypes; - internal ParametersArray(ILGenerator il, Type[] paramTypes) + internal ParametersArray(ProxyAssembly assembly, ILGenerator il, Type[] paramTypes) { + _assembly = assembly; _il = il; _paramTypes = paramTypes; } @@ -808,17 +855,19 @@ internal void EndSet(int i, Type stackType) { Debug.Assert(_paramTypes[i].IsByRef); Type argType = _paramTypes[i].GetElementType()!; - Convert(_il, stackType, argType, false); + Convert(_assembly, _il, stackType, argType, false); Stind(_il, argType); } } private sealed class GenericArray { + private readonly ProxyAssembly _assembly; private readonly ILGenerator _il; private readonly LocalBuilder _lb; - internal GenericArray(ILGenerator il, int len) + internal GenericArray(ProxyAssembly assembly, ILGenerator il, int len) { + _assembly = assembly; _il = il; _lb = il.DeclareLocal(typeof(T[])); @@ -847,7 +896,7 @@ internal void BeginSet(int i) internal void EndSet(Type stackType) { - Convert(_il, stackType, typeof(T), false); + Convert(_assembly, _il, stackType, typeof(T), false); _il.Emit(OpCodes.Stelem_Ref); } } diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index 238a0709a695dc..b2360cd765598a 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -1018,9 +1018,9 @@ public static unsafe void Invoke_Pointer_Parameter_Receives_Pointer(bool useGene Assert.IsType(received); Assert.Equal((nint)(&value), (nint)Pointer.Unbox(received)); + received = "Failed"; proxy.Echo(null); - Assert.IsType(received); - Assert.Equal(0, (nint)Pointer.Unbox(received)); + Assert.Null(received); } [Theory] @@ -1087,6 +1087,21 @@ public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate proxy.Ref(ref p); Assert.Equal(0, (nint)p); + proxy.Ref(ref p); + Assert.Null(received); + + // The pointer type of a written-back Pointer is not checked. + written = Pointer.Box((long*)values, typeof(long*)); + proxy.Ref(ref p); + Assert.Equal((nint)values, (nint)p); + + written = 42; + Assert.Throws(() => { proxy.Ref(ref p); }); + + written = (IntPtr)(values + 1); + proxy.Out(out p); + Assert.Equal((nint)(values + 1), (nint)p); + written = Pointer.Box(values, typeof(int*)); proxy.Out(out p); Assert.Null(received); @@ -1146,49 +1161,44 @@ public static unsafe void Invoke_Void_Pointer_Double_Pointer_And_In_Pointer(bool Assert.Equal((nint)(&value), (nint)p); } - [ActiveIssue("https://github.com/dotnet/runtime/issues/100020", TestRuntimes.CoreCLR)] [Theory] [InlineData(false)] [InlineData(true)] - public static unsafe void Invoke_Function_Pointer_Parameter_And_Return(bool useGenericCreate) + public static void Invoke_Pointer_Parameter_In_Another_AssemblyLoadContext(bool useGenericCreate) { - delegate* twice = &TestType_FunctionPointerService.Twice; - var proxy = CreateHelper(useGenericCreate); - - object received = null; - object result = null; - object written = null; - ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => - { - received = args[0]; - args[0] = written; - return result; - }; - - result = (IntPtr)twice; - Assert.Equal((nint)twice, (nint)proxy.Echo(twice)); - Assert.Equal((nint)twice, Assert.IsType(received)); + if (typeof(DispatchProxyTests).Assembly.Location == "") + return; - result = null; - Assert.Equal(0, (nint)proxy.Echo(null)); - Assert.Equal(0, Assert.IsType(received)); + EchoPointerThroughProxy(useGenericCreate); - result = Pointer.Box((void*)twice, typeof(void*)); - Assert.Throws(() => { proxy.Echo(twice); }); + // Each new load context gets its own proxy assembly, whose first pointer conversion is a box in + // EchoPointerThroughProxy and an unbox in ReturnPointerThroughProxy. + InvokeInNewAssemblyLoadContext(nameof(EchoPointerThroughProxy), useGenericCreate); + InvokeInNewAssemblyLoadContext(nameof(ReturnPointerThroughProxy), useGenericCreate); + } - delegate* f = twice; - written = null; - proxy.Ref(ref f); - Assert.Equal((nint)twice, Assert.IsType(received)); - Assert.Equal(0, (nint)f); + private static void InvokeInNewAssemblyLoadContext(string methodName, bool useGenericCreate) => + new AssemblyLoadContext(methodName) + .LoadFromAssemblyPath(typeof(DispatchProxyTests).Assembly.Location) + .GetType(typeof(DispatchProxyTests).FullName, true) + .GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic) + .Invoke(null, new object[] { useGenericCreate }); - written = (IntPtr)twice; - proxy.Ref(ref f); - Assert.Equal((nint)twice, (nint)f); + private static unsafe void EchoPointerThroughProxy(bool useGenericCreate) + { + int value = 42; + var proxy = CreateHelper(useGenericCreate); + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => args[0]; + Assert.Equal((nint)(&value), (nint)proxy.Echo(&value)); + } - var target = new TestType_FunctionPointerService(); - ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => method.Invoke(target, args); - Assert.Equal(42, proxy.Echo(twice)(21)); + private static unsafe void ReturnPointerThroughProxy(bool useGenericCreate) + { + int value = 42; + object result = Pointer.Box(&value, typeof(int*)); + var proxy = CreateHelper(useGenericCreate); + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => result; + Assert.Equal((nint)(&value), (nint)proxy.Get()); } private static TestType_IHelloService CreateTestHelloProxy(bool useGenericCreate) => diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs index 93615cd64b78cf..e69eeda18faadf 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs @@ -165,18 +165,10 @@ public unsafe class TestType_PointerService : TestType_IPointerService public void In(in int* p) { } } -// Demonstrates proxies can be made for methods with function pointer parameters and returns. -public unsafe interface TestType_IFunctionPointerService +// Demonstrates proxies can be made for a pointer return with no pointer parameter. +public unsafe interface TestType_IPointerReturnService { - delegate* Echo(delegate* f); - void Ref(ref delegate* f); -} - -public unsafe class TestType_FunctionPointerService : TestType_IFunctionPointerService -{ - public static int Twice(int x) => 2 * x; - public delegate* Echo(delegate* f) => f; - public void Ref(ref delegate* f) => f = &Twice; + int* Get(); } // Negative -- demonstrates trying to use a class for the interface type for the proxy diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/TrimmingTests/DispatchProxyTest.cs b/src/libraries/System.Reflection.DispatchProxy/tests/TrimmingTests/DispatchProxyTest.cs index 55177e1c5ae5ec..0f53af628989a3 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/TrimmingTests/DispatchProxyTest.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/TrimmingTests/DispatchProxyTest.cs @@ -9,7 +9,7 @@ /// class Program { - static int Main() + static unsafe int Main() { IFoo foo = CountingProxy.Wrap(new Foo()); @@ -17,7 +17,13 @@ static int Main() foo.Method1(); foo.Method2(); - if (((CountingProxy)foo).InvocationCount != 3) + int value = 42; + if (foo.PointerMethod(&value) != &value) + { + return -2; + } + + if (((CountingProxy)foo).InvocationCount != 4) { return -1; } @@ -34,6 +40,7 @@ public interface IFoo public void Method1(); public void Method2(); public void UnusedMethod3(); + public unsafe int* PointerMethod(int* p); } class Foo : IFoo @@ -44,6 +51,7 @@ class Foo : IFoo public void Method1() { } public void Method2() { } public void UnusedMethod3() { } + public unsafe int* PointerMethod(int* p) => p; } public class CountingProxy : DispatchProxy From 747c7c8d963c523c66c58cd11bb5a07061bd856b Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Tue, 27 Oct 2026 23:20:07 +0200 Subject: [PATCH 3/6] Use field for lazy properties; explain helpers The lazily initialized properties this change adds use the field keyword. The two that return the delegate fields used to fill both fields from one helper type; each delegate now has a helper type of its own, so each property fills only its own backing field. Comments that restated the code are removed or rewritten to say why. The load-context test is conditional on HasAssemblyFiles through ConditionalTheory, as tests.instructions.md asks; the older tests beside it return early instead. Co-Authored-By: Claude Opus 5.5 --- .../Reflection/DispatchProxyGenerator.cs | 74 +++++-------------- .../tests/DispatchProxyTests.cs | 12 +-- .../tests/TestTypes.cs | 2 - 3 files changed, 23 insertions(+), 65 deletions(-) diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index be960feb2a3a22..572bf756fbcc91 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -52,25 +52,16 @@ internal static class DispatchProxyGenerator private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; private static readonly MethodInfo s_makeGenericMethodMethod = GetGenericMethodMethodInfo(); - // Generated proxies call BoxPointer and UnboxPointer through delegates held in static fields of - // their assembly (ProxyAssembly.DefinePointerHelpers). The delegates take and return pointers as IntPtr. - private static MethodInfo? s_boxPointerInvokeMethod; - private static MethodInfo? s_unboxPointerInvokeMethod; - private static MethodInfo BoxPointerInvokeMethod => - s_boxPointerInvokeMethod ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + field ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; private static MethodInfo UnboxPointerInvokeMethod => - s_unboxPointerInvokeMethod ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + field ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; - // Passes a pointer argument to Invoke as a System.Reflection.Pointer of the parameter's type, - // and a null pointer as null. private static unsafe object? BoxPointer(IntPtr pointer, Type type) => pointer == IntPtr.Zero ? null : Pointer.Box((void*)pointer, type); - // Takes a pointer returned or written back by Invoke as a System.Reflection.Pointer, an IntPtr, - // or null as a null pointer. The pointer type stored in a Pointer is not checked. - // Anything else is unboxed as an IntPtr. + // A Pointer's type is not checked against the target type: Pointer exposes it only internally. private static unsafe IntPtr UnboxPointer(object? value) => value is Pointer pointer ? (IntPtr)Pointer.Unbox(pointer) : value is null ? IntPtr.Zero : @@ -139,9 +130,6 @@ private sealed class ProxyAssembly private readonly HashSet _ignoresAccessAssemblyNames = new HashSet(); private ConstructorInfo? _ignoresAccessChecksToAttributeConstructor; - private FieldInfo? _boxPointerField; - private FieldInfo? _unboxPointerField; - [RequiresDynamicCode("Defining a dynamic assembly requires generating code at runtime")] public ProxyAssembly(AssemblyLoadContext alc) { @@ -286,44 +274,23 @@ internal void EnsureTypeIsVisible(Type type) } } - // The PointerHelpers fields holding the Func and Func - // delegates that generated code calls. - internal FieldInfo BoxPointerField - { - get - { - if (_boxPointerField is null) - DefinePointerHelpers(); - return _boxPointerField!; - } - } + internal FieldInfo BoxPointerField => + field ??= DefinePointerHelper(nameof(BoxPointer), new Func(BoxPointer)); - internal FieldInfo UnboxPointerField - { - get - { - if (_unboxPointerField is null) - DefinePointerHelpers(); - return _unboxPointerField!; - } - } + internal FieldInfo UnboxPointerField => + field ??= DefinePointerHelper(nameof(UnboxPointer), new Func(UnboxPointer)); - // Defines and creates the type holding the delegate fields and sets them, once per proxy assembly, - // when the first pointer conversion is emitted, so before any proxy type that uses them is created. - // Runs under the lock taken by GetProxyType. - private void DefinePointerHelpers() + // Proxies call BoxPointer and UnboxPointer through a delegate because they can't call them directly: + // the methods aren't accessible outside System.Reflection.DispatchProxy. IgnoresAccessChecksTo would + // allow the call on CoreCLR, but Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only + // once, so after a pointer call it would miss the one added later for a proxy's internal base type. + // Called under GetProxyType's lock, so each helper type is defined once per proxy assembly. + private FieldInfo DefinePointerHelper(string name, Delegate helper) { - TypeBuilder tb = _mb.DefineType("PointerHelpers", TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed); - FieldBuilder box = tb.DefineField(nameof(BoxPointer), typeof(Func), FieldAttributes.Assembly | FieldAttributes.Static); - FieldBuilder unbox = tb.DefineField(nameof(UnboxPointer), typeof(Func), FieldAttributes.Assembly | FieldAttributes.Static); - Type helpers = tb.CreateType(); - - const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static; - helpers.GetField(nameof(BoxPointer), Flags)!.SetValue(null, new Func(BoxPointer)); - helpers.GetField(nameof(UnboxPointer), Flags)!.SetValue(null, new Func(UnboxPointer)); - - _boxPointerField = box; - _unboxPointerField = unbox; + TypeBuilder tb = _mb.DefineType(name + "Helper", TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed); + FieldBuilder fb = tb.DefineField(name, helper.GetType(), FieldAttributes.Assembly | FieldAttributes.Static); + tb.CreateType().GetField(name, BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(null, helper); + return fb; } } @@ -766,10 +733,8 @@ private static void Convert(ProxyAssembly assembly, ILGenerator il, Type source, } } - // The delegate comes before the value on the stack, so the value is held in a local - // while the delegate is loaded under it. - - // object? DispatchProxyGenerator.BoxPointer(IntPtr pointer, Type type) + // The delegate must be on the stack below the value, which is already there, + // so the value goes through a local while the delegate is loaded. private static void EmitBoxPointer(ProxyAssembly assembly, ILGenerator il, Type type) { LocalBuilder pointer = il.DeclareLocal(typeof(IntPtr)); @@ -781,7 +746,6 @@ private static void EmitBoxPointer(ProxyAssembly assembly, ILGenerator il, Type il.Emit(OpCodes.Callvirt, BoxPointerInvokeMethod); } - // IntPtr DispatchProxyGenerator.UnboxPointer(object? value) private static void EmitUnboxPointer(ProxyAssembly assembly, ILGenerator il) { LocalBuilder value = il.DeclareLocal(typeof(object)); diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index b2360cd765598a..877966e05a7c98 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -1044,7 +1044,7 @@ public static unsafe void Invoke_Pointer_Return_Accepts_Pointer_IntPtr_And_Null( result = null; Assert.Equal(0, (nint)proxy.Echo(null)); - // The pointer type of a returned Pointer is not checked. + // Accepted: the proxy can't read a Pointer's type to check it. result = Pointer.Box((long*)p, typeof(long*)); Assert.Equal((nint)p, (nint)proxy.Echo(null)); @@ -1090,7 +1090,7 @@ public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate proxy.Ref(ref p); Assert.Null(received); - // The pointer type of a written-back Pointer is not checked. + // Accepted: the proxy can't read a Pointer's type to check it. written = Pointer.Box((long*)values, typeof(long*)); proxy.Ref(ref p); Assert.Equal((nint)values, (nint)p); @@ -1161,18 +1161,14 @@ public static unsafe void Invoke_Void_Pointer_Double_Pointer_And_In_Pointer(bool Assert.Equal((nint)(&value), (nint)p); } - [Theory] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] [InlineData(false)] [InlineData(true)] public static void Invoke_Pointer_Parameter_In_Another_AssemblyLoadContext(bool useGenericCreate) { - if (typeof(DispatchProxyTests).Assembly.Location == "") - return; - EchoPointerThroughProxy(useGenericCreate); - // Each new load context gets its own proxy assembly, whose first pointer conversion is a box in - // EchoPointerThroughProxy and an unbox in ReturnPointerThroughProxy. + // Each new load context gets a proxy assembly that must define its own helpers. InvokeInNewAssemblyLoadContext(nameof(EchoPointerThroughProxy), useGenericCreate); InvokeInNewAssemblyLoadContext(nameof(ReturnPointerThroughProxy), useGenericCreate); } diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs index e69eeda18faadf..c6f918b872bc30 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs @@ -142,7 +142,6 @@ public interface TypeType_GenericMethod T Echo(T messages); } -// Demonstrates proxies can be made for methods with pointer parameters and returns. public unsafe interface TestType_IPointerService { int* Echo(int* p); @@ -165,7 +164,6 @@ public unsafe class TestType_PointerService : TestType_IPointerService public void In(in int* p) { } } -// Demonstrates proxies can be made for a pointer return with no pointer parameter. public unsafe interface TestType_IPointerReturnService { int* Get(); From 26fdcd342870dd89d47e86c78dd8292ce13e7d95 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Tue, 27 Oct 2026 23:38:44 +0200 Subject: [PATCH 4/6] Name the subject of two test comments Co-Authored-By: Claude Opus 5.5 --- .../tests/DispatchProxyTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index 877966e05a7c98..133449c58f6dcc 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -1044,7 +1044,7 @@ public static unsafe void Invoke_Pointer_Return_Accepts_Pointer_IntPtr_And_Null( result = null; Assert.Equal(0, (nint)proxy.Echo(null)); - // Accepted: the proxy can't read a Pointer's type to check it. + // A Pointer of another pointer type is accepted: the proxy can't read its type to check it. result = Pointer.Box((long*)p, typeof(long*)); Assert.Equal((nint)p, (nint)proxy.Echo(null)); @@ -1090,7 +1090,7 @@ public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate proxy.Ref(ref p); Assert.Null(received); - // Accepted: the proxy can't read a Pointer's type to check it. + // A Pointer of another pointer type is accepted: the proxy can't read its type to check it. written = Pointer.Box((long*)values, typeof(long*)); proxy.Ref(ref p); Assert.Equal((nint)values, (nint)p); From 81325a9778cec0601907f9d7992c89de66ea84f5 Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Wed, 28 Oct 2026 10:31:09 +0200 Subject: [PATCH 5/6] Return FieldBuilder from DefinePointerHelper CA1859 flagged the FieldInfo return type in the builds that run analyzers. Co-Authored-By: Claude Opus 5.5 --- .../src/System/Reflection/DispatchProxyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index 572bf756fbcc91..b322b18b006146 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -285,7 +285,7 @@ internal void EnsureTypeIsVisible(Type type) // allow the call on CoreCLR, but Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only // once, so after a pointer call it would miss the one added later for a proxy's internal base type. // Called under GetProxyType's lock, so each helper type is defined once per proxy assembly. - private FieldInfo DefinePointerHelper(string name, Delegate helper) + private FieldBuilder DefinePointerHelper(string name, Delegate helper) { TypeBuilder tb = _mb.DefineType(name + "Helper", TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed); FieldBuilder fb = tb.DefineField(name, helper.GetType(), FieldAttributes.Assembly | FieldAttributes.Static); From 9cd377b7d2089bb55d8e2d4530bfcb6a38a8bb4c Mon Sep 17 00:00:00 2001 From: Iurie Caraion Date: Thu, 29 Oct 2026 19:55:41 +0200 Subject: [PATCH 6/6] Call pointer helpers directly; skip on Mono Proxies call BoxPointer and UnboxPointer directly instead of through delegates in helper types. A proxy that converts a pointer gets its proxy assembly an IgnoresAccessChecksTo attribute for System.Reflection.DispatchProxy, through EnsureTypeIsVisible, as for internal proxied types. Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once, so it misses the one a pointer proxy or a proxy of an internal type adds later. The pointer tests are skipped on Mono. Co-Authored-By: Claude Opus 5.5 --- .../Reflection/DispatchProxyGenerator.cs | 45 +++++-------------- .../tests/DispatchProxyTests.cs | 40 +++++++++++++---- .../tests/TestTypes.cs | 5 +++ 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs index b322b18b006146..4de61abfe2d8ea 100644 --- a/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs +++ b/src/libraries/System.Reflection.DispatchProxy/src/System/Reflection/DispatchProxyGenerator.cs @@ -52,11 +52,11 @@ internal static class DispatchProxyGenerator private static readonly MethodInfo s_getTypeFromHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle", new Type[] { typeof(RuntimeTypeHandle) })!; private static readonly MethodInfo s_makeGenericMethodMethod = GetGenericMethodMethodInfo(); - private static MethodInfo BoxPointerInvokeMethod => - field ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + private static MethodInfo BoxPointerMethod => + field ??= typeof(DispatchProxyGenerator).GetMethod(nameof(BoxPointer), BindingFlags.NonPublic | BindingFlags.Static)!; - private static MethodInfo UnboxPointerInvokeMethod => - field ??= typeof(Func).GetMethod(nameof(Func.Invoke))!; + private static MethodInfo UnboxPointerMethod => + field ??= typeof(DispatchProxyGenerator).GetMethod(nameof(UnboxPointer), BindingFlags.NonPublic | BindingFlags.Static)!; private static unsafe object? BoxPointer(IntPtr pointer, Type type) => pointer == IntPtr.Zero ? null : Pointer.Box((void*)pointer, type); @@ -273,25 +273,6 @@ internal void EnsureTypeIsVisible(Type type) } } } - - internal FieldInfo BoxPointerField => - field ??= DefinePointerHelper(nameof(BoxPointer), new Func(BoxPointer)); - - internal FieldInfo UnboxPointerField => - field ??= DefinePointerHelper(nameof(UnboxPointer), new Func(UnboxPointer)); - - // Proxies call BoxPointer and UnboxPointer through a delegate because they can't call them directly: - // the methods aren't accessible outside System.Reflection.DispatchProxy. IgnoresAccessChecksTo would - // allow the call on CoreCLR, but Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only - // once, so after a pointer call it would miss the one added later for a proxy's internal base type. - // Called under GetProxyType's lock, so each helper type is defined once per proxy assembly. - private FieldBuilder DefinePointerHelper(string name, Delegate helper) - { - TypeBuilder tb = _mb.DefineType(name + "Helper", TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed); - FieldBuilder fb = tb.DefineField(name, helper.GetType(), FieldAttributes.Assembly | FieldAttributes.Static); - tb.CreateType().GetField(name, BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(null, helper); - return fb; - } } private sealed class ProxyBuilder @@ -733,26 +714,20 @@ private static void Convert(ProxyAssembly assembly, ILGenerator il, Type source, } } - // The delegate must be on the stack below the value, which is already there, - // so the value goes through a local while the delegate is loaded. + // The proxy assembly can call BoxPointer and UnboxPointer only through an IgnoresAccessChecksTo + // attribute for System.Reflection.DispatchProxy, which EnsureTypeIsVisible adds. private static void EmitBoxPointer(ProxyAssembly assembly, ILGenerator il, Type type) { - LocalBuilder pointer = il.DeclareLocal(typeof(IntPtr)); - il.Emit(OpCodes.Stloc, pointer); - il.Emit(OpCodes.Ldsfld, assembly.BoxPointerField); - il.Emit(OpCodes.Ldloc, pointer); + assembly.EnsureTypeIsVisible(typeof(DispatchProxyGenerator)); il.Emit(OpCodes.Ldtoken, type); il.Emit(OpCodes.Call, s_getTypeFromHandleMethod); - il.Emit(OpCodes.Callvirt, BoxPointerInvokeMethod); + il.Emit(OpCodes.Call, BoxPointerMethod); } private static void EmitUnboxPointer(ProxyAssembly assembly, ILGenerator il) { - LocalBuilder value = il.DeclareLocal(typeof(object)); - il.Emit(OpCodes.Stloc, value); - il.Emit(OpCodes.Ldsfld, assembly.UnboxPointerField); - il.Emit(OpCodes.Ldloc, value); - il.Emit(OpCodes.Callvirt, UnboxPointerInvokeMethod); + assembly.EnsureTypeIsVisible(typeof(DispatchProxyGenerator)); + il.Emit(OpCodes.Call, UnboxPointerMethod); } private static void Ldind(ILGenerator il, Type type) diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs index 133449c58f6dcc..a24370b36f8ac7 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/DispatchProxyTests.cs @@ -1002,10 +1002,11 @@ private static void TestRefOutInInvocation(Action invocation, [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static unsafe void Invoke_Pointer_Parameter_Receives_Pointer(bool useGenericCreate) { int value = 42; - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); object received = null; ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => @@ -1026,11 +1027,12 @@ public static unsafe void Invoke_Pointer_Parameter_Receives_Pointer(bool useGene [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static unsafe void Invoke_Pointer_Return_Accepts_Pointer_IntPtr_And_Null(bool useGenericCreate) { int value = 42; int* p = &value; - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); object result = null; ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => result; @@ -1058,10 +1060,11 @@ public static unsafe void Invoke_Pointer_Return_Accepts_Pointer_IntPtr_And_Null( [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate) { int* values = stackalloc int[3]; - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); object received = "Failed"; object written = null; @@ -1115,12 +1118,13 @@ public static unsafe void Invoke_Ref_Out_Pointer_Parameter(bool useGenericCreate [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static unsafe void Invoke_Pointer_Method_Forwarded_To_MethodInfo_Invoke(bool useGenericCreate) { int* ints = stackalloc int[2]; long* longs = stackalloc long[2]; var target = new TestType_PointerService(); - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => method.Invoke(target, args); Assert.Equal((nint)(ints + 1), (nint)proxy.Echo(ints)); @@ -1133,11 +1137,12 @@ public static unsafe void Invoke_Pointer_Method_Forwarded_To_MethodInfo_Invoke(b [Theory] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static unsafe void Invoke_Void_Pointer_Double_Pointer_And_In_Pointer(bool useGenericCreate) { int value = 42; int* p = &value; - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); object received = null; object result = null; @@ -1164,12 +1169,15 @@ public static unsafe void Invoke_Void_Pointer_Double_Pointer_And_In_Pointer(bool [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] [InlineData(false)] [InlineData(true)] + [SkipOnMono("Mono reads a dynamic assembly's IgnoresAccessChecksTo attributes only once")] public static void Invoke_Pointer_Parameter_In_Another_AssemblyLoadContext(bool useGenericCreate) { EchoPointerThroughProxy(useGenericCreate); - // Each new load context gets a proxy assembly that must define its own helpers. - InvokeInNewAssemblyLoadContext(nameof(EchoPointerThroughProxy), useGenericCreate); + // Each new load context gets its own proxy assembly, which needs its own access to BoxPointer and + // UnboxPointer. One proxy here only boxes a pointer and the other only unboxes one, so each conversion + // must add that access by itself. + InvokeInNewAssemblyLoadContext(nameof(PassPointerThroughProxy), useGenericCreate); InvokeInNewAssemblyLoadContext(nameof(ReturnPointerThroughProxy), useGenericCreate); } @@ -1183,16 +1191,30 @@ private static void InvokeInNewAssemblyLoadContext(string methodName, bool useGe private static unsafe void EchoPointerThroughProxy(bool useGenericCreate) { int value = 42; - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerService proxy = CreateHelper(useGenericCreate); ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => args[0]; Assert.Equal((nint)(&value), (nint)proxy.Echo(&value)); } + private static unsafe void PassPointerThroughProxy(bool useGenericCreate) + { + int value = 42; + object received = null; + TestType_IPointerArgumentService proxy = CreateHelper(useGenericCreate); + ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => + { + received = args[0]; + return null; + }; + proxy.Set(&value); + Assert.Equal((nint)(&value), (nint)Pointer.Unbox(received)); + } + private static unsafe void ReturnPointerThroughProxy(bool useGenericCreate) { int value = 42; object result = Pointer.Box(&value, typeof(int*)); - var proxy = CreateHelper(useGenericCreate); + TestType_IPointerReturnService proxy = CreateHelper(useGenericCreate); ((TestDispatchProxy)proxy).CallOnInvoke = (method, args) => result; Assert.Equal((nint)(&value), (nint)proxy.Get()); } diff --git a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs index c6f918b872bc30..f4d8b9a1f00fc8 100644 --- a/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs +++ b/src/libraries/System.Reflection.DispatchProxy/tests/TestTypes.cs @@ -169,6 +169,11 @@ public unsafe interface TestType_IPointerReturnService int* Get(); } +public unsafe interface TestType_IPointerArgumentService +{ + void Set(int* p); +} + // Negative -- demonstrates trying to use a class for the interface type for the proxy public class TestType_ConcreteClass {