Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/dynamicproxy-by-ref-like-parameters.md
Original file line number Diff line number Diff line change
@@ -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<T>` values are replaced with instances of `Castle.DynamicProxy.SpanReference<T>`.
* `System.ReadOnlySpan<T>` values are replaced with instances of `Castle.DynamicProxy.ReadOnlySpanReference<T>`.
* Values of any other `ref struct` type `TByRefLike` are replaced with instances of `Castle.DynamicProxy.ByRefLikeReference<TByRefLike>` (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<TByRefLike> | <------- | ReadOnlySpanReference<T> |
+----------------------+ +==================================+ \ +================================+
| +Value: ref TByRefLike | \ | +Value: ref ReadOnlySpan<T> |
+----------------------------------+ \ +-------------------------------++
\
\ +------------------------+
\ | SpanReference<T> |
---- +========================+
| +Value: ref Span<T> |
+------------------------+
```

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<int> arg);
}

var proxy = proxyGenerator.CreateInterfaceProxyWithoutTarget<TypeToBeProxied>(new MethodInterceptor());

class MethodInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var argRef = (SpanReference<int>)invocation.Arguments[0];
Span<int> 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 &ndash; `SpanReference<T>`, `ByRefLikeReference<TByRefLike>`, etc. &ndash; 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.
1 change: 1 addition & 0 deletions docs/dynamicproxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions ref/Castle.Core-net8.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Castle.DynamicProxy.CustomAttributeInfo>
{
public CustomAttributeInfo(System.Reflection.ConstructorInfo constructor, object?[] constructorArgs) { }
Expand Down Expand Up @@ -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<T> : Castle.DynamicProxy.ByRefLikeReference
{
[System.CLSCompliant(false)]
public ReadOnlySpanReference(System.Type type, void* ptr) { }
public System.ReadOnlySpan<>& Value { get; }
}
public class SpanReference<T> : 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() { }
Expand Down
26 changes: 26 additions & 0 deletions ref/Castle.Core-net9.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TByRefLike> : 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<Castle.DynamicProxy.CustomAttributeInfo>
{
public CustomAttributeInfo(System.Reflection.ConstructorInfo constructor, object?[] constructorArgs) { }
Expand Down Expand Up @@ -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<T> : Castle.DynamicProxy.ByRefLikeReference<System.ReadOnlySpan<T>>
{
[System.CLSCompliant(false)]
public ReadOnlySpanReference(System.Type type, void* ptr) { }
}
public class SpanReference<T> : Castle.DynamicProxy.ByRefLikeReference<System.Span<T>>
{
[System.CLSCompliant(false)]
public SpanReference(System.Type type, void* ptr) { }
}
public class StandardInterceptor : Castle.DynamicProxy.IInterceptor
{
public StandardInterceptor() { }
Expand Down
1 change: 1 addition & 0 deletions src/Castle.Core.Tests/Castle.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0;net462</TargetFrameworks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests for the substitute types used by DynamicProxy to implement by-ref-like parameter and return type support.
/// </summary>
[TestFixture]
public class ByRefLikeReferenceTestCase
{
#region `ByRefLikeReference`

[Test]
public unsafe void Ctor_throws_if_non_by_ref_like_type()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
bool local = default;
_ = new ByRefLikeReference(typeof(bool), &local);
});
}

[Test]
public unsafe void Ctor_succeeds_if_by_ref_like_type()
{
ReadOnlySpan<char> local = default;
_ = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
}

[Test]
public unsafe void Invalidate_throws_if_address_mismatch()
{
ReadOnlySpan<char> local = default;
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
Assert.Throws<AccessViolationException>(() =>
{
ReadOnlySpan<char> otherLocal = default;
reference.Invalidate(&otherLocal);
});
}

[Test]
public unsafe void Invalidate_succeeds_if_address_match()
{
ReadOnlySpan<char> local = default;
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
reference.Invalidate(&local);
}

[Test]
public unsafe void GetPtr_throws_if_type_mismatch()
{
ReadOnlySpan<char> local = default;
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
Assert.Throws<AccessViolationException>(() => reference.GetPtr(typeof(bool)));
}

[Test]
public unsafe void GetPtr_returns_ctor_address_if_type_match()
{
ReadOnlySpan<char> local = default;
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
var ptr = reference.GetPtr(typeof(ReadOnlySpan<char>));
Assert.True(ptr == &local);
}

[Test]
public unsafe void GetPtr_throws_after_Invalidate()
{
ReadOnlySpan<char> local = default;
var reference = new ByRefLikeReference(typeof(ReadOnlySpan<char>), &local);
reference.Invalidate(&local);
Assert.Throws<AccessViolationException>(() => reference.GetPtr(typeof(ReadOnlySpan<char>)));
}

#endregion

#region `ReadOnlySpanReference<T>`

// We do not repeat the above tests for `ReadOnlySpanReference<T>`
// since it inherits the tested methods from `ByRefLikeReference`.

public unsafe void ReadOnlySpanReference_ctor_throws_if_type_mismatch()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
ReadOnlySpan<bool> local = default;
_ = new ReadOnlySpanReference<char>(typeof(ReadOnlySpan<bool>), &local);
});
}

public unsafe void ReadOnlySpanReference_Value_returns_equal_span()
{
ReadOnlySpan<char> local = "foo".AsSpan();
var reference = new ReadOnlySpanReference<char>(typeof(ReadOnlySpan<char>), &local);
Assert.True(reference.Value == "foo".AsSpan());
}

#if NET9_0_OR_GREATER
[Test]
public unsafe void ReadOnlySpanReference_Value_returns_same_span()
{
ReadOnlySpan<char> local = "foo".AsSpan();
var reference = new ReadOnlySpanReference<char>(typeof(ReadOnlySpan<char>), &local);
Assert.True(Unsafe.AreSame(ref reference.Value, ref local));
}
#endif

[Test]
public unsafe void ReadOnlySpanReference_Value_can_update_original()
{
ReadOnlySpan<char> local = "foo".AsSpan();
var reference = new ReadOnlySpanReference<char>(typeof(ReadOnlySpan<char>), &local);
reference.Value = "bar".AsSpan();
Assert.True(local == "bar".AsSpan());
}

#endregion

// We do not test `ByRefLikeReference<TByRefLike>` and `SpanReference<T>`
// since these two types are practically identical to `ReadOnlySpanReference<T>`.
}
}

#pragma warning restore CS8500

#endif
1 change: 1 addition & 0 deletions src/Castle.Core/Castle.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net462;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>

<PropertyGroup>
Expand Down
Loading
Loading