diff --git a/docs/dynamicproxy-by-ref-like-parameters.md b/docs/dynamicproxy-by-ref-like-parameters.md new file mode 100644 index 0000000000..9c47269cf2 --- /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 d729e9a2cc..4816e4b683 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) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index c3856114a4..5a9c621df0 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 ff07f7f7da..87d3f43345 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() { } diff --git a/src/Castle.Core.Tests/Castle.Core.Tests.csproj b/src/Castle.Core.Tests/Castle.Core.Tests.csproj index 1bb03c0e11..9f2af28ee6 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 0000000000..8904196c10 --- /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 diff --git a/src/Castle.Core/Castle.Core.csproj b/src/Castle.Core/Castle.Core.csproj index 7d45404a57..b6d3999817 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/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs new file mode 100644 index 0000000000..f99f22b6fd --- /dev/null +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -0,0 +1,284 @@ +// 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 +{ + using System; + using System.ComponentModel; + using System.Diagnostics; + 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. + // + // 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 `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 `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 `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 `ByRefLikeReference` instance out of the `IInvocation.Arguments` + // and uses it from another thread. + // + // 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! This type should only be used by DynamicProxy internals. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe class ByRefLikeReference + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Type type; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private nint 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) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + + if (ptr == null) + { + throw new ArgumentNullException(nameof(ptr)); + } + + this.type = type; + 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) + { + throw new AccessViolationException(); + } + + return GetPtrNocheck(); + } + + internal void* GetPtrNocheck() + { + var ptr = (void*)Volatile.Read(ref this.ptr); + + if (ptr == null) + { + throw new AccessViolationException(); + } + + 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); + + if (ptr == null || checkPtr != ptr) + { + throw new AccessViolationException(); + } + } + } + +#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, 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 + { + /// + /// 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)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + public ref TByRefLike Value + { + get + { + return ref *(TByRefLike*)GetPtrNocheck(); + } + } + } +#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 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 + : ByRefLikeReference> +#else + : ByRefLikeReference +#endif + { + /// + /// 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)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + +#if !NET9_0_OR_GREATER + public ref ReadOnlySpan Value + { + get + { + return ref *(ReadOnlySpan*)GetPtrNocheck(); + } + } +#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 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 + : ByRefLikeReference> +#else + : ByRefLikeReference +#endif + { + /// + /// 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)) + { + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + +#if !NET9_0_OR_GREATER + public ref Span Value + { + get + { + return ref *(Span*)GetPtrNocheck(); + } + } +#endif + } +} + +#pragma warning restore CS8500 + +#endif diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs index e4ab715a38..d0c0a199c0 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 209a8bb72b..e13b1f2d26 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, ByRefLikeReferenceMethods.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 0000000000..871004b327 --- /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 04958beaf8..6c2fa5f6c8 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 / `ByRefLikeReference` 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 `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 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)), + ByRefLikeReferenceMethods.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 (`ByRefLikeReference.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 624d241d16..d022f84bb6 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -16,13 +16,14 @@ 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 - using Castle.Core.Internal; using Castle.DynamicProxy.Contributors; using Castle.DynamicProxy.Generators.Emitters; using Castle.DynamicProxy.Generators.Emitters.SimpleAST; @@ -103,7 +104,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 `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); ctorArguments = ModifyArguments(@class, ctorArguments); @@ -117,7 +121,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 +129,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 +238,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 +269,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 `ByRefLikeReference` wrappers that reference them. + +#if NET9_0_OR_GREATER + // TODO: perhaps we should cache these `ConstructorInfo`s? + ConstructorInfo referenceCtor = typeof(ByRefLikeReference<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single(); +#else + ConstructorInfo referenceCtor = ByRefLikeReferenceMethods.Constructor; +#endif + if (dereferencedArgumentType.IsConstructedGenericType) + { + var typeDef = dereferencedArgumentType.GetGenericTypeDefinition(); + if (typeDef == typeof(ReadOnlySpan<>)) + { + var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; + referenceCtor = typeof(ReadOnlySpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); + } + else if (typeDef == typeof(Span<>)) + { + var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; + referenceCtor = typeof(SpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); + } + } + + var proxy = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeReference)); method.CodeBuilder.AddStatement( new AssignStatement( - new ArrayElementReference(argumentsArray, i), - NullExpression.Instance)); + proxy, + new NewInstanceExpression( + referenceCtor, + 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 +328,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 / `ByRefLikeReference` 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 `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(ByRefLikeReference)), + ByRefLikeReferenceMethods.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 +385,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 / `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 + 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/ByRefLikeReferenceMethods.cs b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs new file mode 100644 index 0000000000..5e0b81a567 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs @@ -0,0 +1,34 @@ +// 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; + + internal static class ByRefLikeReferenceMethods + { + public static ConstructorInfo Constructor = typeof(ByRefLikeReference).GetConstructors().Single(); + + public static MethodInfo GetPtr = typeof(ByRefLikeReference).GetMethod(nameof(ByRefLikeReference.GetPtr))!; + + public static MethodInfo Invalidate = typeof(ByRefLikeReference).GetMethod(nameof(ByRefLikeReference.Invalidate))!; + } +} + +#endif