Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Breaking Changes:
- Moved `Castle.Core.Logging.DiagnosticsLogger` into a separate NuGet package `Castle.Core-DiagnosticsLogger`, which renders the main package dependency-free for .NET 8+ (@snakefoot, #694)

Enhancements:
- Minimally improved support for methods having `ref struct` parameter and return types, such as `Span<T>`: Intercepting such methods caused the runtime to throw `InvalidProgramException` and `NullReferenceException` due to forbidden conversions of `ref struct` values when transferring them into & out of `IInvocation` instances. To prevent these exceptions from being thrown, such values now get replaced with `null` in `IInvocation`, and with `default` values in return values and `out` arguments. When proceeding to a target, the target methods likewise receive such nullified values. (@stakx, #665)
- Comprehensive support for proxying methods with byref-like (`ref struct`) parameter and return types, such as `Span<T>`. Until now, any `ref struct` values triggered exceptions during interception because DynamicProxy could not box them to `object` & transfer them into `IInvocation` instances. Now, byref-like values are represented in `IInvocation` instances with new substitute types `SpanReference<T>`, `ReadOnlySpanReference<T>`, and `ByRefLikeReference<TByRefLike>` which grant you indirect access to the actual values. Please [read the documentation for details](docs/dynamicproxy-byref-like-parameters.md). (@stakx, #665, #712)
- Restore ability on .NET 9 and later to save dynamic assemblies to disk using `PersistentProxyBuilder` (@stakx, #718)
- Configure SourceLink & `.snupkg` symbols package format (@Romfos, #722)
- Dependencies were updated
Expand Down
2 changes: 1 addition & 1 deletion buildscripts/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<DefineConstants>$(NetStandard20Constants)</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net8.0' Or '$(TargetFramework)'=='net9.0'">
<PropertyGroup Condition="'$(TargetFramework)'=='net8.0' Or '$(TargetFramework)'=='net9.0' Or '$(TargetFramework)'=='net10.0'">
<DefineConstants>$(DefineConstants);FEATURE_BYREFLIKE</DefineConstants>
</PropertyGroup>

Expand Down
106 changes: 106 additions & 0 deletions docs/dynamicproxy-byref-like-parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Support for byref-like (`ref struct`) parameters and return types

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 &ndash; also known as `ref struct` types in C# &ndash; 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


### Reading (and updating) a byref-like parameter

```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)
}
}
```


### Returning a byref-like value

```csharp
public interface TypeToBeProxied
{
ReadOnlySpan<char> Method();
}

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

class MethodInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var returnValueRef = (ReadOnlySpanReference<int>)invocation.ReturnValue;

var returnValue = "Hello there!".AsSpan();
returnValueRef.Value = returnValue;
}
}
```


## 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 and return types](dynamicproxy-byref-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 byref-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
Loading