Skip to content

Commit 82664d5

Browse files
committed
Add .NET 9 TFM & more type-safe ByRefLikeArgument<>
(.NET 9 is needed for that generic version of the wrapper type because older runtimes did not allow using `ref struct`s as generic type args.)
1 parent 503fdc6 commit 82664d5

5 files changed

Lines changed: 108 additions & 9 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ build.cmd
5050

5151
The following conditional compilation symbols (vertical) are currently defined for each of the build configurations (horizontal):
5252

53-
Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8
54-
----------------------------------- | ------------------ | ----------------- | ------------------
55-
`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
56-
`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
57-
`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark:
58-
`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
59-
`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
53+
Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8 | .NET 9
54+
------------------------------------------- | ------------------ | ----------------- | ------------------ | ------------
55+
`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
56+
`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
57+
`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: | :white_check_mark:
58+
`FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark:
59+
`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
60+
`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
6061

6162
* `FEATURE_APPDOMAIN` - enables support for features that make use of an AppDomain in the host.
6263
* `FEATURE_ASSEMBLYBUILDER_SAVE` - enabled support for saving the dynamically generated proxy assembly.
64+
* `FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` - enables support for by-ref-like (`ref struct`) types being used as generic type arguments.
6365
* `FEATURE_BYREFLIKE` - enables support for by-ref-like (`ref struct`) types such as `Span<T>` and `ReadOnlySpan<T>`.
6466
* `FEATURE_SERIALIZATION` - enables support for serialization of dynamic proxies and other types.
6567
* `FEATURE_SYSTEM_CONFIGURATION` - enables features that use System.Configuration and the ConfigurationManager.

buildscripts/common.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171
<DefineConstants>$(DefineConstants);FEATURE_BYREFLIKE</DefineConstants>
7272
</PropertyGroup>
7373

74+
<PropertyGroup Condition="'$(TargetFramework)'=='net9.0'">
75+
<DefineConstants>$(DefineConstants);FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT</DefineConstants>
76+
</PropertyGroup>
77+
7478
<ItemGroup>
7579
<None Include="$(SolutionDir)docs\images\castle-logo.png" Pack="true" PackagePath=""/>
7680
</ItemGroup>

src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,21 @@ public unsafe void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_
221221
Assert.AreEqual("original", unwrappedArg.ToString());
222222
}
223223

224+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
225+
[Test]
226+
public void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
227+
{
228+
var interceptor = new ObservingInterceptor();
229+
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
230+
var arg = "original".AsSpan();
231+
proxy.Method(arg);
232+
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
233+
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
234+
var unwrappedArg = wrappedArg.Get();
235+
Assert.AreEqual("original", unwrappedArg.ToString());
236+
}
237+
#endif
238+
224239
[Test]
225240
public void By_ref_like_in_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
226241
{
@@ -244,6 +259,21 @@ public unsafe void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgume
244259
Assert.AreEqual("original", unwrappedArg.ToString());
245260
}
246261

262+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
263+
[Test]
264+
public void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
265+
{
266+
var interceptor = new ObservingInterceptor();
267+
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(interceptor);
268+
var arg = "original".AsSpan();
269+
proxy.Method(in arg);
270+
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
271+
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
272+
var unwrappedArg = wrappedArg.Get();
273+
Assert.AreEqual("original", unwrappedArg.ToString());
274+
}
275+
#endif
276+
247277
[Test]
248278
public void By_ref_like_ref_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
249279
{
@@ -267,6 +297,21 @@ public unsafe void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgum
267297
Assert.AreEqual("original", unwrappedArg.ToString());
268298
}
269299

300+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
301+
[Test]
302+
public void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
303+
{
304+
var interceptor = new ObservingInterceptor();
305+
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(interceptor);
306+
var arg = "original".AsSpan();
307+
proxy.Method(ref arg);
308+
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
309+
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
310+
var unwrappedArg = wrappedArg.Get();
311+
Assert.AreEqual("original", unwrappedArg.ToString());
312+
}
313+
#endif
314+
270315
// Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values
271316
// of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter").
272317
// DynamicProxy does not distinguish between `ref` and `out` in this regard.
@@ -295,6 +340,23 @@ public unsafe void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgum
295340
Assert.AreEqual("original", unwrappedArg.ToString());
296341
}
297342

343+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
344+
// Should theoretically be as above (read comment there), but isn't. To be revisited later!
345+
[Test]
346+
[Ignore("Have not yet found out why byref-like `out` arguments are initially set to their default values unlike `ref` ones.")]
347+
public void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
348+
{
349+
var interceptor = new ObservingInterceptor();
350+
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(interceptor);
351+
var arg = "original".AsSpan();
352+
proxy.Method(out arg);
353+
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
354+
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
355+
var unwrappedArg = wrappedArg.Get();
356+
Assert.AreEqual("original", unwrappedArg.ToString());
357+
}
358+
#endif
359+
298360
#endregion
299361

300362
#region What values do proceeded-to targets see for by-ref-like arguments?

src/Castle.Core/Castle.Core.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Import Project="..\..\buildscripts\common.props"></Import>
44

55
<PropertyGroup>
6-
<TargetFrameworks>net8.0;net462;netstandard2.0</TargetFrameworks>
6+
<TargetFrameworks>net9.0;net8.0;net462;netstandard2.0</TargetFrameworks>
77
</PropertyGroup>
88

99
<PropertyGroup>
@@ -37,7 +37,7 @@
3737
<PackageReference Include="System.Diagnostics.EventLog" Version="8.0.2" />
3838
</ItemGroup>
3939

40-
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
40+
<ItemGroup Condition="'$(TargetFramework)'=='net8.0' Or '$(TargetFramework)'=='net9.0'">
4141
<PackageReference Include="System.Diagnostics.EventLog" Version="8.0.2" />
4242
</ItemGroup>
4343

src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace Castle.DynamicProxy
2020
{
2121
using System;
2222
using System.ComponentModel;
23+
using System.Runtime.CompilerServices;
2324

2425
/// <summary>
2526
/// Wraps a byref-like (<c>ref struct</c>) method argument
@@ -49,6 +50,36 @@ public ByRefLikeArgument(void* ptr)
4950
return ptr;
5051
}
5152
}
53+
54+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
55+
56+
/// <summary>
57+
/// Wraps a byref-like (<c>ref struct</c>) method argument
58+
/// such that it can be placed in the <see cref="IInvocation.Arguments"/> array during interception.
59+
/// </summary>
60+
public unsafe class ByRefLikeArgument<TByRefLike> : ByRefLikeArgument where TByRefLike : allows ref struct
61+
{
62+
/// <summary>
63+
/// Do not use this! Only generated proxies should construct instances this type.
64+
/// </summary>
65+
[CLSCompliant(false)]
66+
[EditorBrowsable(EditorBrowsableState.Never)]
67+
public ByRefLikeArgument(void* ptr)
68+
: base(ptr)
69+
{
70+
}
71+
72+
/// <summary>
73+
/// Gets the byref-like (<c>ref struct</c>) argument.
74+
/// </summary>
75+
public ref TByRefLike Get()
76+
{
77+
return ref Unsafe.AsRef<TByRefLike>(ptr);
78+
}
79+
}
80+
81+
#endif
82+
5283
}
5384

5485
#endif

0 commit comments

Comments
 (0)