From 1a017479c4d57ef103a66bac30c32228cbc7b160 Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Mon, 26 Jan 2026 00:42:10 +0100 Subject: [PATCH 1/8] Define internal `ByRefLikeProxy` classes & public-facing `IByRefLikeProxy` interfaces --- src/Castle.Core/Castle.Core.csproj | 1 + .../DynamicProxy/IByRefLikeProxy.cs | 87 +++++++ .../DynamicProxy/Internal/ByRefLikeProxy.cs | 237 ++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs create mode 100644 src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs diff --git a/src/Castle.Core/Castle.Core.csproj b/src/Castle.Core/Castle.Core.csproj index 7d45404a5..b6d399981 100644 --- a/src/Castle.Core/Castle.Core.csproj +++ b/src/Castle.Core/Castle.Core.csproj @@ -4,6 +4,7 @@ net8.0;net9.0;net462;netstandard2.0 + True diff --git a/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs new file mode 100644 index 000000000..178fde0f7 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs @@ -0,0 +1,87 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if FEATURE_BYREFLIKE + +#nullable enable + +using System; + +namespace Castle.DynamicProxy +{ +#if NET9_0_OR_GREATER + /// + /// Permits indirect access to by-ref-like argument values during method interception. + /// + /// + /// Instances of by-ref-like (ref struct) types live exclusively on the evaluation stack. + /// Therefore, they cannot be boxed and put into the -typed array. + /// DynamicProxy replaces these unboxable values with references + /// (or alternatively, in the case of spans, with or ), + /// which grant you indirect read/write access to the actual values. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + /// A by-ref-like (ref struct) type. + public interface IByRefLikeProxy where TByRefLike : struct, allows ref struct + { + TByRefLike Get(); + void Set(in TByRefLike value); + } +#endif + + /// + /// Permits indirect access to -typed argument values during method interception. + /// + /// + /// is a by-ref-like (ref struct) type, which means that + /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed + /// and put into the -typed array. + /// DynamicProxy replaces these unboxable values with references, + /// which grant you indirect read/write access to the actual value. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + public interface IReadOnlySpanProxy + { + ReadOnlySpan Get(); + void Set(in ReadOnlySpan value); + } + + /// + /// Permits indirect access to -typed argument values during method interception. + /// + /// + /// is a by-ref-like (ref struct) type, which means that + /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed + /// and put into the -typed array. + /// DynamicProxy replaces these unboxable values with references, + /// which grant you indirect read/write access to the actual value. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + public interface ISpanProxy + { + Span Get(); + void Set(in Span value); + } +} + +#endif diff --git a/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs new file mode 100644 index 000000000..b7da6da3b --- /dev/null +++ b/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs @@ -0,0 +1,237 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if FEATURE_BYREFLIKE + +#nullable enable +#pragma warning disable CS8500 + +namespace Castle.DynamicProxy.Internal +{ + using System; + using System.ComponentModel; + using System.Threading; + + // This file contains a set of `unsafe` types used at runtime by DynamicProxy proxies to represent by-ref-like values + // in an `IInvocation`. Such values live exclusively on the evaluation stack and therefore cannot be boxed. Thus they are + // in principle incompatible with `IInvocation` and we need to replace them with something else... namely these types here. + // + // What follows are the safety considerations that went into the design of these types. + // + // *) These types use unmanaged pointers (`void*`) to reference storage locations (of by-ref-like method parameters). + // + // *) Unmanaged pointers are generally unsafe when used to reference unpinned heap-allocated objects. + // These types here should NEVER reference heap-allocated objects. We attempt to enforce this by asking for the + // `type` of the storage location, and throw for anything other than by-ref-like types (which by definition cannot + // live on the heap). + // + // *) Unmanaged pointers can be safe when used to reference stack-allocated objects. However, that is only true + // when they point into "live" stack frames. That is, they MUST NOT reference parameters or local variables + // of methods that have already finished executing. This is why we have the `ByRefLikeProxy.Invalidate` method: + // DynamicProxy (or whatever else instantiated a `ByRefLikeProxy` object to point at a method parameter or local + // variable) must invoke this method before said method returns (or tail-calls). + // + // *) The `checkType` / `checkPtr` arguments of `GetPtr` or `Invalidate`, respectively, have two purposes: + // + // 1. DynamicProxy, or whatever else instantiated a `ByRefLikeProxy`, is expected to know at all times what + // exactly each instance references. These parameters make it harder for anyone to use the type directly + // if they didn't also instantiate it themselves. + // + // 2. `checkPtr` of `Invalidate` attempts to prevent re-use of a referenced storage location for another + // similarly-typed local variable by the JIT. DynamicProxy typically instantiates `ByRefLikeProxy` instances + // at the start of intercepted method bodies, and it invokes `Invalidate` at the very end, meaning that + // the address of the local/parameter is taken at each method boundary, meaning that static analysis should + // never during the whole method see the local/parameter as "no longer in use". (This may be a little + // paranoid, since the CoreCLR JIT probably exempts so-called "address-exposed" locals from reuse anyway.) + // + // *) Finally, we only ever access the unmanaged pointer field through `Volatile` or `Interlocked` to better guard + // against cases where someone foolishly copied a `ByRefLikeProxy` instance out of the `IInvocation.Arguments` + // and uses it from another thread. + // + // As far as I can reason, `ByRefLikeProxy` et al. should be safe to use IFF they are never copied out from an + // `IInvocation`, and IFF DynamicProxy succeeds in destructing them and erasing them from the `IInvocation` right + // before the intercepted method finishes executing. + + /// + /// Do not use! Only DynamicProxy internals may interact with this class type directly. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe class ByRefLikeProxy + { + private readonly Type type; + private nint ptr; + + public ByRefLikeProxy(Type type, void* ptr) + { + if (type.IsByRefLikeSafe() == false) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + + if (ptr == null) + { + throw new ArgumentNullException(nameof(ptr)); + } + + this.type = type; + this.ptr = (nint)ptr; + } + + public void* GetPtr(Type checkType) + { + if (checkType != type) + { + throw new AccessViolationException(); + } + + return GetPtrNocheck(); + } + + internal void* GetPtrNocheck() + { + var ptr = (void*)Volatile.Read(ref this.ptr); + + if (ptr == null) + { + throw new AccessViolationException(); + } + + return ptr; + } + + public void Invalidate(void* checkPtr) + { + var ptr = (void*)Interlocked.CompareExchange(ref this.ptr, (nint)null, (nint)checkPtr); + + if (ptr == null || checkPtr != ptr) + { + throw new AccessViolationException(); + } + } + } + +#if NET9_0_OR_GREATER + /// + /// Access instances of this type through the public-facing interface. + /// Only DynamicProxy internals may interact with this class type directly. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe class ByRefLikeProxy : ByRefLikeProxy, IByRefLikeProxy + where TByRefLike : struct, allows ref struct + { + public ByRefLikeProxy(Type type, void* ptr) + : base(type, ptr) + { + if (type != typeof(TByRefLike)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + public TByRefLike Get() + { + return *(TByRefLike*)GetPtrNocheck(); + } + + public void Set(in TByRefLike value) + { + *(TByRefLike*)GetPtrNocheck() = value; + } + } +#endif + +#if !NET9_0_OR_GREATER + /// + /// Access instances of this type through the public-facing interface. + /// Only DynamicProxy internals may interact with this class type directly. + /// +#else + /// + /// Access instances of this type through either of the public-facing + /// or interfaces. + /// Only DynamicProxy internals may interact with this class type directly. + /// +#endif + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe class ReadOnlySpanProxy : ByRefLikeProxy, IReadOnlySpanProxy +#if NET9_0_OR_GREATER + , IByRefLikeProxy> +#endif + { + public ReadOnlySpanProxy(Type type, void* ptr) + : base(type, ptr) + { + if (type != typeof(ReadOnlySpan)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + public ReadOnlySpan Get() + { + return *(ReadOnlySpan*)GetPtrNocheck(); + } + + public void Set(in ReadOnlySpan value) + { + *(ReadOnlySpan*)GetPtrNocheck() = value; + } + } + +#if !NET9_0_OR_GREATER + /// + /// Access instances of this type through the public-facing interface. + /// Only DynamicProxy internals may interact with this class type directly. + /// +#else + /// + /// Access instances of this type through either of the public-facing + /// or interfaces. + /// Only DynamicProxy internals may interact with this class type directly. + /// +#endif + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe class SpanProxy : ByRefLikeProxy, ISpanProxy +#if NET9_0_OR_GREATER + , IByRefLikeProxy> +#endif + { + public SpanProxy(Type type, void* ptr) + : base(type, ptr) + { + if (type != typeof(Span)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + public Span Get() + { + return *(Span*)GetPtrNocheck(); + } + + public void Set(in Span value) + { + *(Span*)GetPtrNocheck() = value; + } + } +} + +#pragma warning restore CS8500 + +#endif From 9c5519233a969deaa638ba9d99b2dc70fe205cde Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Mon, 26 Jan 2026 00:46:30 +0100 Subject: [PATCH 2/8] Convert by-ref-like args to/from `ByRefLikeProxy` types instead of nullifying them --- .../Emitters/SimpleAST/ArgumentReference.cs | 4 +- .../ConvertArgumentFromObjectExpression.cs | 29 ++- .../Emitters/SimpleAST/PointerReference.cs | 58 ++++++ .../Generators/InvocationTypeGenerator.cs | 75 +++---- .../MethodWithInvocationGenerator.cs | 196 ++++++++++-------- .../Tokens/ByRefLikeProxyMethods.cs | 36 ++++ 6 files changed, 261 insertions(+), 137 deletions(-) create mode 100644 src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/PointerReference.cs create mode 100644 src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs index e4ab715a3..d0c0a199c 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ public ArgumentReference(Type argumentType, int position) public override void EmitAddress(ILGenerator gen) { - throw new NotSupportedException(); + gen.Emit(Position > 255 ? OpCodes.Ldarga : OpCodes.Ldarga_S, Position); } public override void Emit(ILGenerator gen) diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs index 209a8bb72..6781749d5 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs @@ -18,6 +18,9 @@ namespace Castle.DynamicProxy.Generators.Emitters.SimpleAST using System.Diagnostics; using System.Reflection.Emit; + using Castle.DynamicProxy.Internal; + using Castle.DynamicProxy.Tokens; + internal class ConvertArgumentFromObjectExpression : IExpression { private readonly IExpression obj; @@ -42,17 +45,29 @@ public void Emit(ILGenerator gen) if (dereferencedArgumentType.IsValueType) { - // Unbox conversion - // Assumes fromType is a boxed value - // if we can, we emit a box and ldind, otherwise, we will use unbox.any - if (LdindOpCodesDictionary.Instance[dereferencedArgumentType] != LdindOpCodesDictionary.EmptyOpCode) +#if FEATURE_BYREFLIKE + if (dereferencedArgumentType.IsByRefLikeSafe()) { - gen.Emit(OpCodes.Unbox, dereferencedArgumentType); - OpCodeUtil.EmitLoadIndirectOpCodeForType(gen, dereferencedArgumentType); + gen.Emit(OpCodes.Ldtoken, dereferencedArgumentType); + gen.Emit(OpCodes.Call, TypeMethods.GetTypeFromHandle); + gen.Emit(OpCodes.Call, ByRefLikeProxyMethods.GetPtr); + gen.Emit(OpCodes.Ldobj, dereferencedArgumentType); } else +#endif { - gen.Emit(OpCodes.Unbox_Any, dereferencedArgumentType); + // Unbox conversion + // Assumes fromType is a boxed value + // if we can, we emit a box and ldind, otherwise, we will use unbox.any + if (LdindOpCodesDictionary.Instance[dereferencedArgumentType] != LdindOpCodesDictionary.EmptyOpCode) + { + gen.Emit(OpCodes.Unbox, dereferencedArgumentType); + OpCodeUtil.EmitLoadIndirectOpCodeForType(gen, dereferencedArgumentType); + } + else + { + gen.Emit(OpCodes.Unbox_Any, dereferencedArgumentType); + } } } else diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/PointerReference.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/PointerReference.cs new file mode 100644 index 000000000..871004b32 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/PointerReference.cs @@ -0,0 +1,58 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#nullable enable + +namespace Castle.DynamicProxy.Generators.Emitters.SimpleAST +{ + using System; + using System.Diagnostics; + using System.Reflection; + using System.Reflection.Emit; + + /// + /// Represents the storage location X referenced by a + /// holding an unmanaged pointer &X to it. + /// It essentially has the same function as the pointer indirection / dereferencing operator *. + /// + [DebuggerDisplay("*{ptr}")] + internal class PointerReference : Reference + { + private readonly IExpression ptr; + + public PointerReference(IExpression ptr, Type elementType) + : base(elementType) + { + this.ptr = ptr; + } + + public override void EmitAddress(ILGenerator gen) + { + ptr.Emit(gen); + } + + public override void Emit(ILGenerator gen) + { + ptr.Emit(gen); + OpCodeUtil.EmitLoadIndirectOpCodeForType(gen, Type); + } + + public override void EmitStore(IExpression value, ILGenerator gen) + { + ptr.Emit(gen); + value.Emit(gen); + OpCodeUtil.EmitStoreIndirectOpCodeForType(gen, Type); + } + } +} \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs index 04958beaf..f4876ae49 100644 --- a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs @@ -287,26 +287,15 @@ public void CopyOut(out IExpression[] arguments, out LocalReference?[] byRefArgu IExpression dereferencedArgument; -#if FEATURE_BYREFLIKE - if (dereferencedArgumentType.IsByRefLikeSafe()) - { - // The argument value in the invocation `Arguments` array is an `object` - // and cannot be converted back to its original by-ref-like type. - // We need to replace it with some other value. + // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // since `ConvertArgumentFromObjectExpression` knows how to deal with those. - // For now, we just substitute the by-ref-like type's default value: - dereferencedArgument = new DefaultValueExpression(dereferencedArgumentType); - } - else -#endif - { - dereferencedArgument = new ConvertArgumentFromObjectExpression( - new MethodInvocationExpression( - ThisExpression.Instance, - InvocationMethods.GetArgumentValue, - new LiteralIntExpression(i)), - dereferencedArgumentType); - } + dereferencedArgument = new ConvertArgumentFromObjectExpression( + new MethodInvocationExpression( + ThisExpression.Instance, + InvocationMethods.GetArgumentValue, + new LiteralIntExpression(i)), + dereferencedArgumentType); if (argumentType.IsByRef) { @@ -333,17 +322,20 @@ public void CopyIn(LocalReference?[] byRefArguments) #if FEATURE_BYREFLIKE if (localCopy.Type.IsByRefLikeSafe()) { - // The by-ref-like value in the local buffer variable cannot be put back - // into the invocation `Arguments` array, because it cannot be boxed. - // We need to replace it with some other value. - - // For now, we just erase it by substituting `null`: + // For by-ref-like values, a `ByRefLikeProxy` has previously been placed in `IInvocation.Arguments`. + // We must not replace that proxy, but use it to update the referenced by-ref-like parameter: method.CodeBuilder.AddStatement( - new MethodInvocationExpression( - ThisExpression.Instance, - InvocationMethods.SetArgumentValue, - new LiteralIntExpression(i), - NullExpression.Instance)); + new AssignStatement( + new PointerReference( + new MethodInvocationExpression( + new MethodInvocationExpression( + ThisExpression.Instance, + InvocationMethods.GetArgumentValue, + new LiteralIntExpression(i)), + ByRefLikeProxyMethods.GetPtr, + new TypeTokenExpression(localCopy.Type)), + localCopy.Type), + localCopy)); } else #endif @@ -361,25 +353,14 @@ public void CopyIn(LocalReference?[] byRefArguments) public void SetReturnValue(LocalReference returnValue) { #if FEATURE_BYREFLIKE - if (returnValue.Type.IsByRefLikeSafe()) - { - // The by-ref-like return value cannot be put into the `ReturnValue` property, - // because it cannot be boxed. We need to replace it with some other value. - - // For now, we just erase it by substituting `null`: - method.CodeBuilder.AddStatement(new MethodInvocationExpression( - ThisExpression.Instance, - InvocationMethods.SetReturnValue, - NullExpression.Instance)); - } - else + // TODO: For by-ref-like return values, we will need to read `IInvocation.ReturnValue` + // and set the return value via pointer indirection (`ByRefLikeProxy.GetPtr`). #endif - { - method.CodeBuilder.AddStatement(new MethodInvocationExpression( - ThisExpression.Instance, - InvocationMethods.SetReturnValue, - new ConvertArgumentToObjectExpression(returnValue))); - } + + method.CodeBuilder.AddStatement(new MethodInvocationExpression( + ThisExpression.Instance, + InvocationMethods.SetReturnValue, + new ConvertArgumentToObjectExpression(returnValue))); } } } diff --git a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs index 624d241d1..653ce6bb9 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -16,8 +16,10 @@ namespace Castle.DynamicProxy.Generators { using System; using System.Diagnostics; + using System.Linq; using System.Reflection; using System.Reflection.Emit; + using System.Runtime.CompilerServices; #if FEATURE_SERIALIZATION using System.Xml.Serialization; #endif @@ -103,7 +105,10 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C var argumentsMarshaller = new ArgumentsMarshaller(emitter, MethodToOverride.GetParameters()); - argumentsMarshaller.CopyIn(out var argumentsArray, out var hasByRefArguments); + argumentsMarshaller.CopyIn(out var argumentsArray, out var hasByRefArguments, out var hasByRefLikeArguments); + + // TODO: If the return type is by-ref-like, we should prepare a local variable and a `ByRefLikeProxy` for it + // and place it in `IInvocation.ReturnValue`, so that the interception pipeline has a means to return something. var ctorArguments = GetCtorArguments(@class, proxiedMethodTokenExpression, argumentsArray, methodInterceptors); ctorArguments = ModifyArguments(@class, ctorArguments); @@ -117,7 +122,7 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C EmitLoadGenericMethodArguments(emitter, MethodToOverride.MakeGenericMethod(genericArguments), invocationLocal); } - if (hasByRefArguments) + if (hasByRefArguments || hasByRefLikeArguments) { emitter.CodeBuilder.AddStatement(TryStatement.Instance); } @@ -125,15 +130,20 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C var proceed = new MethodInvocationExpression(invocationLocal, InvocationMethods.Proceed); emitter.CodeBuilder.AddStatement(proceed); - if (hasByRefArguments) + if (hasByRefArguments || hasByRefLikeArguments) { emitter.CodeBuilder.AddStatement(FinallyStatement.Instance); - } - argumentsMarshaller.CopyOut(argumentsArray); + if (hasByRefArguments) + { + argumentsMarshaller.CopyOut(argumentsArray); + } + + if (hasByRefLikeArguments) + { + argumentsMarshaller.InvalidateByRefLikeProxies(argumentsArray); + } - if (hasByRefArguments) - { emitter.CodeBuilder.AddStatement(EndExceptionBlockStatement.Instance); } @@ -229,12 +239,13 @@ public ArgumentsMarshaller(MethodEmitter method, ParameterInfo[] parameters) this.parameters = parameters; } - public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments) + public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments, out bool hasByRefLikeArguments) { var arguments = method.Arguments; argumentsArray = method.CodeBuilder.DeclareLocal(typeof(object[])); hasByRefArguments = false; + hasByRefLikeArguments = false; method.CodeBuilder.AddStatement( new AssignStatement( @@ -259,23 +270,49 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments #if FEATURE_BYREFLIKE if (dereferencedArgumentType.IsByRefLikeSafe()) { - // The by-ref-like argument value cannot be put into the `object[]` array, - // because it cannot be boxed. We need to replace it with some other value. + hasByRefLikeArguments = true; - // For now, we just erase it by substituting `null`: + // By-ref-like values live exclusively on the stack and cannot be boxed to `object`. + // Instead of them, we prepare instances of `ByRefLikeProxy` wrappers that reference them. + +#if NET9_0_OR_GREATER + // TODO: perhaps we should cache these `ConstructorInfo`s? + ConstructorInfo proxyCtor = typeof(ByRefLikeProxy<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single(); +#else + ConstructorInfo proxyCtor = ByRefLikeProxyMethods.Constructor; +#endif + if (dereferencedArgumentType.IsConstructedGenericType) + { + var typeDef = dereferencedArgumentType.GetGenericTypeDefinition(); + if (typeDef == typeof(ReadOnlySpan<>)) + { + var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; + proxyCtor = typeof(ReadOnlySpanProxy<>).MakeGenericType(typeArg).GetConstructors().Single(); + } + else if (typeDef == typeof(Span<>)) + { + var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; + proxyCtor = typeof(SpanProxy<>).MakeGenericType(typeArg).GetConstructors().Single(); + } + } + + var proxy = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeProxy)); method.CodeBuilder.AddStatement( new AssignStatement( - new ArrayElementReference(argumentsArray, i), - NullExpression.Instance)); + proxy, + new NewInstanceExpression( + proxyCtor, + new TypeTokenExpression(dereferencedArgumentType), + new AddressOfExpression(dereferencedArgument)))); + + dereferencedArgument = proxy; } - else #endif - { + method.CodeBuilder.AddStatement( - new AssignStatement( - new ArrayElementReference(argumentsArray, i), - new ConvertArgumentToObjectExpression(dereferencedArgument))); - } + new AssignStatement( + new ArrayElementReference(argumentsArray, i), + new ConvertArgumentToObjectExpression(dereferencedArgument))); } } @@ -292,41 +329,51 @@ public void CopyOut(LocalReference argumentsArray) var dereferencedArgument = new IndirectReference(arguments[i]); var dereferencedArgumentType = dereferencedArgument.Type; + // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // since `ConvertArgumentFromObjectExpression` knows how to deal with those. + + method.CodeBuilder.AddStatement( + new AssignStatement( + dereferencedArgument, + new ConvertArgumentFromObjectExpression( + new ArrayElementReference(argumentsArray, i), + dereferencedArgumentType))); + } + } + } + + public void InvalidateByRefLikeProxies(LocalReference argumentsArray) + { #if FEATURE_BYREFLIKE - if (dereferencedArgumentType.IsByRefLikeSafe()) - { - // The argument value in the invocation `Arguments` array is an `object` - // and cannot be converted back to its original by-ref-like type. - // We need to replace it with some other value. + var arguments = method.Arguments; - // For now, we just substitute the by-ref-like type's default value: - if (parameters[i].IsOut) - { - method.CodeBuilder.AddStatement( - new AssignStatement( - dereferencedArgument, - new DefaultValueExpression(dereferencedArgumentType))); - } - else - { - // ... except when we're dealing with a `ref` parameter. Unlike with `out`, - // where we would be expected to definitely assign to it, we are free to leave - // the original incoming value untouched. For now, that's likely the better - // interim solution than unconditionally resetting. - } - } - else -#endif - { - method.CodeBuilder.AddStatement( - new AssignStatement( - dereferencedArgument, - new ConvertArgumentFromObjectExpression( - new ArrayElementReference(argumentsArray, i), - dereferencedArgumentType))); - } + for (int i = 0, n = arguments.Length; i < n; ++i) + { + var argument = arguments[i]; + var argumentType = argument.Type; + var dereferencedArgumentType = argumentType.IsByRef ? argumentType.GetElementType()! : argumentType; + + if (dereferencedArgumentType.IsByRefLikeSafe()) + { + // The `ByRefLikeProxy` invocation argument must be rendered unusable + // at the end of the (intercepted) method invocation, since it references + // a method argument that is about to be popped off the stack. + method.CodeBuilder.AddStatement( + new MethodInvocationExpression( + new AsTypeExpression( + new ArrayElementReference(argumentsArray, i), + typeof(ByRefLikeProxy)), + ByRefLikeProxyMethods.Invalidate, + argumentType.IsByRef ? argument : new AddressOfExpression(argument))); + + // Make the unusable proxy unreachable by erasing it from the invocation arguments array. + method.CodeBuilder.AddStatement( + new AssignStatement( + new ArrayElementReference(argumentsArray, i), + NullExpression.Instance)); } } +#endif } public void Return(LocalReference invocation) @@ -339,42 +386,29 @@ public void Return(LocalReference invocation) return; } -#if FEATURE_BYREFLIKE - if (returnType.IsByRefLikeSafe()) - { - // The return value in the `ReturnValue` property is an `object` - // and cannot be converted back to the original by-ref-like return type. - // We need to replace it with some other value. + var returnValue = method.CodeBuilder.DeclareLocal(typeof(object)); + method.CodeBuilder.AddStatement( + new AssignStatement( + returnValue, + new MethodInvocationExpression(invocation, InvocationMethods.GetReturnValue))); - // For now, we just substitute the by-ref-like type's default value: - method.CodeBuilder.AddStatement( - new ReturnStatement( - new DefaultValueExpression(returnType))); - } - else -#endif + // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // since `ConvertArgumentFromObjectExpression` knows how to deal with those. + + // Emit code to ensure a value type return type is not null, otherwise the cast will cause a null-deref + if (returnType.IsValueType && !returnType.IsNullableType()) { - var returnValue = method.CodeBuilder.DeclareLocal(typeof(object)); method.CodeBuilder.AddStatement( - new AssignStatement( + new IfNullExpression( returnValue, - new MethodInvocationExpression(invocation, InvocationMethods.GetReturnValue))); - - // Emit code to ensure a value type return type is not null, otherwise the cast will cause a null-deref - if (returnType.IsValueType && !returnType.IsNullableType()) - { - method.CodeBuilder.AddStatement( - new IfNullExpression( - returnValue, - new ThrowStatement( - typeof(InvalidOperationException), - "Interceptors failed to set a return value, or swallowed the exception thrown by the target"))); - } - - method.CodeBuilder.AddStatement( - new ReturnStatement( - new ConvertArgumentFromObjectExpression(returnValue, returnType))); + new ThrowStatement( + typeof(InvalidOperationException), + "Interceptors failed to set a return value, or swallowed the exception thrown by the target"))); } + + method.CodeBuilder.AddStatement( + new ReturnStatement( + new ConvertArgumentFromObjectExpression(returnValue, returnType))); } } } diff --git a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs new file mode 100644 index 000000000..33df2be3b --- /dev/null +++ b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs @@ -0,0 +1,36 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if FEATURE_BYREFLIKE + +#nullable enable + +namespace Castle.DynamicProxy.Tokens +{ + using System.Linq; + using System.Reflection; + + using Castle.DynamicProxy.Internal; + + internal static class ByRefLikeProxyMethods + { + public static ConstructorInfo Constructor = typeof(ByRefLikeProxy).GetConstructors().Single(); + + public static MethodInfo GetPtr = typeof(ByRefLikeProxy).GetMethod(nameof(ByRefLikeProxy.GetPtr))!; + + public static MethodInfo Invalidate = typeof(ByRefLikeProxy).GetMethod(nameof(ByRefLikeProxy.Invalidate))!; + } +} + +#endif From ff73c807fedaa70a7cde28cb5064c150b14805ba Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Mon, 26 Jan 2026 22:21:04 +0100 Subject: [PATCH 3/8] Replace `Get`/`Set` method pairs with ref `Value` property --- .../DynamicProxy/IByRefLikeProxy.cs | 9 ++--- .../DynamicProxy/Internal/ByRefLikeProxy.cs | 36 ++++++++----------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs index 178fde0f7..8da7dcea4 100644 --- a/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs +++ b/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs @@ -38,8 +38,7 @@ namespace Castle.DynamicProxy /// A by-ref-like (ref struct) type. public interface IByRefLikeProxy where TByRefLike : struct, allows ref struct { - TByRefLike Get(); - void Set(in TByRefLike value); + ref TByRefLike Value { get; } } #endif @@ -59,8 +58,7 @@ public interface IByRefLikeProxy where TByRefLike : struct, allows r /// public interface IReadOnlySpanProxy { - ReadOnlySpan Get(); - void Set(in ReadOnlySpan value); + ref ReadOnlySpan Value { get; } } /// @@ -79,8 +77,7 @@ public interface IReadOnlySpanProxy /// public interface ISpanProxy { - Span Get(); - void Set(in Span value); + ref Span Value { get; } } } diff --git a/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs index b7da6da3b..db305ebd5 100644 --- a/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs +++ b/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs @@ -141,14 +141,12 @@ public ByRefLikeProxy(Type type, void* ptr) } } - public TByRefLike Get() + public ref TByRefLike Value { - return *(TByRefLike*)GetPtrNocheck(); - } - - public void Set(in TByRefLike value) - { - *(TByRefLike*)GetPtrNocheck() = value; + get + { + return ref *(TByRefLike*)GetPtrNocheck(); + } } } #endif @@ -181,14 +179,12 @@ public ReadOnlySpanProxy(Type type, void* ptr) } } - public ReadOnlySpan Get() - { - return *(ReadOnlySpan*)GetPtrNocheck(); - } - - public void Set(in ReadOnlySpan value) + public ref ReadOnlySpan Value { - *(ReadOnlySpan*)GetPtrNocheck() = value; + get + { + return ref *(ReadOnlySpan*)GetPtrNocheck(); + } } } @@ -220,14 +216,12 @@ public SpanProxy(Type type, void* ptr) } } - public Span Get() + public ref Span Value { - return *(Span*)GetPtrNocheck(); - } - - public void Set(in Span value) - { - *(Span*)GetPtrNocheck() = value; + get + { + return ref *(Span*)GetPtrNocheck(); + } } } } From e75819965fc0e2c75e1cb30e950e59b8e8985b69 Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Mon, 26 Jan 2026 23:29:22 +0100 Subject: [PATCH 4/8] Rename & simplify types * Calling `ByRefLikeProxy` et al. "proxies" could be misleading, since DynamicProxy proxies typically have the exact same public surface as the proxied types. This is not the case here, `ByRefLikeProxy` types come with their own distinct API. I am choosing "reference" because that's exactly what the types are. Alternatives considered were "value accessor" and "argument". The former would lead to long type names (`ByRefLikeValueAccessor`), and the latter would be inaccurate once we start using these types for `IInvocation.ReturnValue`, too. * There seems to be little benefit to having a parallel interface type hierarchy. On the contrary: users observing (say) a `SpanProxy` inst- ance in the debugger and then being told in an XML documentation comment to access it through the `ISpanProxy` interface doesn't seem particularly user-friendly. Let's go with the simplest solution: keep only the classes. --- ...yRefLikeProxy.cs => ByRefLikeReference.cs} | 143 ++++++++++++------ .../ConvertArgumentFromObjectExpression.cs | 2 +- .../Generators/InvocationTypeGenerator.cs | 8 +- .../MethodWithInvocationGenerator.cs | 27 ++-- .../DynamicProxy/IByRefLikeProxy.cs | 84 ---------- ...ethods.cs => ByRefLikeReferenceMethods.cs} | 10 +- 6 files changed, 118 insertions(+), 156 deletions(-) rename src/Castle.Core/DynamicProxy/{Internal/ByRefLikeProxy.cs => ByRefLikeReference.cs} (52%) delete mode 100644 src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs rename src/Castle.Core/DynamicProxy/Tokens/{ByRefLikeProxyMethods.cs => ByRefLikeReferenceMethods.cs} (66%) diff --git a/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs similarity index 52% rename from src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs rename to src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index db305ebd5..f245b09b8 100644 --- a/src/Castle.Core/DynamicProxy/Internal/ByRefLikeProxy.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -17,12 +17,15 @@ #nullable enable #pragma warning disable CS8500 -namespace Castle.DynamicProxy.Internal +namespace Castle.DynamicProxy { using System; using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; using System.Threading; + using Castle.DynamicProxy.Internal; + // This file contains a set of `unsafe` types used at runtime by DynamicProxy proxies to represent by-ref-like values // in an `IInvocation`. Such values live exclusively on the evaluation stack and therefore cannot be boxed. Thus they are // in principle incompatible with `IInvocation` and we need to replace them with something else... namely these types here. @@ -38,42 +41,46 @@ namespace Castle.DynamicProxy.Internal // // *) Unmanaged pointers can be safe when used to reference stack-allocated objects. However, that is only true // when they point into "live" stack frames. That is, they MUST NOT reference parameters or local variables - // of methods that have already finished executing. This is why we have the `ByRefLikeProxy.Invalidate` method: - // DynamicProxy (or whatever else instantiated a `ByRefLikeProxy` object to point at a method parameter or local + // of methods that have already finished executing. This is why we have the `ByRefLikeReference.Invalidate` method: + // DynamicProxy (or whatever else instantiated a `ByRefLikeReference` object to point at a method parameter or local // variable) must invoke this method before said method returns (or tail-calls). // // *) The `checkType` / `checkPtr` arguments of `GetPtr` or `Invalidate`, respectively, have two purposes: // - // 1. DynamicProxy, or whatever else instantiated a `ByRefLikeProxy`, is expected to know at all times what + // 1. DynamicProxy, or whatever else instantiated a `ByRefLikeReference`, is expected to know at all times what // exactly each instance references. These parameters make it harder for anyone to use the type directly // if they didn't also instantiate it themselves. // // 2. `checkPtr` of `Invalidate` attempts to prevent re-use of a referenced storage location for another - // similarly-typed local variable by the JIT. DynamicProxy typically instantiates `ByRefLikeProxy` instances + // similarly-typed local variable by the JIT. DynamicProxy typically instantiates `ByRefLikeReference` instances // at the start of intercepted method bodies, and it invokes `Invalidate` at the very end, meaning that // the address of the local/parameter is taken at each method boundary, meaning that static analysis should // never during the whole method see the local/parameter as "no longer in use". (This may be a little // paranoid, since the CoreCLR JIT probably exempts so-called "address-exposed" locals from reuse anyway.) // // *) Finally, we only ever access the unmanaged pointer field through `Volatile` or `Interlocked` to better guard - // against cases where someone foolishly copied a `ByRefLikeProxy` instance out of the `IInvocation.Arguments` + // against cases where someone foolishly copied a `ByRefLikeReference` instance out of the `IInvocation.Arguments` // and uses it from another thread. // - // As far as I can reason, `ByRefLikeProxy` et al. should be safe to use IFF they are never copied out from an + // As far as I can reason, `ByRefLikeReference` et al. should be safe to use IFF they are never copied out from an // `IInvocation`, and IFF DynamicProxy succeeds in destructing them and erasing them from the `IInvocation` right // before the intercepted method finishes executing. /// - /// Do not use! Only DynamicProxy internals may interact with this class type directly. + /// Do not use! This type should only be used by DynamicProxy internals. /// - [CLSCompliant(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public unsafe class ByRefLikeProxy + public unsafe class ByRefLikeReference { private readonly Type type; private nint ptr; - public ByRefLikeProxy(Type type, void* ptr) + /// + /// Do not use! This constructor should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ByRefLikeReference(Type type, void* ptr) { if (type.IsByRefLikeSafe() == false) { @@ -89,6 +96,11 @@ public ByRefLikeProxy(Type type, void* ptr) this.ptr = (nint)ptr; } + /// + /// Do not use! This method should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] public void* GetPtr(Type checkType) { if (checkType != type) @@ -111,6 +123,11 @@ public ByRefLikeProxy(Type type, void* ptr) return ptr; } + /// + /// Do not use! This method should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] public void Invalidate(void* checkPtr) { var ptr = (void*)Interlocked.CompareExchange(ref this.ptr, (nint)null, (nint)checkPtr); @@ -124,15 +141,29 @@ public void Invalidate(void* checkPtr) #if NET9_0_OR_GREATER /// - /// Access instances of this type through the public-facing interface. - /// Only DynamicProxy internals may interact with this class type directly. + /// Permits indirect access to by-ref-like argument values during method interception. /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public unsafe class ByRefLikeProxy : ByRefLikeProxy, IByRefLikeProxy + /// + /// Instances of by-ref-like (ref struct) types live exclusively on the evaluation stack. + /// Therefore, they cannot be boxed and put into the -typed array. + /// DynamicProxy replaces these unboxable values with references + /// (or, in the case of spans, with or ), + /// which grant you indirect read/write access to the actual values. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + /// A by-ref-like (ref struct) type. + public unsafe class ByRefLikeReference : ByRefLikeReference where TByRefLike : struct, allows ref struct { - public ByRefLikeProxy(Type type, void* ptr) + /// + /// Do not use! This constructor should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ByRefLikeReference(Type type, void* ptr) : base(type, ptr) { if (type != typeof(TByRefLike)) @@ -151,26 +182,33 @@ public ref TByRefLike Value } #endif -#if !NET9_0_OR_GREATER - /// - /// Access instances of this type through the public-facing interface. - /// Only DynamicProxy internals may interact with this class type directly. - /// -#else /// - /// Access instances of this type through either of the public-facing - /// or interfaces. - /// Only DynamicProxy internals may interact with this class type directly. + /// Permits indirect access to -typed argument values during method interception. /// -#endif - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public unsafe class ReadOnlySpanProxy : ByRefLikeProxy, IReadOnlySpanProxy + /// + /// is a by-ref-like (ref struct) type, which means that + /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed + /// and put into the -typed array. + /// DynamicProxy replaces these unboxable values with instances of , + /// which grant you indirect read/write access to the actual value. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + public unsafe class ReadOnlySpanReference #if NET9_0_OR_GREATER - , IByRefLikeProxy> + : ByRefLikeReference> +#else + : ByRefLikeReference #endif { - public ReadOnlySpanProxy(Type type, void* ptr) + /// + /// Do not use! This constructor should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ReadOnlySpanReference(Type type, void* ptr) : base(type, ptr) { if (type != typeof(ReadOnlySpan)) @@ -179,6 +217,7 @@ public ReadOnlySpanProxy(Type type, void* ptr) } } +#if !NET9_0_OR_GREATER public ref ReadOnlySpan Value { get @@ -186,28 +225,36 @@ public ref ReadOnlySpan Value return ref *(ReadOnlySpan*)GetPtrNocheck(); } } +#endif } -#if !NET9_0_OR_GREATER - /// - /// Access instances of this type through the public-facing interface. - /// Only DynamicProxy internals may interact with this class type directly. - /// -#else /// - /// Access instances of this type through either of the public-facing - /// or interfaces. - /// Only DynamicProxy internals may interact with this class type directly. + /// Permits indirect access to -typed argument values during method interception. /// -#endif - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public unsafe class SpanProxy : ByRefLikeProxy, ISpanProxy + /// + /// is a by-ref-like (ref struct) type, which means that + /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed + /// and put into the -typed array. + /// DynamicProxy replaces these unboxable values with instances of , + /// which grant you indirect read/write access to the actual value. + /// + /// These references are only valid for the duration of the intercepted method call. + /// Any attempt to use it beyond that will result in a . + /// + /// + public unsafe class SpanReference #if NET9_0_OR_GREATER - , IByRefLikeProxy> + : ByRefLikeReference> +#else + : ByRefLikeReference #endif { - public SpanProxy(Type type, void* ptr) + /// + /// Do not use! This constructor should only be called by DynamicProxy internals. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public SpanReference(Type type, void* ptr) : base(type, ptr) { if (type != typeof(Span)) @@ -216,6 +263,7 @@ public SpanProxy(Type type, void* ptr) } } +#if !NET9_0_OR_GREATER public ref Span Value { get @@ -223,6 +271,7 @@ public ref Span Value return ref *(Span*)GetPtrNocheck(); } } +#endif } } diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs index 6781749d5..e13b1f2d2 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs @@ -50,7 +50,7 @@ public void Emit(ILGenerator gen) { gen.Emit(OpCodes.Ldtoken, dereferencedArgumentType); gen.Emit(OpCodes.Call, TypeMethods.GetTypeFromHandle); - gen.Emit(OpCodes.Call, ByRefLikeProxyMethods.GetPtr); + gen.Emit(OpCodes.Call, ByRefLikeReferenceMethods.GetPtr); gen.Emit(OpCodes.Ldobj, dereferencedArgumentType); } else diff --git a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs index f4876ae49..6c2fa5f6c 100644 --- a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs @@ -287,7 +287,7 @@ public void CopyOut(out IExpression[] arguments, out LocalReference?[] byRefArgu IExpression dereferencedArgument; - // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // Note that we don't need special logic for by-ref-like values / `ByRefLikeReference` here, // since `ConvertArgumentFromObjectExpression` knows how to deal with those. dereferencedArgument = new ConvertArgumentFromObjectExpression( @@ -322,7 +322,7 @@ public void CopyIn(LocalReference?[] byRefArguments) #if FEATURE_BYREFLIKE if (localCopy.Type.IsByRefLikeSafe()) { - // For by-ref-like values, a `ByRefLikeProxy` has previously been placed in `IInvocation.Arguments`. + // For by-ref-like values, a `ByRefLikeReference` has previously been placed in `IInvocation.Arguments`. // We must not replace that proxy, but use it to update the referenced by-ref-like parameter: method.CodeBuilder.AddStatement( new AssignStatement( @@ -332,7 +332,7 @@ public void CopyIn(LocalReference?[] byRefArguments) ThisExpression.Instance, InvocationMethods.GetArgumentValue, new LiteralIntExpression(i)), - ByRefLikeProxyMethods.GetPtr, + ByRefLikeReferenceMethods.GetPtr, new TypeTokenExpression(localCopy.Type)), localCopy.Type), localCopy)); @@ -354,7 +354,7 @@ public void SetReturnValue(LocalReference returnValue) { #if FEATURE_BYREFLIKE // TODO: For by-ref-like return values, we will need to read `IInvocation.ReturnValue` - // and set the return value via pointer indirection (`ByRefLikeProxy.GetPtr`). + // and set the return value via pointer indirection (`ByRefLikeReference.GetPtr`). #endif method.CodeBuilder.AddStatement(new MethodInvocationExpression( diff --git a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs index 653ce6bb9..d022f84bb 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -24,7 +24,6 @@ namespace Castle.DynamicProxy.Generators using System.Xml.Serialization; #endif - using Castle.Core.Internal; using Castle.DynamicProxy.Contributors; using Castle.DynamicProxy.Generators.Emitters; using Castle.DynamicProxy.Generators.Emitters.SimpleAST; @@ -107,7 +106,7 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C argumentsMarshaller.CopyIn(out var argumentsArray, out var hasByRefArguments, out var hasByRefLikeArguments); - // TODO: If the return type is by-ref-like, we should prepare a local variable and a `ByRefLikeProxy` for it + // TODO: If the return type is by-ref-like, we should prepare a local variable and a `ByRefLikeReference` for it // and place it in `IInvocation.ReturnValue`, so that the interception pipeline has a means to return something. var ctorArguments = GetCtorArguments(@class, proxiedMethodTokenExpression, argumentsArray, methodInterceptors); @@ -273,13 +272,13 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments hasByRefLikeArguments = true; // By-ref-like values live exclusively on the stack and cannot be boxed to `object`. - // Instead of them, we prepare instances of `ByRefLikeProxy` wrappers that reference them. + // Instead of them, we prepare instances of `ByRefLikeReference` wrappers that reference them. #if NET9_0_OR_GREATER // TODO: perhaps we should cache these `ConstructorInfo`s? - ConstructorInfo proxyCtor = typeof(ByRefLikeProxy<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single(); + ConstructorInfo referenceCtor = typeof(ByRefLikeReference<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single(); #else - ConstructorInfo proxyCtor = ByRefLikeProxyMethods.Constructor; + ConstructorInfo referenceCtor = ByRefLikeReferenceMethods.Constructor; #endif if (dereferencedArgumentType.IsConstructedGenericType) { @@ -287,21 +286,21 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments if (typeDef == typeof(ReadOnlySpan<>)) { var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; - proxyCtor = typeof(ReadOnlySpanProxy<>).MakeGenericType(typeArg).GetConstructors().Single(); + referenceCtor = typeof(ReadOnlySpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); } else if (typeDef == typeof(Span<>)) { var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; - proxyCtor = typeof(SpanProxy<>).MakeGenericType(typeArg).GetConstructors().Single(); + referenceCtor = typeof(SpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); } } - var proxy = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeProxy)); + var proxy = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeReference)); method.CodeBuilder.AddStatement( new AssignStatement( proxy, new NewInstanceExpression( - proxyCtor, + referenceCtor, new TypeTokenExpression(dereferencedArgumentType), new AddressOfExpression(dereferencedArgument)))); @@ -329,7 +328,7 @@ public void CopyOut(LocalReference argumentsArray) var dereferencedArgument = new IndirectReference(arguments[i]); var dereferencedArgumentType = dereferencedArgument.Type; - // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // Note that we don't need special logic for by-ref-like values / `ByRefLikeReference` here, // since `ConvertArgumentFromObjectExpression` knows how to deal with those. method.CodeBuilder.AddStatement( @@ -355,15 +354,15 @@ public void InvalidateByRefLikeProxies(LocalReference argumentsArray) if (dereferencedArgumentType.IsByRefLikeSafe()) { - // The `ByRefLikeProxy` invocation argument must be rendered unusable + // The `ByRefLikeReference` invocation argument must be rendered unusable // at the end of the (intercepted) method invocation, since it references // a method argument that is about to be popped off the stack. method.CodeBuilder.AddStatement( new MethodInvocationExpression( new AsTypeExpression( new ArrayElementReference(argumentsArray, i), - typeof(ByRefLikeProxy)), - ByRefLikeProxyMethods.Invalidate, + typeof(ByRefLikeReference)), + ByRefLikeReferenceMethods.Invalidate, argumentType.IsByRef ? argument : new AddressOfExpression(argument))); // Make the unusable proxy unreachable by erasing it from the invocation arguments array. @@ -392,7 +391,7 @@ public void Return(LocalReference invocation) returnValue, new MethodInvocationExpression(invocation, InvocationMethods.GetReturnValue))); - // Note that we don't need special logic for by-ref-like values / `ByRefLikeProxy` here, + // Note that we don't need special logic for by-ref-like values / `ByRefLikeReference` here, // since `ConvertArgumentFromObjectExpression` knows how to deal with those. // Emit code to ensure a value type return type is not null, otherwise the cast will cause a null-deref diff --git a/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs b/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs deleted file mode 100644 index 8da7dcea4..000000000 --- a/src/Castle.Core/DynamicProxy/IByRefLikeProxy.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#if FEATURE_BYREFLIKE - -#nullable enable - -using System; - -namespace Castle.DynamicProxy -{ -#if NET9_0_OR_GREATER - /// - /// Permits indirect access to by-ref-like argument values during method interception. - /// - /// - /// Instances of by-ref-like (ref struct) types live exclusively on the evaluation stack. - /// Therefore, they cannot be boxed and put into the -typed array. - /// DynamicProxy replaces these unboxable values with references - /// (or alternatively, in the case of spans, with or ), - /// which grant you indirect read/write access to the actual values. - /// - /// These references are only valid for the duration of the intercepted method call. - /// Any attempt to use it beyond that will result in a . - /// - /// - /// A by-ref-like (ref struct) type. - public interface IByRefLikeProxy where TByRefLike : struct, allows ref struct - { - ref TByRefLike Value { get; } - } -#endif - - /// - /// Permits indirect access to -typed argument values during method interception. - /// - /// - /// is a by-ref-like (ref struct) type, which means that - /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed - /// and put into the -typed array. - /// DynamicProxy replaces these unboxable values with references, - /// which grant you indirect read/write access to the actual value. - /// - /// These references are only valid for the duration of the intercepted method call. - /// Any attempt to use it beyond that will result in a . - /// - /// - public interface IReadOnlySpanProxy - { - ref ReadOnlySpan Value { get; } - } - - /// - /// Permits indirect access to -typed argument values during method interception. - /// - /// - /// is a by-ref-like (ref struct) type, which means that - /// instances of it live exclusively on the evaluation stack. Therefore, they cannot be boxed - /// and put into the -typed array. - /// DynamicProxy replaces these unboxable values with references, - /// which grant you indirect read/write access to the actual value. - /// - /// These references are only valid for the duration of the intercepted method call. - /// Any attempt to use it beyond that will result in a . - /// - /// - public interface ISpanProxy - { - ref Span Value { get; } - } -} - -#endif diff --git a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs similarity index 66% rename from src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs rename to src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs index 33df2be3b..5e0b81a56 100644 --- a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeProxyMethods.cs +++ b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs @@ -21,15 +21,13 @@ namespace Castle.DynamicProxy.Tokens using System.Linq; using System.Reflection; - using Castle.DynamicProxy.Internal; - - internal static class ByRefLikeProxyMethods + internal static class ByRefLikeReferenceMethods { - public static ConstructorInfo Constructor = typeof(ByRefLikeProxy).GetConstructors().Single(); + public static ConstructorInfo Constructor = typeof(ByRefLikeReference).GetConstructors().Single(); - public static MethodInfo GetPtr = typeof(ByRefLikeProxy).GetMethod(nameof(ByRefLikeProxy.GetPtr))!; + public static MethodInfo GetPtr = typeof(ByRefLikeReference).GetMethod(nameof(ByRefLikeReference.GetPtr))!; - public static MethodInfo Invalidate = typeof(ByRefLikeProxy).GetMethod(nameof(ByRefLikeProxy.Invalidate))!; + public static MethodInfo Invalidate = typeof(ByRefLikeReference).GetMethod(nameof(ByRefLikeReference.Invalidate))!; } } From 095ab6ce2398023b632f6834a7a3eb83a65af7bd Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Mon, 26 Jan 2026 23:44:25 +0100 Subject: [PATCH 5/8] Hide internals from debugger --- src/Castle.Core/DynamicProxy/ByRefLikeReference.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index f245b09b8..f99f22b6f 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -21,6 +21,7 @@ namespace Castle.DynamicProxy { using System; using System.ComponentModel; + using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -72,7 +73,10 @@ namespace Castle.DynamicProxy [EditorBrowsable(EditorBrowsableState.Never)] public unsafe class ByRefLikeReference { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Type type; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private nint ptr; /// From e8393753bf182ad6b8a8879abefcb35004f62b02 Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Tue, 27 Jan 2026 21:17:59 +0100 Subject: [PATCH 6/8] Add new documentation about by-ref-like parameters --- docs/dynamicproxy-by-ref-like-parameters.md | 79 +++++++++++++++++++++ docs/dynamicproxy.md | 1 + 2 files changed, 80 insertions(+) create mode 100644 docs/dynamicproxy-by-ref-like-parameters.md diff --git a/docs/dynamicproxy-by-ref-like-parameters.md b/docs/dynamicproxy-by-ref-like-parameters.md new file mode 100644 index 000000000..9c47269cf --- /dev/null +++ b/docs/dynamicproxy-by-ref-like-parameters.md @@ -0,0 +1,79 @@ +# Support for byref-like (`ref struct`) parameters + +Starting with version 6.0.0 of the library, and when targeting .NET 8 or later, DynamicProxy has comprehensive support for byref-like types. + + +## What are byref-like types? + +Byref-like types – also known as `ref struct` types in C# – are a special category of types that by definition live exclusively on the evaluation stack. Therefore, they can never be found on the heap, or be parts of heap-allocated objects. This implies that unlike other value types, they cannot be boxed (converted to `object`). + + +## How does DynamicProxy place `ref struct` values into an `IInvocation`? + +The impossibility of converting byref-like values to `object` poses a fundamental problem for DynamicProxy, which represents all of an intercepted method's arguments as well as the intended return value as `object`s in an `IInvocation` instance: arguments are placed in the `object[]`-typed `IInvocation.Arguments` property, and the intended return value can be set via the `object`-typed `IInvocation.ReturnValue` property. So how can `ref struct`-typed argument values possibly appear in `IInvocation`? + +The answer is that they cannot. DynamicProxy substitutes `ref struct` argument values with values of different, boxable types: + + * `System.Span` values are replaced with instances of `Castle.DynamicProxy.SpanReference`. + * `System.ReadOnlySpan` values are replaced with instances of `Castle.DynamicProxy.ReadOnlySpanReference`. + * Values of any other `ref struct` type `TByRefLike` are replaced with instances of `Castle.DynamicProxy.ByRefLikeReference` (for .NET 9+) or with `Castle.DynamicProxy.ByRefLikeReference` (for .NET 8). + +Here is a diagram showing these types' hierarchy: +``` + (not for direct use) (only available on .NET 9+) ++----------------------+ +----------------------------------+ +--------------------------------+ +| ByRefLikeReference | <----- | ByRefLikeReference | <------- | ReadOnlySpanReference | ++----------------------+ +==================================+ \ +================================+ + | +Value: ref TByRefLike | \ | +Value: ref ReadOnlySpan | + +----------------------------------+ \ +-------------------------------++ + \ + \ +------------------------+ + \ | SpanReference | + ---- +========================+ + | +Value: ref Span | + +------------------------+ +``` + +These types do not contain the actual byref-like values, but (like their names imply) they hold a "reference" that they can resolve to the values. With the exception of the non-generic `ByRefLikeReference` substitute type (which is meant for use by the DynamicProxy runtime, not by user code!), all of these expose a `ref`-returning `Value` property which can be used to: + + * read the actual byref-like argument or return value; or, + * for `ref` and `out` parameters, update the parameter. + + +## Basic usage example + +```csharp +public interface TypeToBeProxied +{ + void Method(ref Span arg); +} + +var proxy = proxyGenerator.CreateInterfaceProxyWithoutTarget(new MethodInterceptor()); + +class MethodInterceptor : IInterceptor +{ + public void Intercept(IInvocation invocation) + { + var argRef = (SpanReference)invocation.Arguments[0]; + Span arg = argRef.Value; // read the argument value + argRef.Value = arg[0..^1]; // update the parameter (only makes sense for `ref` and `out` parameters) + } +} +``` + + +## Rules for safe & correct usage + + 1. ✅ The only permissible interaction with values of the above-mentioned substitute types – `SpanReference`, `ByRefLikeReference`, etc. – is reading and writing their `Value` property. + + 2. 🚫 The substitutes may only be accessed while the intercepted method is running. Once it returns, the substitute values become invalid, and any further attempts at using them in any way will cause immediate `AccessViolationException`s. (The reason for this is that the substitutes reference storage locations in the intercepted method's stack frame. Once the method stops executing, its stack frame is popped off the evaluation stack, which effectively ends the method's arguments' lifetime.) + + - DynamicProxy tries to prevent you from making this mistake by actively erasing the substitute values from the `IInvocation` instance once the intercepted method returns to the caller. + + - Note also that the substitute values cannot survive an async `await` or `yield return` boundary for the same reason. (This is not unlike the C# language rule which forbids the use of byref-like parameters in `async` methods.) + + - A final consequence of this is that `ref struct` arguments will be unavailable in an interception pipeline restarted by `IInvocationProceedInfo` / `invocation.CaptureProceedInfo`. + + 3. 🚫 It is very strongly recommended that values of the substitute types not be copied anywhere else. DynamicProxy places them in specific spots inside an `IInvocation` instance, and that is precisely where they should stay. Don't copy them into variables for later use, or from one `IInvocation.Arguments` array position to another, etc. + + - DynamicProxy performs some checks against this practice, and if any such attempt is detected, an `AccessViolationException` gets thrown. diff --git a/docs/dynamicproxy.md b/docs/dynamicproxy.md index d729e9a2c..4816e4b68 100644 --- a/docs/dynamicproxy.md +++ b/docs/dynamicproxy.md @@ -18,6 +18,7 @@ If you're new to DynamicProxy you can read a [quick introduction](dynamicproxy-i * [Make your supporting classes serializable](dynamicproxy-serializable-types.md) * [Use proxy generation hooks and interceptor selectors for fine grained control](dynamicproxy-fine-grained-control.md) * [SRP applies to interceptors](dynamicproxy-srp-applies-to-interceptors.md) +* [Support for byref-like (`ref struct`) parameters](dynamicproxy-by-ref-like-parameters.md) * [Behavior of by-reference parameters during interception](dynamicproxy-by-ref-parameters.md) * [Optional parameter value limitations](dynamicproxy-optional-parameter-value-limitations.md) * [Asynchronous interception](dynamicproxy-async-interception.md) From a70bac3b5be30dd97ad9f6d7ad6047506e465642 Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Wed, 28 Jan 2026 22:16:13 +0100 Subject: [PATCH 7/8] Update `ref/` contract files --- ref/Castle.Core-net8.0.cs | 21 +++++++++++++++++++++ ref/Castle.Core-net9.0.cs | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index c3856114a..5a9c621df 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2422,6 +2422,15 @@ public virtual void MethodsInspected() { } public virtual void NonProxyableMemberNotification(System.Type type, System.Reflection.MemberInfo memberInfo) { } public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.MethodInfo methodInfo) { } } + public class ByRefLikeReference + { + [System.CLSCompliant(false)] + public ByRefLikeReference(System.Type type, void* ptr) { } + [System.CLSCompliant(false)] + public unsafe void* GetPtr(System.Type checkType) { } + [System.CLSCompliant(false)] + public unsafe void Invalidate(void* checkPtr) { } + } public class CustomAttributeInfo : System.IEquatable { public CustomAttributeInfo(System.Reflection.ConstructorInfo constructor, object?[] constructorArgs) { } @@ -2690,6 +2699,18 @@ public static bool IsAccessible(System.Reflection.MethodBase method, [System.Dia public static bool IsProxy(object? instance) { } public static bool IsProxyType(System.Type type) { } } + public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference + { + [System.CLSCompliant(false)] + public ReadOnlySpanReference(System.Type type, void* ptr) { } + public System.ReadOnlySpan<>& Value { get; } + } + public class SpanReference : Castle.DynamicProxy.ByRefLikeReference + { + [System.CLSCompliant(false)] + public SpanReference(System.Type type, void* ptr) { } + public System.Span<>& Value { get; } + } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor { public StandardInterceptor() { } diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index ff07f7f7d..87d3f4334 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2422,6 +2422,22 @@ public virtual void MethodsInspected() { } public virtual void NonProxyableMemberNotification(System.Type type, System.Reflection.MemberInfo memberInfo) { } public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.MethodInfo methodInfo) { } } + public class ByRefLikeReference + { + [System.CLSCompliant(false)] + public ByRefLikeReference(System.Type type, void* ptr) { } + [System.CLSCompliant(false)] + public unsafe void* GetPtr(System.Type checkType) { } + [System.CLSCompliant(false)] + public unsafe void Invalidate(void* checkPtr) { } + } + public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeReference + where TByRefLike : struct + { + [System.CLSCompliant(false)] + public ByRefLikeReference(System.Type type, void* ptr) { } + public TByRefLike& Value { get; } + } public class CustomAttributeInfo : System.IEquatable { public CustomAttributeInfo(System.Reflection.ConstructorInfo constructor, object?[] constructorArgs) { } @@ -2700,6 +2716,16 @@ public static bool IsAccessible(System.Reflection.MethodBase method, [System.Dia public static bool IsProxy(object? instance) { } public static bool IsProxyType(System.Type type) { } } + public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference> + { + [System.CLSCompliant(false)] + public ReadOnlySpanReference(System.Type type, void* ptr) { } + } + public class SpanReference : Castle.DynamicProxy.ByRefLikeReference> + { + [System.CLSCompliant(false)] + public SpanReference(System.Type type, void* ptr) { } + } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor { public StandardInterceptor() { } From a3126c6c0820abaab048ce89269e1ae8dd97e7c1 Mon Sep 17 00:00:00 2001 From: Dominique Schuppli Date: Thu, 29 Jan 2026 00:14:30 +0100 Subject: [PATCH 8/8] Add unit tests for `ByRefLikeReference` & subtypes --- .../Castle.Core.Tests.csproj | 1 + .../ByRefLikeReferenceTestCase.cs | 151 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs diff --git a/src/Castle.Core.Tests/Castle.Core.Tests.csproj b/src/Castle.Core.Tests/Castle.Core.Tests.csproj index 1bb03c0e1..9f2af28ee 100644 --- a/src/Castle.Core.Tests/Castle.Core.Tests.csproj +++ b/src/Castle.Core.Tests/Castle.Core.Tests.csproj @@ -4,6 +4,7 @@ net8.0;net9.0;net10.0;net462 + True diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs new file mode 100644 index 000000000..8904196c1 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -0,0 +1,151 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if FEATURE_BYREFLIKE + +#nullable enable +#pragma warning disable CS8500 + +namespace Castle.DynamicProxy.Tests.ByRefLikeSupport +{ + using System; +#if NET9_0_OR_GREATER + using System.Runtime.CompilerServices; +#endif + + using NUnit.Framework; + + /// + /// Tests for the substitute types used by DynamicProxy to implement by-ref-like parameter and return type support. + /// + [TestFixture] + public class ByRefLikeReferenceTestCase + { + #region `ByRefLikeReference` + + [Test] + public unsafe void Ctor_throws_if_non_by_ref_like_type() + { + Assert.Throws(() => + { + bool local = default; + _ = new ByRefLikeReference(typeof(bool), &local); + }); + } + + [Test] + public unsafe void Ctor_succeeds_if_by_ref_like_type() + { + ReadOnlySpan local = default; + _ = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + } + + [Test] + public unsafe void Invalidate_throws_if_address_mismatch() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + Assert.Throws(() => + { + ReadOnlySpan otherLocal = default; + reference.Invalidate(&otherLocal); + }); + } + + [Test] + public unsafe void Invalidate_succeeds_if_address_match() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + reference.Invalidate(&local); + } + + [Test] + public unsafe void GetPtr_throws_if_type_mismatch() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + Assert.Throws(() => reference.GetPtr(typeof(bool))); + } + + [Test] + public unsafe void GetPtr_returns_ctor_address_if_type_match() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var ptr = reference.GetPtr(typeof(ReadOnlySpan)); + Assert.True(ptr == &local); + } + + [Test] + public unsafe void GetPtr_throws_after_Invalidate() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + reference.Invalidate(&local); + Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); + } + + #endregion + + #region `ReadOnlySpanReference` + + // We do not repeat the above tests for `ReadOnlySpanReference` + // since it inherits the tested methods from `ByRefLikeReference`. + + public unsafe void ReadOnlySpanReference_ctor_throws_if_type_mismatch() + { + Assert.Throws(() => + { + ReadOnlySpan local = default; + _ = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + }); + } + + public unsafe void ReadOnlySpanReference_Value_returns_equal_span() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + Assert.True(reference.Value == "foo".AsSpan()); + } + +#if NET9_0_OR_GREATER + [Test] + public unsafe void ReadOnlySpanReference_Value_returns_same_span() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + Assert.True(Unsafe.AreSame(ref reference.Value, ref local)); + } +#endif + + [Test] + public unsafe void ReadOnlySpanReference_Value_can_update_original() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + reference.Value = "bar".AsSpan(); + Assert.True(local == "bar".AsSpan()); + } + + #endregion + + // We do not test `ByRefLikeReference` and `SpanReference` + // since these two types are practically identical to `ReadOnlySpanReference`. + } +} + +#pragma warning restore CS8500 + +#endif