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
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,18 @@ build.cmd

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

Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8
----------------------------------- | ------------------ | ----------------- | ------------------
`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark:
`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign:
Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8 | .NET 9
------------------------------------------- | ------------------ | ----------------- | ------------------ | ------------
`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: | :white_check_mark:
`FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark:
`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:
`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign:

* `FEATURE_APPDOMAIN` - enables support for features that make use of an AppDomain in the host.
* `FEATURE_ASSEMBLYBUILDER_SAVE` - enabled support for saving the dynamically generated proxy assembly.
* `FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` - enables support for by-ref-like (`ref struct`) types being used as generic type arguments.
* `FEATURE_BYREFLIKE` - enables support for by-ref-like (`ref struct`) types such as `Span<T>` and `ReadOnlySpan<T>`.
* `FEATURE_SERIALIZATION` - enables support for serialization of dynamic proxies and other types.
* `FEATURE_SYSTEM_CONFIGURATION` - enables features that use System.Configuration and the ConfigurationManager.
5 changes: 5 additions & 0 deletions buildscripts/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<LangVersion>14.0</LangVersion>
<NoWarn>$(NoWarn);CS1591;CS3014;CS3003;CS3001;CS3021</NoWarn>
<NoWarn>$(NoWarn);CS0612;CS0618</NoWarn> <!-- TODO: Remove this line once `[Obsolete]` members have been dealt with. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/castleproject/Core</RepositoryUrl>
<BuildVersion>0.0.0</BuildVersion>
Expand Down Expand Up @@ -70,6 +71,10 @@
<DefineConstants>$(DefineConstants);FEATURE_BYREFLIKE</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net9.0'">
<DefineConstants>$(DefineConstants);FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT</DefineConstants>
</PropertyGroup>

<ItemGroup>
<None Include="$(SolutionDir)docs\images\castle-logo.png" Pack="true" PackagePath=""/>
</ItemGroup>
Expand Down
190 changes: 182 additions & 8 deletions src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#if FEATURE_BYREFLIKE

#nullable enable
#pragma warning disable CS8500

namespace Castle.DynamicProxy.Tests
{
Expand Down Expand Up @@ -198,47 +199,179 @@ public virtual ByRefLike Method()
#region What values do interceptors see for by-ref-like arguments?

[Test]
public void By_ref_like_arguments_are_replaced_with_null_in_invocation()
public void By_ref_like_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(arg);
Assert.IsNull(interceptor.ObservedArg);
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
#endif
}

[Test]
public unsafe void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(arg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument);
var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!;
var unwrappedArg = *(ReadOnlySpan<char>*)wrappedArg.GetPointer();
Assert.AreEqual("original", unwrappedArg.ToString());
}

#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
[Test]
public void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(arg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
var unwrappedArg = wrappedArg.Get();
Assert.AreEqual("original", unwrappedArg.ToString());
}
#endif

[Test]
public void By_ref_like_in_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(in arg);
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
#endif
}

[Test]
public unsafe void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(in arg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument);
var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!;
var unwrappedArg = *(ReadOnlySpan<char>*)wrappedArg.GetPointer();
Assert.AreEqual("original", unwrappedArg.ToString());
}

#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
[Test]
public void By_ref_like_in_arguments_are_replaced_with_null_in_invocation()
public void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(in arg);
Assert.IsNull(interceptor.ObservedArg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
var unwrappedArg = wrappedArg.Get();
Assert.AreEqual("original", unwrappedArg.ToString());
}
#endif

[Test]
public void By_ref_like_ref_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(ref arg);
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
#endif
}

[Test]
public void By_ref_like_ref_arguments_are_replaced_with_null_in_invocation()
public unsafe void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(ref arg);
Assert.IsNull(interceptor.ObservedArg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument);
var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!;
var unwrappedArg = *(ReadOnlySpan<char>*)wrappedArg.GetPointer();
Assert.AreEqual("original", unwrappedArg.ToString());
}

#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
[Test]
public void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(ref arg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
var unwrappedArg = wrappedArg.Get();
Assert.AreEqual("original", unwrappedArg.ToString());
}
#endif

// Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values
// of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter").
// DynamicProxy does not distinguish between `ref` and `out` in this regard.
[Test]
public void By_ref_like_out_arguments_are_replaced_with_null_in_invocation()
public void By_ref_like_out_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(out arg);
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
#endif
}

// Should theoretically be as above (read comment there), but isn't. To be revisited later!
[Test]
[Ignore("Have not yet found out why byref-like `out` arguments are initially set to their default values unlike `ref` ones.")]
public unsafe void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(out arg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument);
var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!;
var unwrappedArg = *(ReadOnlySpan<char>*)wrappedArg.GetPointer();
Assert.AreEqual("original", unwrappedArg.ToString());
}

#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
// Should theoretically be as above (read comment there), but isn't. To be revisited later!
[Test]
[Ignore("Have not yet found out why byref-like `out` arguments are initially set to their default values unlike `ref` ones.")]
public void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(interceptor);
var arg = "original".AsSpan();
proxy.Method(out arg);
Assert.IsNull(interceptor.ObservedArg);
Assume.That(interceptor.ObservedArg is ByRefLikeArgument<ReadOnlySpan<char>>);
var wrappedArg = (ByRefLikeArgument<ReadOnlySpan<char>>)interceptor.ObservedArg!;
var unwrappedArg = wrappedArg.Get();
Assert.AreEqual("original", unwrappedArg.ToString());
}
#endif

#endregion

Expand Down Expand Up @@ -367,6 +500,43 @@ public void By_ref_like_out_arguments_cannot_be_set_by_target()

#endregion

#region Memory safety

// Byref-like arguments live exclusively on the evaluation stack.
// We need to make sure that references to them (such as those in `IInvocation.Arguments`)
// do not have a longer lifetime than the arguments themselves.

[Test]
public unsafe void Cannot_use_ByRefLikeArgument_GetPointer_after_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
proxy.Method(default);
var byRefLikeArg = (ByRefLikeArgument)interceptor.ObservedArg!;
Assert.Throws<ObjectDisposedException>(() => _ = byRefLikeArg.GetPointer());
}

[Test]
public void Cannot_use_ByRefLikeArgument_Get_after_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
proxy.Method(default);
var byRefLikeArg = (ReadOnlySpanArgument<char>)interceptor.ObservedArg!;
Assert.Throws<ObjectDisposedException>(() => _ = byRefLikeArg.Get());
}

[Test]
public void ByRefLikeArguments_are_erased_from_invocation_Arguments_after_invocation()
{
var interceptor = new ObservingInterceptor();
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
proxy.Method(default);
Assert.IsNull(interceptor.AllArguments![0]);
}

#endregion

public class HasMethodWithSpanParameter
{
public string? RecordedArg;
Expand Down Expand Up @@ -408,14 +578,18 @@ public virtual void Method(out ReadOnlySpan<char> arg)

public class ObservingInterceptor : IInterceptor
{
public object?[]? AllArguments;
public object? ObservedArg;

public void Intercept(IInvocation invocation)
{
AllArguments = invocation.Arguments;
ObservedArg = invocation.Arguments[0];
}
}
}
}

#pragma warning restore CS8500

#endif
4 changes: 2 additions & 2 deletions src/Castle.Core/Castle.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="..\..\buildscripts\common.props"></Import>

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

<PropertyGroup>
Expand Down Expand Up @@ -37,7 +37,7 @@
<PackageReference Include="System.Diagnostics.EventLog" Version="8.0.2" />
</ItemGroup>

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

Expand Down
20 changes: 20 additions & 0 deletions src/Castle.Core/DynamicProxy/AbstractInvocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ public void Proceed()
finally
{
currentInterceptorIndex--;

#if FEATURE_BYREFLIKE
if (currentInterceptorIndex < 0)
{
// TODO: This will require further optimization;
// we should not iterate through the arguments array on a hot code path!
for (int i = 0; i < arguments.Length; ++i)
{
if (arguments[i] is ByRefLikeArgument byRefLikeArg)
{
// Invalidate the `ByRefLikeArgument` in case someone copied it away
// (i. e. let it escape the lifetime of the invocation and its arguments on the stack):
byRefLikeArg.Dispose();

// Reset the argument so the `ByRefLikeArgument` becomes eligible for GC sooner:
arguments[i] = null;
}
}
}
#endif
}
}

Expand Down
Loading
Loading