From c8d9a8d785085cdaaebbf56ca8f1ada53b5d14d7 Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Mon, 16 Mar 2026 21:07:06 +0100 Subject: [PATCH 1/8] Throw less scary exceptions --- .../ByRefLikeSupport/ByRefLikeReferenceTestCase.cs | 6 +++--- src/Castle.Core/DynamicProxy/ByRefLikeReference.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs index 8717b567e..1696c05f7 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -56,7 +56,7 @@ public unsafe void Invalidate_throws_if_address_mismatch() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); - Assert.Throws(() => + Assert.Throws(() => { ReadOnlySpan otherLocal = default; reference.Invalidate(&otherLocal); @@ -76,7 +76,7 @@ public unsafe void GetPtr_throws_if_type_mismatch() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); - Assert.Throws(() => reference.GetPtr(typeof(bool))); + Assert.Throws(() => reference.GetPtr(typeof(bool))); } [Test] @@ -94,7 +94,7 @@ 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))); + Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); } #endregion diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 457f8f470..a8acb7048 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -109,7 +109,7 @@ public ByRefLikeReference(Type type, void* ptr) { if (checkType != type) { - throw new AccessViolationException(); + throw new ArgumentException($"The reference type {type.FullName} does not match the expected type {checkType.FullName}"); } return GetPtrNocheck(); @@ -121,7 +121,7 @@ public ByRefLikeReference(Type type, void* ptr) if (ptr == null) { - throw new AccessViolationException(); + throw new ObjectDisposedException("This reference was already invalidated"); } return ptr; @@ -138,7 +138,7 @@ public void Invalidate(void* checkPtr) if (ptr == null || checkPtr != ptr) { - throw new AccessViolationException(); + throw new InvalidOperationException($"BUG: Pointer mismatch on reference invalidation. Expected: {(nint)checkPtr:X16}, Actual: {(nint)ptr:X16}"); } } } From 8171d2cf39bfd5bbee6c6b6f75a44278956775d1 Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Mon, 16 Mar 2026 21:07:07 +0100 Subject: [PATCH 2/8] Allow to access by ref reference only from the owner thread Do not use Volatile anymore, as code is single-threaded now. Also use void* to simplify code --- .../ByRefLikeReferenceTestCase.cs | 32 +++++++++++++++ .../DynamicProxy/ByRefLikeReference.cs | 39 ++++++++++++------- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs index 1696c05f7..018fb77f7 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -20,6 +20,7 @@ namespace Castle.DynamicProxy.Tests.ByRefLikeSupport { using System; + using System.Threading.Tasks; #if NET9_0_OR_GREATER using System.Runtime.CompilerServices; #endif @@ -70,6 +71,17 @@ public unsafe void Invalidate_succeeds_if_address_match() var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); reference.Invalidate(&local); } + + [Test] + public unsafe void Invalidate_throws_when_access_from_other_thread() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var address = reference.GetPtr(typeof(ReadOnlySpan)); + var task = Task.Run(() => reference.Invalidate(address)); + var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; + StringAssert.Contains("thread", msg); + } [Test] public unsafe void GetPtr_throws_if_type_mismatch() @@ -96,6 +108,16 @@ public unsafe void GetPtr_throws_after_Invalidate() reference.Invalidate(&local); Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); } + + [Test] + public unsafe void GetPtr_throws_when_access_from_other_thread() + { + ReadOnlySpan local = default; + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var task = Task.Run(() => reference.GetPtr(typeof(ReadOnlySpan))); + var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; + StringAssert.Contains("thread", msg); + } #endregion @@ -138,6 +160,16 @@ public unsafe void ReadOnlySpanReference_Value_can_update_original() reference.Value = "bar".AsSpan(); Assert.True(local == "bar".AsSpan()); } + + [Test] + public unsafe void ReadOnlySpanReference_Value_throws_when_access_from_other_thread() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + var task = Task.Run(() => reference.Value.ToString()); + var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; + StringAssert.Contains("thread", msg); + } #endregion diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index a8acb7048..e3dc8c82e 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -59,9 +59,7 @@ namespace Castle.DynamicProxy // 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. + // *) Finally, we allow accessing reference data from the owning thread only to avoid all possible concurrency-related issues. // // 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 @@ -77,7 +75,9 @@ public unsafe class ByRefLikeReference private readonly Type type; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private nint ptr; + private void* ptr; + + private Thread ownerThread; /// /// Do not use! This constructor should only be called by DynamicProxy internals. @@ -97,7 +97,8 @@ public ByRefLikeReference(Type type, void* ptr) } this.type = type; - this.ptr = (nint)ptr; + this.ptr = ptr; + this.ownerThread = Thread.CurrentThread; } /// @@ -107,6 +108,8 @@ public ByRefLikeReference(Type type, void* ptr) [EditorBrowsable(EditorBrowsableState.Never)] public void* GetPtr(Type checkType) { + AssertCurrentThread(); + if (checkType != type) { throw new ArgumentException($"The reference type {type.FullName} does not match the expected type {checkType.FullName}"); @@ -117,14 +120,14 @@ public ByRefLikeReference(Type type, void* ptr) internal void* GetPtrNocheck() { - var ptr = (void*)Volatile.Read(ref this.ptr); - - if (ptr == null) + AssertCurrentThread(); + + if (this.ptr == null) { throw new ObjectDisposedException("This reference was already invalidated"); } - - return ptr; + + return this.ptr; } /// @@ -134,11 +137,21 @@ public ByRefLikeReference(Type type, void* ptr) [EditorBrowsable(EditorBrowsableState.Never)] public void Invalidate(void* checkPtr) { - var ptr = (void*)Interlocked.CompareExchange(ref this.ptr, (nint)null, (nint)checkPtr); + AssertCurrentThread(); + + if (this.ptr == null || this.ptr != checkPtr) + { + throw new InvalidOperationException($"BUG: Pointer mismatch on reference invalidation. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}"); + } + + this.ptr = null; + } - if (ptr == null || checkPtr != ptr) + private void AssertCurrentThread() + { + if (this.ownerThread != Thread.CurrentThread) { - throw new InvalidOperationException($"BUG: Pointer mismatch on reference invalidation. Expected: {(nint)checkPtr:X16}, Actual: {(nint)ptr:X16}"); + throw new InvalidOperationException("This reference cannot be used from another thread"); } } } From 2a86da298fb569e9b54c29c72e0a199d6119445a Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Mon, 16 Mar 2026 22:58:23 +0100 Subject: [PATCH 3/8] Add IsScoped parameter to the ByRefLikeReference --- ref/Castle.Core-net8.0.cs | 7 +- ref/Castle.Core-net9.0.cs | 9 +- .../ByRefLikeReferenceTestCase.cs | 48 ++++--- .../CallerToInterceptorTestCase.cs | 119 ++++++++++++++++++ .../InterceptorToCallerTestCase.cs | 18 +++ .../ByRefLikeSupport/ProxyableTestCase.cs | 4 + .../ByRefLikeSupport/TestedTypes.cs | 25 ++++ .../DynamicProxy/ByRefLikeReference.cs | 23 ++-- .../MethodWithInvocationGenerator.cs | 10 +- 9 files changed, 232 insertions(+), 31 deletions(-) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 5a9c621df..097b670e9 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2425,7 +2425,8 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me public class ByRefLikeReference { [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } + public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } + public bool ValueIsScoped { get; } [System.CLSCompliant(false)] public unsafe void* GetPtr(System.Type checkType) { } [System.CLSCompliant(false)] @@ -2702,13 +2703,13 @@ public static bool IsProxyType(System.Type type) { } public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference { [System.CLSCompliant(false)] - public ReadOnlySpanReference(System.Type type, void* ptr) { } + public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.ReadOnlySpan<>& Value { get; } } public class SpanReference : Castle.DynamicProxy.ByRefLikeReference { [System.CLSCompliant(false)] - public SpanReference(System.Type type, void* ptr) { } + public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.Span<>& Value { get; } } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index 87d3f4334..71cc10979 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2425,7 +2425,8 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me public class ByRefLikeReference { [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } + public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } + public bool ValueIsScoped { get; } [System.CLSCompliant(false)] public unsafe void* GetPtr(System.Type checkType) { } [System.CLSCompliant(false)] @@ -2435,7 +2436,7 @@ public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeRefer where TByRefLike : struct { [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } + public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } public TByRefLike& Value { get; } } public class CustomAttributeInfo : System.IEquatable @@ -2719,12 +2720,12 @@ public static bool IsProxyType(System.Type type) { } public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference> { [System.CLSCompliant(false)] - public ReadOnlySpanReference(System.Type type, void* ptr) { } + public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } } public class SpanReference : Castle.DynamicProxy.ByRefLikeReference> { [System.CLSCompliant(false)] - public SpanReference(System.Type type, void* ptr) { } + public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor { diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs index 018fb77f7..74568eb00 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -41,7 +41,7 @@ public unsafe void Ctor_throws_if_non_by_ref_like_type() Assert.Throws(() => { bool local = default; - _ = new ByRefLikeReference(typeof(bool), &local); + _ = new ByRefLikeReference(typeof(bool), &local, false); }); } @@ -49,14 +49,24 @@ public unsafe void Ctor_throws_if_non_by_ref_like_type() public unsafe void Ctor_succeeds_if_by_ref_like_type() { ReadOnlySpan local = default; - _ = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + _ = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public unsafe void Ctor_preserves_value_is_scoped_value(bool valueIsScoped) + { + ReadOnlySpan local = default; + var result = new ByRefLikeReference(typeof(ReadOnlySpan), &local, valueIsScoped: valueIsScoped); + Assert.AreEqual(valueIsScoped, result.ValueIsScoped); } [Test] public unsafe void Invalidate_throws_if_address_mismatch() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); Assert.Throws(() => { ReadOnlySpan otherLocal = default; @@ -68,7 +78,7 @@ public unsafe void Invalidate_throws_if_address_mismatch() public unsafe void Invalidate_succeeds_if_address_match() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); reference.Invalidate(&local); } @@ -76,7 +86,7 @@ public unsafe void Invalidate_succeeds_if_address_match() public unsafe void Invalidate_throws_when_access_from_other_thread() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); var address = reference.GetPtr(typeof(ReadOnlySpan)); var task = Task.Run(() => reference.Invalidate(address)); var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; @@ -87,7 +97,7 @@ public unsafe void Invalidate_throws_when_access_from_other_thread() public unsafe void GetPtr_throws_if_type_mismatch() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); Assert.Throws(() => reference.GetPtr(typeof(bool))); } @@ -95,7 +105,7 @@ public unsafe void GetPtr_throws_if_type_mismatch() public unsafe void GetPtr_returns_ctor_address_if_type_match() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); var ptr = reference.GetPtr(typeof(ReadOnlySpan)); Assert.True(ptr == &local); } @@ -104,7 +114,7 @@ public unsafe void GetPtr_returns_ctor_address_if_type_match() public unsafe void GetPtr_throws_after_Invalidate() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); reference.Invalidate(&local); Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); } @@ -113,7 +123,7 @@ public unsafe void GetPtr_throws_after_Invalidate() public unsafe void GetPtr_throws_when_access_from_other_thread() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); var task = Task.Run(() => reference.GetPtr(typeof(ReadOnlySpan))); var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; StringAssert.Contains("thread", msg); @@ -131,14 +141,24 @@ public unsafe void ReadOnlySpanReference_ctor_throws_if_type_mismatch() Assert.Throws(() => { ReadOnlySpan local = default; - _ = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + _ = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); }); } + [Test] + [TestCase(true)] + [TestCase(false)] + public unsafe void ReadOnlySpanReference_ctor_preserves_value_is_scoped_value(bool valueIsScoped) + { + ReadOnlySpan local = default; + var result = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, valueIsScoped: valueIsScoped); + Assert.AreEqual(valueIsScoped, result.ValueIsScoped); + } + public unsafe void ReadOnlySpanReference_Value_returns_equal_span() { ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); Assert.True(reference.Value == "foo".AsSpan()); } @@ -147,7 +167,7 @@ public unsafe void ReadOnlySpanReference_Value_returns_equal_span() public unsafe void ReadOnlySpanReference_Value_returns_same_span() { ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); Assert.True(Unsafe.AreSame(ref reference.Value, ref local)); } #endif @@ -156,7 +176,7 @@ public unsafe void ReadOnlySpanReference_Value_returns_same_span() public unsafe void ReadOnlySpanReference_Value_can_update_original() { ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); reference.Value = "bar".AsSpan(); Assert.True(local == "bar".AsSpan()); } @@ -165,7 +185,7 @@ public unsafe void ReadOnlySpanReference_Value_can_update_original() public unsafe void ReadOnlySpanReference_Value_throws_when_access_from_other_thread() { ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); var task = Task.Run(() => reference.Value.ToString()); var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; StringAssert.Contains("thread", msg); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs index 3877297a7..4700dcc8f 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs @@ -133,6 +133,125 @@ public void ByRefLike__passed_by_ref_ref__is_replaced_with_ByRefLikeReference() #endif + delegate void ByRefLike__scoped_property_shall_be_correct_ProxyInvocation(TInterface proxy, ByRefLike value) where TInterface : class; + + /// + /// Do not test other arg variations - we use the same call path. + /// + [Test] + public void ByRefLike__scoped_property__shall_be_correct() + { + { + static void Demo(ByRefLikeContainer container, ByRefLike value) + { + container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByValue(v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, scoped ByRefLike value) + { + // Compiler error + // container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByValue(v), + expectedValueIsScoped: true); + } + + { + static void Demo(ByRefLikeContainer container, in ByRefLike value) + { + container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefIn(v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, scoped in ByRefLike value) + { + container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefIn(v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, ref ByRefLike value) + { + container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefRef(ref v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, scoped ref ByRefLike value) + { + container.Value = value; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefRef(ref v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, out ByRefLike value) + { + // IsScoped is not actually relevant here, as it's not possible to access the value + // container.Value = value; + + value = default; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefOut(out v), + expectedValueIsScoped: false); + } + + { + static void Demo(ByRefLikeContainer container, scoped out ByRefLike value) + { + // IsScoped is not actually relevant here, as it's not possible to access the value + // container.Value = value; + + value = default; + } + + InvokeProxyAndAssertIsScoped( + invoke: (p, v) => p.PassByRefOut(out v), + expectedValueIsScoped: false); + } + + void InvokeProxyAndAssertIsScoped(ByRefLike__scoped_property_shall_be_correct_ProxyInvocation invoke, bool expectedValueIsScoped) where TInterface : class + { + InvokeProxyAndInspectInvocationArgument( + invoke: (TInterface proxy) => + { + var byRefLike = new ByRefLike("from caller"); + invoke(proxy, byRefLike); + }, + inspect: (object? invocationArg) => + { + var arg = (ByRefLikeReference)invocationArg!; + Assert.AreEqual(expectedValueIsScoped, arg.ValueIsScoped); + }); + } + } + #endregion #region Tests for `ReadOnlySpan` parameters diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs index 079168945..97bb19598 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs @@ -85,6 +85,24 @@ public void ByRefLike__return_value__can_be_written_to_ByRefLikeReference() ((ByRefLikeReference)invocationReturnValue!).Value = returnValue; }); } + + /// + /// Do not test other types, as the logic is the same there. + /// + [Test] + public void ByRefLike__return_value__should_not_be_scoped() + { + InvokeProxyAndSetInvocationReturnValue( + invoke: (IReturnByRefLikeByValue proxy) => + { + ByRefLike returnValue = proxy.ReturnByValue(); + }, + set: (object? invocationReturnValue) => + { + Assert.IsInstanceOf>(invocationReturnValue); + Assert.AreEqual(false, ((ByRefLikeReference)invocationReturnValue).ValueIsScoped); + }); + } #endif diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ProxyableTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ProxyableTestCase.cs index 1a4d16009..89bf2eaf5 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ProxyableTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ProxyableTestCase.cs @@ -30,9 +30,13 @@ public class ProxyableTestCase : BasePEVerifyTestCase { // `ByRefLike` types: [TestCase(typeof(IPassByRefLikeByValue))] + [TestCase(typeof(IPassByRefLikeByValueScoped))] [TestCase(typeof(IPassByRefLikeByRefIn))] + [TestCase(typeof(IPassByRefLikeByRefInScoped))] [TestCase(typeof(IPassByRefLikeByRefRef))] + [TestCase(typeof(IPassByRefLikeByRefRefScoped))] [TestCase(typeof(IPassByRefLikeByRefOut))] + [TestCase(typeof(IPassByRefLikeByRefOutScoped))] [TestCase(typeof(IReturnByRefLikeByValue))] // `ReadOnlySpan` types: [TestCase(typeof(IPassReadOnlySpanByValue))] diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TestedTypes.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TestedTypes.cs index 55bdc6ec7..3983dfbef 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TestedTypes.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TestedTypes.cs @@ -37,26 +37,51 @@ public object? Value } } + public ref struct ByRefLikeContainer + { + public ByRefLike Value { get; set; } + } + public interface IPassByRefLikeByValue { void PassByValue(ByRefLike arg); } + public interface IPassByRefLikeByValueScoped + { + void PassByValue(scoped ByRefLike arg); + } + public interface IPassByRefLikeByRefIn { void PassByRefIn(in ByRefLike arg); } + public interface IPassByRefLikeByRefInScoped + { + void PassByRefIn(scoped in ByRefLike arg); + } + public interface IPassByRefLikeByRefRef { void PassByRefRef(ref ByRefLike arg); } + public interface IPassByRefLikeByRefRefScoped + { + void PassByRefRef(scoped ref ByRefLike arg); + } + public interface IPassByRefLikeByRefOut { void PassByRefOut(out ByRefLike arg); } + public interface IPassByRefLikeByRefOutScoped + { + void PassByRefOut(scoped out ByRefLike arg); + } + public interface IReturnByRefLikeByValue { ByRefLike ReturnByValue(); diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index e3dc8c82e..4ce2bac08 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -59,6 +59,10 @@ namespace Castle.DynamicProxy // 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.) // + // *) We track if each reference represents scoped value. Scoped values usually represent data living on stack only, + // so we should apply more strict rules on how we expose the value. + // Dynamic generator analyzes method signature and provides the correct value for us. + // // *) Finally, we allow accessing reference data from the owning thread only to avoid all possible concurrency-related issues. // // As far as I can reason, `ByRefLikeReference` et al. should be safe to use IFF they are never copied out from an @@ -78,13 +82,15 @@ public unsafe class ByRefLikeReference private void* ptr; private Thread ownerThread; - + + public bool ValueIsScoped { get; } + /// /// Do not use! This constructor should only be called by DynamicProxy internals. /// [CLSCompliant(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public ByRefLikeReference(Type type, void* ptr) + public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) { if (type.IsByRefLikeSafe() == false) { @@ -98,6 +104,7 @@ public ByRefLikeReference(Type type, void* ptr) this.type = type; this.ptr = ptr; + this.ValueIsScoped = valueIsScoped; this.ownerThread = Thread.CurrentThread; } @@ -180,8 +187,8 @@ public unsafe class ByRefLikeReference : ByRefLikeReference /// [CLSCompliant(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public ByRefLikeReference(Type type, void* ptr) - : base(type, ptr) + public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(TByRefLike)) { @@ -225,8 +232,8 @@ public unsafe class ReadOnlySpanReference /// [CLSCompliant(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public ReadOnlySpanReference(Type type, void* ptr) - : base(type, ptr) + public ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(ReadOnlySpan)) { @@ -271,8 +278,8 @@ public unsafe class SpanReference /// [CLSCompliant(false)] [EditorBrowsable(EditorBrowsableState.Never)] - public SpanReference(Type type, void* ptr) - : base(type, ptr) + public SpanReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(Span)) { diff --git a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs index 9ec97f9f6..b3d4a4a41 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -305,13 +305,17 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments // Instead of them, we prepare instances of `ByRefLikeReference` wrappers that reference them. var referenceCtor = GetByRefLikeReferenceCtorFor(dereferencedArgumentType); var reference = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeReference)); + // Notice, if a parameter is by-ref, scoped applies to the reference - not to the value itself. + // We are interested only in tracking if the value is scoped. + var valueIsScoped = parameters[i].GetCustomAttribute() != null && !parameters[i].IsByRef; method.CodeBuilder.AddStatement( new AssignStatement( reference, new NewInstanceExpression( referenceCtor, new TypeTokenExpression(dereferencedArgumentType), - new AddressOfExpression(dereferencedArgument)))); + new AddressOfExpression(dereferencedArgument), + new LiteralBoolExpression(valueIsScoped)))); dereferencedArgument = reference; } @@ -419,7 +423,9 @@ public void PrepareReturnValueBuffer(LocalReference invocation, out LocalReferen new NewInstanceExpression( referenceCtor, new TypeTokenExpression(returnType), - new AddressOfExpression(returnValueBuffer)))); + new AddressOfExpression(returnValueBuffer), + // Return values are never scoped + new LiteralBoolExpression(false)))); #else returnValueBuffer = null; #endif From 219c26e99f5b71bff20b4d5eb32b98dc4fca019f Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Thu, 19 Mar 2026 21:00:05 +0100 Subject: [PATCH 4/8] chore: Simplify code by removing GetPtrNocheck() --- src/Castle.Core/DynamicProxy/ByRefLikeReference.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 4ce2bac08..35685a259 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -122,13 +122,6 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) throw new ArgumentException($"The reference type {type.FullName} does not match the expected type {checkType.FullName}"); } - return GetPtrNocheck(); - } - - internal void* GetPtrNocheck() - { - AssertCurrentThread(); - if (this.ptr == null) { throw new ObjectDisposedException("This reference was already invalidated"); @@ -200,7 +193,7 @@ public ref TByRefLike Value { get { - return ref *(TByRefLike*)GetPtrNocheck(); + return ref *(TByRefLike*)GetPtr(typeof(TByRefLike)); } } } @@ -246,7 +239,7 @@ public ref ReadOnlySpan Value { get { - return ref *(ReadOnlySpan*)GetPtrNocheck(); + return ref *(ReadOnlySpan*)GetPtr(typeof(ReadOnlySpan)); } } #endif @@ -292,7 +285,7 @@ public ref Span Value { get { - return ref *(Span*)GetPtrNocheck(); + return ref *(Span*)GetPtr(typeof(Span)); } } #endif From 3953dd0866450234be6e8dc1f4f131669c8286fc Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Thu, 19 Mar 2026 21:13:00 +0100 Subject: [PATCH 5/8] Introduce GetValue/UseValue and SetValue methods --- ref/Castle.Core-net8.0.cs | 14 +++ ref/Castle.Core-net9.0.cs | 7 ++ .../ByRefLikeReferenceTestCase.cs | 40 ++++--- .../DynamicProxy/ByRefLikeReference.cs | 101 ++++++++++++++++++ 4 files changed, 150 insertions(+), 12 deletions(-) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 097b670e9..9639537c8 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2705,12 +2705,26 @@ public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference [System.CLSCompliant(false)] public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.ReadOnlySpan<>& Value { get; } + public System.ReadOnlySpan GetValue() { } + public void SetValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueGetter valueGetter) { } + public void UseValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueConsumer valueConsumer) { } + public TResult UseValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueConsumerWithResult valueConsumer) { } + public delegate void ValueConsumer([System.Runtime.CompilerServices.ScopedRef] System.ReadOnlySpan value); + public delegate TResult ValueConsumerWithResult([System.Runtime.CompilerServices.ScopedRef] System.ReadOnlySpan value); + public delegate System.ReadOnlySpan ValueGetter(); } public class SpanReference : Castle.DynamicProxy.ByRefLikeReference { [System.CLSCompliant(false)] public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.Span<>& Value { get; } + public System.Span GetValue() { } + public void SetValue(Castle.DynamicProxy.SpanReference.ValueGetter valueGetter) { } + public void UseValue(Castle.DynamicProxy.SpanReference.ValueConsumer valueConsumer) { } + public TResult UseValue(Castle.DynamicProxy.SpanReference.ValueConsumerWithResult valueConsumer) { } + public delegate void ValueConsumer([System.Runtime.CompilerServices.ScopedRef] System.Span value); + public delegate TResult ValueConsumerWithResult([System.Runtime.CompilerServices.ScopedRef] System.Span value); + public delegate System.Span ValueGetter(); } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor { diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index 71cc10979..51c6eea59 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2438,6 +2438,13 @@ public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeRefer [System.CLSCompliant(false)] public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } public TByRefLike& Value { get; } + public TByRefLike GetValue() { } + public void SetValue(Castle.DynamicProxy.ByRefLikeReference.ValueGetter valueGetter) { } + public void UseValue(Castle.DynamicProxy.ByRefLikeReference.ValueConsumer valueConsumer) { } + public TResult UseValue(Castle.DynamicProxy.ByRefLikeReference.ValueConsumerWithResult valueConsumer) { } + public delegate void ValueConsumer([System.Runtime.CompilerServices.ScopedRef] TByRefLike value); + public delegate TResult ValueConsumerWithResult([System.Runtime.CompilerServices.ScopedRef] TByRefLike value); + public delegate TByRefLike ValueGetter(); } public class CustomAttributeInfo : System.IEquatable { diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs index 74568eb00..bee4e7790 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -155,41 +155,57 @@ public unsafe void ReadOnlySpanReference_ctor_preserves_value_is_scoped_value(bo Assert.AreEqual(valueIsScoped, result.ValueIsScoped); } - public unsafe void ReadOnlySpanReference_Value_returns_equal_span() + public unsafe void ReadOnlySpanReference_GetValue_returns_equal_span() { ReadOnlySpan local = "foo".AsSpan(); var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); - Assert.True(reference.Value == "foo".AsSpan()); + Assert.True(reference.GetValue() == "foo".AsSpan()); } - -#if NET9_0_OR_GREATER - [Test] - public unsafe void ReadOnlySpanReference_Value_returns_same_span() + + public unsafe void ReadOnlySpanReference_UseValue_returns_equal_span() { ReadOnlySpan local = "foo".AsSpan(); var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); - Assert.True(Unsafe.AreSame(ref reference.Value, ref local)); + var returnedValue = reference.UseValue((scoped x) => x.ToString()); + Assert.True(returnedValue == "foo"); } -#endif [Test] - public unsafe void ReadOnlySpanReference_Value_can_update_original() + public unsafe void ReadOnlySpanReference_SetValue_can_update_original() { ReadOnlySpan local = "foo".AsSpan(); var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); - reference.Value = "bar".AsSpan(); + reference.SetValue(() => "bar".AsSpan()); Assert.True(local == "bar".AsSpan()); } [Test] - public unsafe void ReadOnlySpanReference_Value_throws_when_access_from_other_thread() + public unsafe void ReadOnlySpanReference_GetValue_throws_when_access_from_other_thread() { ReadOnlySpan local = "foo".AsSpan(); var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); - var task = Task.Run(() => reference.Value.ToString()); + var task = Task.Run(() => reference.GetValue().ToString()); var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; StringAssert.Contains("thread", msg); } + + [Test] + public unsafe void ReadOnlySpanReference_GetValue_throws_when_called_for_scoped() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, valueIsScoped: true); + var msg = Assert.Throws(() => reference.GetValue()).Message; + StringAssert.Contains("scoped", msg); + } + + [Test] + public unsafe void ReadOnlySpanReference_UseValue_returns_for_scoped() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, valueIsScoped: true); + var returnedValue = reference.UseValue((scoped x) => x.ToString()); + Assert.True(returnedValue == "foo"); + } #endregion diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 35685a259..63334036a 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -63,6 +63,14 @@ namespace Castle.DynamicProxy // so we should apply more strict rules on how we expose the value. // Dynamic generator analyzes method signature and provides the correct value for us. // + // *) We always return a copy of the original value and never expose the value by reference. + // This is important to make sure that we prevent scenario of `ref struct` interior mutability + // and providing a potential way to leak scoped values out of their lifetime scope. + // + // *) The SetValue() function uses delegate to get the value. This is required to make sure that local variables + // or variables with scoped visibility are not promoted outside their lifetime. + // When we have delegate, we could only use heap-backed values and compiler will enforce the safety for us. + // // *) Finally, we allow accessing reference data from the owning thread only to avoid all possible concurrency-related issues. // // As far as I can reason, `ByRefLikeReference` et al. should be safe to use IFF they are never copied out from an @@ -196,6 +204,37 @@ public ref TByRefLike Value return ref *(TByRefLike*)GetPtr(typeof(TByRefLike)); } } + + public TByRefLike GetValue() + { + if (ValueIsScoped) + { + throw new InvalidOperationException($"Use {nameof(UseValue)} method for scoped arguments"); + } + + return Value; + } + + public void SetValue(ValueGetter valueGetter) + { + Value = valueGetter.Invoke(); + } + + public void UseValue(ValueConsumer valueConsumer) + { + valueConsumer.Invoke(Value); + } + + public TResult UseValue(ValueConsumerWithResult valueConsumer) where TResult : allows ref struct + { + return valueConsumer.Invoke(Value); + } + + public delegate TResult ValueConsumerWithResult(scoped TByRefLike value) where TResult : allows ref struct; + + public delegate void ValueConsumer(scoped TByRefLike value); + + public delegate TByRefLike ValueGetter(); } #endif @@ -242,6 +281,37 @@ public ref ReadOnlySpan Value return ref *(ReadOnlySpan*)GetPtr(typeof(ReadOnlySpan)); } } + + public ReadOnlySpan GetValue() + { + if (ValueIsScoped) + { + throw new InvalidOperationException($"Use {nameof(UseValue)} method for scoped arguments"); + } + + return Value; + } + + public void SetValue(ValueGetter valueGetter) + { + Value = valueGetter.Invoke(); + } + + public void UseValue(ValueConsumer valueConsumer) + { + valueConsumer.Invoke(Value); + } + + public TResult UseValue(ValueConsumerWithResult valueConsumer) + { + return valueConsumer.Invoke(Value); + } + + public delegate TResult ValueConsumerWithResult(scoped ReadOnlySpan value); + + public delegate void ValueConsumer(scoped ReadOnlySpan value); + + public delegate ReadOnlySpan ValueGetter(); #endif } @@ -288,6 +358,37 @@ public ref Span Value return ref *(Span*)GetPtr(typeof(Span)); } } + + public Span GetValue() + { + if (ValueIsScoped) + { + throw new InvalidOperationException($"Use {nameof(UseValue)} method for scoped arguments"); + } + + return Value; + } + + public void SetValue(ValueGetter valueGetter) + { + Value = valueGetter.Invoke(); + } + + public void UseValue(ValueConsumer valueConsumer) + { + valueConsumer.Invoke(Value); + } + + public TResult UseValue(ValueConsumerWithResult valueConsumer) + { + return valueConsumer.Invoke(Value); + } + + public delegate TResult ValueConsumerWithResult(scoped Span value); + + public delegate void ValueConsumer(scoped Span value); + + public delegate Span ValueGetter(); #endif } } From bf2b1fa77bbe4203b74351b025c1a8bbd351bb40 Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Thu, 19 Mar 2026 21:27:21 +0100 Subject: [PATCH 6/8] chore: Use GetValue/UseValue/SetValue and make Value property private --- ref/Castle.Core-net8.0.cs | 2 -- ref/Castle.Core-net9.0.cs | 1 - .../CallerToInterceptorTestCase.cs | 18 ++++++------- .../ByRefLikeSupport/EdgeCasesTestCase.cs | 9 +++---- .../InterceptorToCallerTestCase.cs | 27 +++++++------------ .../InterceptorToTargetTestCase.cs | 27 +++++++------------ .../TargetToInterceptorTestCase.cs | 18 ++++++------- .../DynamicProxy/ByRefLikeReference.cs | 6 ++--- 8 files changed, 42 insertions(+), 66 deletions(-) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 9639537c8..0667eb4e7 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2704,7 +2704,6 @@ public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference { [System.CLSCompliant(false)] public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } - public System.ReadOnlySpan<>& Value { get; } public System.ReadOnlySpan GetValue() { } public void SetValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueConsumer valueConsumer) { } @@ -2717,7 +2716,6 @@ public class SpanReference : Castle.DynamicProxy.ByRefLikeReference { [System.CLSCompliant(false)] public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } - public System.Span<>& Value { get; } public System.Span GetValue() { } public void SetValue(Castle.DynamicProxy.SpanReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.SpanReference.ValueConsumer valueConsumer) { } diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index 51c6eea59..b24119d3d 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2437,7 +2437,6 @@ public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeRefer { [System.CLSCompliant(false)] public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } - public TByRefLike& Value { get; } public TByRefLike GetValue() { } public void SetValue(Castle.DynamicProxy.ByRefLikeReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.ByRefLikeReference.ValueConsumer valueConsumer) { } diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs index 4700dcc8f..0eacf3a60 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs @@ -45,7 +45,7 @@ public void ByRefLike__passed_by_value__can_be_read_from_ByRefLikeReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = ((ByRefLikeReference)invocationArg!).Value; + ByRefLike arg = ((ByRefLikeReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", arg.Value); }); } @@ -62,7 +62,7 @@ public void ByRefLike__passed_by_ref_in__can_be_read_from_ByRefLikeReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = ((ByRefLikeReference)invocationArg!).Value; + ByRefLike arg = ((ByRefLikeReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", arg.Value); }); } @@ -79,7 +79,7 @@ public void ByRefLike__passed_by_ref_ref__can_be_read_from_ByRefLikeReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - var arg = ((ByRefLikeReference)invocationArg!).Value; + var arg = ((ByRefLikeReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", arg.Value); }); } @@ -268,7 +268,7 @@ public void ReadOnlySpan__passed_by_value__can_be_read_from_ReadOnlySpanReferenc inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).Value; + ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } @@ -285,7 +285,7 @@ public void ReadOnlySpan__passed_by_ref_in__can_be_read_from_ReadOnlySpanReferen inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).Value; + ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } @@ -302,7 +302,7 @@ public void ReadOnlySpan__passed_by_ref_ref__can_be_read_from_ReadOnlySpanRefere inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).Value; + ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } @@ -323,7 +323,7 @@ public void Span__passed_by_value__can_be_read_from_SpanReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = ((SpanReference)invocationArg!).Value; + Span arg = ((SpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } @@ -340,7 +340,7 @@ public void Span__passed_by_ref_in__can_be_read_from_SpanReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = ((SpanReference)invocationArg!).Value; + Span arg = ((SpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } @@ -357,7 +357,7 @@ public void Span__passed_by_ref_ref__can_be_read_from_SpanReference() inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = ((SpanReference)invocationArg!).Value; + Span arg = ((SpanReference)invocationArg!).GetValue(); Assert.AreEqual("from caller", new string(arg)); }); } diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/EdgeCasesTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/EdgeCasesTestCase.cs index b53583af3..19e2d6add 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/EdgeCasesTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/EdgeCasesTestCase.cs @@ -45,8 +45,7 @@ public void ByRefLike__passed_by_ref_in__written_to_ByRefLikeReference__does_not set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }); } @@ -65,8 +64,7 @@ public void ReadOnlySpan__passed_by_ref_in__written_to_ByRefLikeReference__does_ set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }); } @@ -83,8 +81,7 @@ public void Span__passed_by_ref_in__written_to_ByRefLikeReference__does_not_prop set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }); } diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs index 97bb19598..f02d851f0 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToCallerTestCase.cs @@ -47,8 +47,7 @@ public void ByRefLike__passed_by_ref_ref___can_be_written_to_ByRefLikeReference( set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }); } @@ -64,8 +63,7 @@ public void ByRefLike__passed_by_ref_out___can_be_written_to_ByRefLikeReference( set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }); } @@ -81,8 +79,7 @@ public void ByRefLike__return_value__can_be_written_to_ByRefLikeReference() set: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - ByRefLike returnValue = new("from interceptor"); - ((ByRefLikeReference)invocationReturnValue!).Value = returnValue; + ((ByRefLikeReference)invocationReturnValue!).SetValue(() => new ByRefLike("from interceptor")); }); } @@ -123,8 +120,7 @@ public void ReadOnlySpan__passed_by_ref_ref___can_be_written_to_ReadOnlySpanRefe set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }); } @@ -140,8 +136,7 @@ public void ReadOnlySpan__passed_by_ref_out___can_be_written_to_ReadOnlySpanRefe set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }); } @@ -157,8 +152,7 @@ public void ReadOnlySpan__return_value__can_be_written_to_ReadOnlySpanReference( set: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - ReadOnlySpan returnValue = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationReturnValue!).Value = returnValue; + ((ReadOnlySpanReference)invocationReturnValue!).SetValue(() => "from interceptor".AsSpan()); }); } @@ -179,8 +173,7 @@ public void Span__passed_by_ref_ref___can_be_written_to_SpanReference() set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }); } @@ -196,8 +189,7 @@ public void Span__passed_by_ref_out___can_be_written_to_SpanReference() set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }); } @@ -213,8 +205,7 @@ public void Span__return_value__can_be_written_to_SpanReference() set: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - Span returnValue = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationReturnValue!).Value = returnValue; + ((SpanReference)invocationReturnValue!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }); } diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToTargetTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToTargetTestCase.cs index 552423daf..aaab3409a 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToTargetTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/InterceptorToTargetTestCase.cs @@ -45,8 +45,7 @@ public void ByRefLike__passed_by_value__written_to_ByRefLikeReference__can_be_re set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }, target: () => new PassByRefLikeTarget(inspect: (ByRefLike arg) => { @@ -66,8 +65,7 @@ public void ByRefLike__passed_by_ref_in__written_to_ByRefLikeReference__can_be_r set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }, target: () => new PassByRefLikeTarget(inspect: (ByRefLike arg) => { @@ -87,8 +85,7 @@ public void ByRefLike__passed_by_ref_ref__written_to_ByRefLikeReference__can_be_ set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = new("from interceptor"); - ((ByRefLikeReference)invocationArg!).Value = arg; + ((ByRefLikeReference)invocationArg!).SetValue(() => new ByRefLike("from interceptor")); }, target: () => new PassByRefLikeTarget(inspect: (ByRefLike arg) => { @@ -114,8 +111,7 @@ public void ReadOnlySpan__passed_by_value__written_to_ByRefLikeReference__can_be set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }, target: () => new PassReadOnlySpanTarget(inspect: (ReadOnlySpan arg) => { @@ -135,8 +131,7 @@ public void ReadOnlySpan__passed_by_ref_in__written_to_ByRefLikeReference__can_b set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }, target: () => new PassReadOnlySpanTarget(inspect: (ReadOnlySpan arg) => { @@ -156,8 +151,7 @@ public void ReadOnlySpan__passed_by_ref_ref__written_to_ByRefLikeReference__can_ set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = "from interceptor".AsSpan(); - ((ReadOnlySpanReference)invocationArg!).Value = arg; + ((ReadOnlySpanReference)invocationArg!).SetValue(() => "from interceptor".AsSpan()); }, target: () => new PassReadOnlySpanTarget(inspect: (ReadOnlySpan arg) => { @@ -181,8 +175,7 @@ public void Span__passed_by_value__written_to_ByRefLikeReference__can_be_read_fr set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }, target: () => new PassSpanTarget(inspect: (Span arg) => { @@ -202,8 +195,7 @@ public void Span__passed_by_ref_in__written_to_ByRefLikeReference__can_be_read_f set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }, target: () => new PassSpanTarget(inspect: (Span arg) => { @@ -223,8 +215,7 @@ public void Span__passed_by_ref_ref__written_to_ByRefLikeReference__can_be_read_ set: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = "from interceptor".ToCharArray().AsSpan(); - ((SpanReference)invocationArg!).Value = arg; + ((SpanReference)invocationArg!).SetValue(() => "from interceptor".ToCharArray().AsSpan()); }, target: () => new PassSpanTarget(inspect: (Span arg) => { diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TargetToInterceptorTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TargetToInterceptorTestCase.cs index 3b3d0b816..9e058430e 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TargetToInterceptorTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/TargetToInterceptorTestCase.cs @@ -49,7 +49,7 @@ public void ByRefLike__passed_by_ref_ref__set_at_target__can_be_read_from_ReadOn inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = ((ByRefLikeReference)invocationArg!).Value; + ByRefLike arg = ((ByRefLikeReference)invocationArg!).GetValue(); Assert.AreEqual("from target", arg.Value); }); } @@ -69,7 +69,7 @@ public void ByRefLike__passed_by_ref_out__set_at_target__can_be_read_from_ReadOn inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ByRefLike arg = ((ByRefLikeReference)invocationArg!).Value; + ByRefLike arg = ((ByRefLikeReference)invocationArg!).GetValue(); Assert.AreEqual("from target", arg.Value); }); } @@ -89,7 +89,7 @@ public void ByRefLike__return_value__returned_at_target__can_be_read_from_ReadOn inspect: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - ByRefLike returnValue = ((ByRefLikeReference)invocationReturnValue!).Value; + ByRefLike returnValue = ((ByRefLikeReference)invocationReturnValue!).GetValue(); Assert.AreEqual("from target", returnValue.Value); }); } @@ -116,7 +116,7 @@ public void ReadOnlySpan__passed_by_ref_ref__set_at_target__can_be_read_from_Rea inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).Value; + ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).GetValue(); Assert.AreEqual("from target", new string(arg)); }); } @@ -136,7 +136,7 @@ public void ReadOnlySpan__passed_by_ref_out__set_at_target__can_be_read_from_Rea inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).Value; + ReadOnlySpan arg = ((ReadOnlySpanReference)invocationArg!).GetValue(); Assert.AreEqual("from target", new string(arg)); }); } @@ -156,7 +156,7 @@ public void ReadOnlySpan__return_value__returned_at_target__can_be_read_from_Rea inspect: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - ReadOnlySpan returnValue = ((ReadOnlySpanReference)invocationReturnValue!).Value; + ReadOnlySpan returnValue = ((ReadOnlySpanReference)invocationReturnValue!).GetValue(); Assert.AreEqual("from target", new string(returnValue)); }); } @@ -181,7 +181,7 @@ public void Span__passed_by_ref_ref__set_at_target__can_be_read_from_ReadOnlySpa inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = ((SpanReference)invocationArg!).Value; + Span arg = ((SpanReference)invocationArg!).GetValue(); Assert.AreEqual("from target", new string(arg)); }); } @@ -201,7 +201,7 @@ public void Span__passed_by_ref_out__set_at_target__can_be_read_from_ReadOnlySpa inspect: (object? invocationArg) => { Assert.IsInstanceOf>(invocationArg); - Span arg = ((SpanReference)invocationArg!).Value; + Span arg = ((SpanReference)invocationArg!).GetValue(); Assert.AreEqual("from target", new string(arg)); }); } @@ -221,7 +221,7 @@ public void Span__return_value__returned_at_target__can_be_read_from_ReadOnlySpa inspect: (object? invocationReturnValue) => { Assert.IsInstanceOf>(invocationReturnValue); - Span returnValue = ((SpanReference)invocationReturnValue!).Value; + Span returnValue = ((SpanReference)invocationReturnValue!).GetValue(); Assert.AreEqual("from target", new string(returnValue)); }); } diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 63334036a..23177491e 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -197,7 +197,7 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) } } - public ref TByRefLike Value + private ref TByRefLike Value { get { @@ -274,7 +274,7 @@ public ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped) } #if !NET9_0_OR_GREATER - public ref ReadOnlySpan Value + private ref ReadOnlySpan Value { get { @@ -351,7 +351,7 @@ public SpanReference(Type type, void* ptr, bool valueIsScoped) } #if !NET9_0_OR_GREATER - public ref Span Value + private ref Span Value { get { From 67db9defebe02fea2971d016c4a7652d3f694430 Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Thu, 19 Mar 2026 22:32:01 +0100 Subject: [PATCH 7/8] Introduce ByRefLikeReferenceUnsafe and use it in generated code By it's nature Unsafe methods are not a part of the "client" API, so we just provide it on a side and should no longer hide methods/constructors from user. Also unsafe API is internal on the original class, so it's not immediately visible to the users. --- ref/Castle.Core-net8.0.cs | 19 +++-- ref/Castle.Core-net9.0.cs | 32 ++++---- .../ByRefLikeReferenceTestCase.cs | 16 ++-- .../DynamicProxy/ByRefLikeReference.cs | 73 +++++------------- .../DynamicProxy/ByRefLikeReferenceUnsafe.cs | 75 +++++++++++++++++++ .../ConvertArgumentFromObjectExpression.cs | 2 +- .../Generators/InvocationTypeGenerator.cs | 6 +- .../MethodWithInvocationGenerator.cs | 34 +++++---- .../Tokens/ByRefLikeReferenceMethods.cs | 34 --------- .../Tokens/ByRefLikeReferenceUnsafeMethods.cs | 41 ++++++++++ 10 files changed, 195 insertions(+), 137 deletions(-) create mode 100644 src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs delete mode 100644 src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs create mode 100644 src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceUnsafeMethods.cs diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 0667eb4e7..260f28b0b 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2424,13 +2424,20 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me } public class ByRefLikeReference { - [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } public bool ValueIsScoped { get; } + } + public static class ByRefLikeReferenceUnsafe + { + [System.CLSCompliant(false)] + public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReferenceUntyped(System.Type type, void* ptr, bool valueIsScoped) { } [System.CLSCompliant(false)] - public unsafe void* GetPtr(System.Type checkType) { } + public static unsafe Castle.DynamicProxy.ReadOnlySpanReference CreateReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } [System.CLSCompliant(false)] - public unsafe void Invalidate(void* checkPtr) { } + public static unsafe Castle.DynamicProxy.SpanReference CreateSpanReference(System.Type type, void* ptr, bool valueIsScoped) { } + [System.CLSCompliant(false)] + public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { } + [System.CLSCompliant(false)] + public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { } } public class CustomAttributeInfo : System.IEquatable { @@ -2702,8 +2709,6 @@ public static bool IsProxyType(System.Type type) { } } public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference { - [System.CLSCompliant(false)] - public ReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.ReadOnlySpan GetValue() { } public void SetValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.ReadOnlySpanReference.ValueConsumer valueConsumer) { } @@ -2714,8 +2719,6 @@ public TResult UseValue(Castle.DynamicProxy.ReadOnlySpanReference.Va } public class SpanReference : Castle.DynamicProxy.ByRefLikeReference { - [System.CLSCompliant(false)] - public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } public System.Span GetValue() { } public void SetValue(Castle.DynamicProxy.SpanReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.SpanReference.ValueConsumer valueConsumer) { } diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index b24119d3d..eab448360 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2424,19 +2424,27 @@ public virtual bool ShouldInterceptMethod(System.Type type, System.Reflection.Me } public class ByRefLikeReference { - [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } public bool ValueIsScoped { get; } + } + public static class ByRefLikeReferenceUnsafe + { + [System.CLSCompliant(false)] + public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) + where TByRefLike : struct { } + [System.CLSCompliant(false)] + public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReferenceUntyped(System.Type type, void* ptr, bool valueIsScoped) { } + [System.CLSCompliant(false)] + public static unsafe Castle.DynamicProxy.ReadOnlySpanReference CreateReadOnlySpanReference(System.Type type, void* ptr, bool valueIsScoped) { } + [System.CLSCompliant(false)] + public static unsafe Castle.DynamicProxy.SpanReference CreateSpanReference(System.Type type, void* ptr, bool valueIsScoped) { } [System.CLSCompliant(false)] - public unsafe void* GetPtr(System.Type checkType) { } + public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { } [System.CLSCompliant(false)] - public unsafe void Invalidate(void* checkPtr) { } + public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { } } public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeReference where TByRefLike : struct { - [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) { } public TByRefLike GetValue() { } public void SetValue(Castle.DynamicProxy.ByRefLikeReference.ValueGetter valueGetter) { } public void UseValue(Castle.DynamicProxy.ByRefLikeReference.ValueConsumer valueConsumer) { } @@ -2723,16 +2731,8 @@ 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, bool valueIsScoped) { } - } - public class SpanReference : Castle.DynamicProxy.ByRefLikeReference> - { - [System.CLSCompliant(false)] - public SpanReference(System.Type type, void* ptr, bool valueIsScoped) { } - } + public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference> { } + public class SpanReference : Castle.DynamicProxy.ByRefLikeReference> { } public class StandardInterceptor : Castle.DynamicProxy.IInterceptor { public StandardInterceptor() { } diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs index bee4e7790..68877ae28 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/ByRefLikeReferenceTestCase.cs @@ -63,32 +63,32 @@ public unsafe void Ctor_preserves_value_is_scoped_value(bool valueIsScoped) } [Test] - public unsafe void Invalidate_throws_if_address_mismatch() + public unsafe void Dispose_throws_if_address_mismatch() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); Assert.Throws(() => { ReadOnlySpan otherLocal = default; - reference.Invalidate(&otherLocal); + reference.Dispose(&otherLocal); }); } [Test] - public unsafe void Invalidate_succeeds_if_address_match() + public unsafe void Dispose_succeeds_if_address_match() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); - reference.Invalidate(&local); + reference.Dispose(&local); } [Test] - public unsafe void Invalidate_throws_when_access_from_other_thread() + public unsafe void Dispose_throws_when_access_from_other_thread() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); var address = reference.GetPtr(typeof(ReadOnlySpan)); - var task = Task.Run(() => reference.Invalidate(address)); + var task = Task.Run(() => reference.Dispose(address)); var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; StringAssert.Contains("thread", msg); } @@ -111,11 +111,11 @@ public unsafe void GetPtr_returns_ctor_address_if_type_match() } [Test] - public unsafe void GetPtr_throws_after_Invalidate() + public unsafe void GetPtr_throws_after_Dispose() { ReadOnlySpan local = default; var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); - reference.Invalidate(&local); + reference.Dispose(&local); Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); } diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 23177491e..230c2aaaa 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -20,7 +20,6 @@ namespace Castle.DynamicProxy { using System; - using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -42,45 +41,43 @@ namespace Castle.DynamicProxy // // *) 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: + // of methods that have already finished executing. This is why we have the `ByRefLikeReference.Dispose` 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: + // *) The `checkType` / `checkPtr` arguments of `GetPtr` or `Dispose`, 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 + // 2. `checkPtr` of `Dispose` 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 + // at the start of intercepted method bodies, and it invokes `Dispose` 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.) // // *) We track if each reference represents scoped value. Scoped values usually represent data living on stack only, - // so we should apply more strict rules on how we expose the value. - // Dynamic generator analyzes method signature and provides the correct value for us. + // so we should apply more strict rules on how we expose the value. Otherwise, we could have use-after-free scenario, + // which could lead to stack corruption and weird behavior. + // Dynamic generator analyzes method signature and provides the correct value. // // *) We always return a copy of the original value and never expose the value by reference. - // This is important to make sure that we prevent scenario of `ref struct` interior mutability - // and providing a potential way to leak scoped values out of their lifetime scope. + // This is important to make sure that we prevent possibility `ref struct` interior mutability (as copy will be changed) + // and close a possibility to leak scoped values out of their lifetime scope by storing them inside another `ref struct`. // // *) The SetValue() function uses delegate to get the value. This is required to make sure that local variables - // or variables with scoped visibility are not promoted outside their lifetime. + // or variables with scoped visibility could never be promoted outside their lifetime. // When we have delegate, we could only use heap-backed values and compiler will enforce the safety for us. // // *) Finally, we allow accessing reference data from the owning thread only to avoid all possible concurrency-related issues. + // Otherwise, it's very easy to have use-after-free scenario, as other thread could access value after function exit. // // 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)] @@ -92,13 +89,8 @@ public unsafe class ByRefLikeReference private Thread ownerThread; public bool ValueIsScoped { get; } - - /// - /// Do not use! This constructor should only be called by DynamicProxy internals. - /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) + + internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) { if (type.IsByRefLikeSafe() == false) { @@ -116,12 +108,7 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) this.ownerThread = Thread.CurrentThread; } - /// - /// Do not use! This method should only be called by DynamicProxy internals. - /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public void* GetPtr(Type checkType) + internal void* GetPtr(Type checkType) { AssertCurrentThread(); @@ -132,24 +119,19 @@ public ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) if (this.ptr == null) { - throw new ObjectDisposedException("This reference was already invalidated"); + throw new ObjectDisposedException("This reference was already disposed"); } return this.ptr; } - /// - /// Do not use! This method should only be called by DynamicProxy internals. - /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public void Invalidate(void* checkPtr) + internal void Dispose(void* checkPtr) { AssertCurrentThread(); if (this.ptr == null || this.ptr != checkPtr) { - throw new InvalidOperationException($"BUG: Pointer mismatch on reference invalidation. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}"); + throw new InvalidOperationException($"BUG: Pointer mismatch on reference disposal. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}"); } this.ptr = null; @@ -183,12 +165,7 @@ private void AssertCurrentThread() 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, bool valueIsScoped) + internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) : base(type, ptr, valueIsScoped) { if (type != typeof(TByRefLike)) @@ -259,12 +236,7 @@ public unsafe class ReadOnlySpanReference : 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, bool valueIsScoped) + internal ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped) : base(type, ptr, valueIsScoped) { if (type != typeof(ReadOnlySpan)) @@ -336,12 +308,7 @@ public unsafe class SpanReference : 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, bool valueIsScoped) + internal SpanReference(Type type, void* ptr, bool valueIsScoped) : base(type, ptr, valueIsScoped) { if (type != typeof(Span)) diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs new file mode 100644 index 000000000..b2bdd6601 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs @@ -0,0 +1,75 @@ +// 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; + +using System; + +/// +/// Collection of methods to perform unsafe manipulations on instances. +/// Usage could lead to memory safe issues, so be careful when using it +/// +public static class ByRefLikeReferenceUnsafe +{ + [CLSCompliant(false)] + public static unsafe ByRefLikeReference CreateByRefLikeReferenceUntyped(Type type, void* ptr, bool valueIsScoped) + { + return new ByRefLikeReference(type, ptr, valueIsScoped); + } + +#if NET9_0_OR_GREATER + + [CLSCompliant(false)] + public static unsafe ByRefLikeReference CreateByRefLikeReference(Type type, void* ptr, bool valueIsScoped) + where TByRefLike : struct, allows ref struct + { + return new ByRefLikeReference(type, ptr, valueIsScoped); + } +#endif + + [CLSCompliant(false)] + public static unsafe ReadOnlySpanReference CreateReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped) + { + return new ReadOnlySpanReference(type, ptr, valueIsScoped); + } + + [CLSCompliant(false)] + public static unsafe SpanReference CreateSpanReference(Type type, void* ptr, bool valueIsScoped) + { + return new SpanReference(type, ptr, valueIsScoped); + } + + /// + /// Returns raw pointer held by the . + /// + [CLSCompliant(false)] + public static unsafe void* GetRawPtr(ByRefLikeReference reference, Type expectedType) + { + return reference.GetPtr(expectedType); + } + + /// + /// Disposes , so that underlying value can no longer be used + /// + [CLSCompliant(false)] + public static unsafe void DisposeReference(ByRefLikeReference reference, void* expectedPtr) + { + reference.Dispose(expectedPtr); + } +} + +#endif \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs index e13b1f2d2..a50039d1c 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ConvertArgumentFromObjectExpression.cs @@ -50,7 +50,7 @@ public void Emit(ILGenerator gen) { gen.Emit(OpCodes.Ldtoken, dereferencedArgumentType); gen.Emit(OpCodes.Call, TypeMethods.GetTypeFromHandle); - gen.Emit(OpCodes.Call, ByRefLikeReferenceMethods.GetPtr); + gen.Emit(OpCodes.Call, ByRefLikeReferenceUnsafeMethods.GetRawPtr); gen.Emit(OpCodes.Ldobj, dereferencedArgumentType); } else diff --git a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs index f1bee1b38..85ddbe60c 100644 --- a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs @@ -331,11 +331,12 @@ public void CopyIn(LocalReference?[] byRefArguments) new AssignStatement( new PointerReference( new MethodInvocationExpression( + instance: null, + ByRefLikeReferenceUnsafeMethods.GetRawPtr, new MethodInvocationExpression( ThisExpression.Instance, InvocationMethods.GetArgumentValue, new LiteralIntExpression(i)), - ByRefLikeReferenceMethods.GetPtr, new TypeTokenExpression(localCopy.Type)), localCopy.Type), localCopy)); @@ -365,10 +366,11 @@ public void SetReturnValue(LocalReference returnValue) new AssignStatement( new PointerReference( new MethodInvocationExpression( + instance: null, + ByRefLikeReferenceUnsafeMethods.GetRawPtr, new MethodInvocationExpression( ThisExpression.Instance, InvocationMethods.GetReturnValue), - ByRefLikeReferenceMethods.GetPtr, new TypeTokenExpression(returnType)), returnType), returnValue)); diff --git a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs index b3d4a4a41..e01943740 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -303,7 +303,7 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments // Byref-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. - var referenceCtor = GetByRefLikeReferenceCtorFor(dereferencedArgumentType); + var referenceFactory = GetByRefLikeReferenceFactoryFor(dereferencedArgumentType); var reference = method.CodeBuilder.DeclareLocal(typeof(ByRefLikeReference)); // Notice, if a parameter is by-ref, scoped applies to the reference - not to the value itself. // We are interested only in tracking if the value is scoped. @@ -311,8 +311,9 @@ public void CopyIn(out LocalReference argumentsArray, out bool hasByRefArguments method.CodeBuilder.AddStatement( new AssignStatement( reference, - new NewInstanceExpression( - referenceCtor, + new MethodInvocationExpression( + instance: null, + referenceFactory, new TypeTokenExpression(dereferencedArgumentType), new AddressOfExpression(dereferencedArgument), new LiteralBoolExpression(valueIsScoped)))); @@ -383,10 +384,11 @@ public void InvalidateByRefLikeProxies(LocalReference argumentsArray, LocalRefer // a method argument that is about to be popped off the stack. method.CodeBuilder.AddStatement( new MethodInvocationExpression( + instance: null, + ByRefLikeReferenceUnsafeMethods.DisposeReference, new AsTypeExpression( new ArrayElementReference(argumentsArray, i), typeof(ByRefLikeReference)), - ByRefLikeReferenceMethods.Invalidate, argumentType.IsByRef ? argument : new AddressOfExpression(argument))); // Make the unusable substitute value unreachable by erasing it from `IInvocation.Arguments`. @@ -415,13 +417,14 @@ public void PrepareReturnValueBuffer(LocalReference invocation, out LocalReferen returnValueBuffer = method.CodeBuilder.DeclareLocal(returnType); - var referenceCtor = GetByRefLikeReferenceCtorFor(returnType); + var referenceFactory = GetByRefLikeReferenceFactoryFor(returnType); method.CodeBuilder.AddStatement( new MethodInvocationExpression( invocation, InvocationMethods.SetReturnValue, - new NewInstanceExpression( - referenceCtor, + new MethodInvocationExpression( + instance: null, + referenceFactory, new TypeTokenExpression(returnType), new AddressOfExpression(returnValueBuffer), // Return values are never scoped @@ -441,10 +444,11 @@ public void InvalidateReturnValueBuffer(LocalReference invocation, LocalReferenc // a local variable (the buffer) that is about to be popped off the stack. method.CodeBuilder.AddStatement( new MethodInvocationExpression( + instance: null, + ByRefLikeReferenceUnsafeMethods.DisposeReference, new AsTypeExpression( new MethodInvocationExpression(invocation, InvocationMethods.GetReturnValue), typeof(ByRefLikeReference)), - ByRefLikeReferenceMethods.Invalidate, new AddressOfExpression(returnValueBuffer))); // Make the unusable proxy unreachable by erasing it from the invocation arguments array. @@ -496,13 +500,13 @@ public void GetReturnValue(LocalReference invocation, out LocalReference returnV } #if FEATURE_BYREFLIKE - private static ConstructorInfo GetByRefLikeReferenceCtorFor(Type dereferencedArgumentType) + private static MethodInfo GetByRefLikeReferenceFactoryFor(Type dereferencedArgumentType) { #if NET9_0_OR_GREATER - // TODO: perhaps we should cache these `ConstructorInfo`s? - ConstructorInfo referenceCtor = typeof(ByRefLikeReference<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single(); + // TODO: perhaps we should cache these `MethodInfo`s? + MethodInfo referenceFactory = ByRefLikeReferenceUnsafeMethods.CreateByRefLikeReference.MakeGenericMethod(dereferencedArgumentType); #else - ConstructorInfo referenceCtor = ByRefLikeReferenceMethods.Constructor; + MethodInfo referenceFactory = ByRefLikeReferenceUnsafeMethods.CreateByRefLikeReferenceUntyped; #endif if (dereferencedArgumentType.IsConstructedGenericType) { @@ -510,16 +514,16 @@ private static ConstructorInfo GetByRefLikeReferenceCtorFor(Type dereferencedArg if (typeDef == typeof(ReadOnlySpan<>)) { var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; - referenceCtor = typeof(ReadOnlySpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); + referenceFactory = ByRefLikeReferenceUnsafeMethods.CreateReadOnlySpanReference.MakeGenericMethod(typeArg); } else if (typeDef == typeof(Span<>)) { var typeArg = dereferencedArgumentType.GetGenericArguments()[0]; - referenceCtor = typeof(SpanReference<>).MakeGenericType(typeArg).GetConstructors().Single(); + referenceFactory = ByRefLikeReferenceUnsafeMethods.CreateSpanReference.MakeGenericMethod(typeArg); } } - return referenceCtor; + return referenceFactory; } #endif } diff --git a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs deleted file mode 100644 index 5e0b81a56..000000000 --- a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceMethods.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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 diff --git a/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceUnsafeMethods.cs b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceUnsafeMethods.cs new file mode 100644 index 000000000..1b63221c3 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/Tokens/ByRefLikeReferenceUnsafeMethods.cs @@ -0,0 +1,41 @@ +// 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.Reflection; + + internal static class ByRefLikeReferenceUnsafeMethods + { + public static MethodInfo CreateByRefLikeReferenceUntyped = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.CreateByRefLikeReferenceUntyped))!; + +#if NET9_0_OR_GREATER + public static MethodInfo CreateByRefLikeReference = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.CreateByRefLikeReference))!; +#endif + + public static MethodInfo CreateReadOnlySpanReference = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.CreateReadOnlySpanReference))!; + + public static MethodInfo CreateSpanReference = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.CreateSpanReference))!; + + public static MethodInfo GetRawPtr = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.GetRawPtr))!; + + public static MethodInfo DisposeReference = typeof(ByRefLikeReferenceUnsafe).GetMethod(nameof(ByRefLikeReferenceUnsafe.DisposeReference))!; + } +} + +#endif From d79f682e556eecdc404289ad42a1065d22b8ddb1 Mon Sep 17 00:00:00 2001 From: Oleks Povar Date: Thu, 19 Mar 2026 23:18:51 +0100 Subject: [PATCH 8/8] Hide `ValueIsScoped` API under `ByRefLikeReferenceUnsafe` --- ref/Castle.Core-net8.0.cs | 6 ++---- ref/Castle.Core-net9.0.cs | 6 ++---- src/Castle.Core/DynamicProxy/ByRefLikeReference.cs | 4 ++-- src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs | 8 ++++++++ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 260f28b0b..69282491e 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2422,10 +2422,7 @@ 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 - { - public bool ValueIsScoped { get; } - } + public class ByRefLikeReference { } public static class ByRefLikeReferenceUnsafe { [System.CLSCompliant(false)] @@ -2438,6 +2435,7 @@ public static unsafe Castle.DynamicProxy.SpanReference CreateSpanReference public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { } [System.CLSCompliant(false)] public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { } + public static bool IsReferenceValueScoped(Castle.DynamicProxy.ByRefLikeReference reference) { } } public class CustomAttributeInfo : System.IEquatable { diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index eab448360..9a0429760 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2422,10 +2422,7 @@ 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 - { - public bool ValueIsScoped { get; } - } + public class ByRefLikeReference { } public static class ByRefLikeReferenceUnsafe { [System.CLSCompliant(false)] @@ -2441,6 +2438,7 @@ public static unsafe Castle.DynamicProxy.SpanReference CreateSpanReference public static unsafe void DisposeReference(Castle.DynamicProxy.ByRefLikeReference reference, void* expectedPtr) { } [System.CLSCompliant(false)] public static unsafe void* GetRawPtr(Castle.DynamicProxy.ByRefLikeReference reference, System.Type expectedType) { } + public static bool IsReferenceValueScoped(Castle.DynamicProxy.ByRefLikeReference reference) { } } public class ByRefLikeReference : Castle.DynamicProxy.ByRefLikeReference where TByRefLike : struct diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs index 230c2aaaa..af1811104 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReference.cs @@ -87,8 +87,8 @@ public unsafe class ByRefLikeReference private void* ptr; private Thread ownerThread; - - public bool ValueIsScoped { get; } + + internal bool ValueIsScoped { get; } internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) { diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs index b2bdd6601..742b00263 100644 --- a/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs @@ -70,6 +70,14 @@ public static unsafe void DisposeReference(ByRefLikeReference reference, void* e { reference.Dispose(expectedPtr); } + + /// + /// Returns information if argument value has `scoped` lifetime + /// + public static bool IsReferenceValueScoped(ByRefLikeReference reference) + { + return reference.ValueIsScoped; + } } #endif \ No newline at end of file