diff --git a/ref/Castle.Core-net8.0.cs b/ref/Castle.Core-net8.0.cs index 5a9c621df..69282491e 100644 --- a/ref/Castle.Core-net8.0.cs +++ b/ref/Castle.Core-net8.0.cs @@ -2422,14 +2422,20 @@ 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 class ByRefLikeReference { } + public static class ByRefLikeReferenceUnsafe { [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } + 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 static bool IsReferenceValueScoped(Castle.DynamicProxy.ByRefLikeReference reference) { } } public class CustomAttributeInfo : System.IEquatable { @@ -2701,15 +2707,23 @@ public static bool IsProxyType(System.Type type) { } } public class ReadOnlySpanReference : Castle.DynamicProxy.ByRefLikeReference { - [System.CLSCompliant(false)] - public ReadOnlySpanReference(System.Type type, void* ptr) { } - 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) { } - 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 87d3f4334..9a0429760 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2422,21 +2422,34 @@ 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 class ByRefLikeReference { } + public static class ByRefLikeReferenceUnsafe { [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } + public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReference(System.Type type, void* ptr, bool valueIsScoped) + where TByRefLike : struct { } [System.CLSCompliant(false)] - public unsafe void* GetPtr(System.Type checkType) { } + public static unsafe Castle.DynamicProxy.ByRefLikeReference CreateByRefLikeReferenceUntyped(System.Type type, void* ptr, bool valueIsScoped) { } [System.CLSCompliant(false)] - public unsafe void Invalidate(void* checkPtr) { } + 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 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 { - [System.CLSCompliant(false)] - public ByRefLikeReference(System.Type type, void* ptr) { } - 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 { @@ -2716,16 +2729,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) { } - } - public class SpanReference : Castle.DynamicProxy.ByRefLikeReference> - { - [System.CLSCompliant(false)] - public SpanReference(System.Type type, void* ptr) { } - } + 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 8717b567e..68877ae28 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 @@ -40,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); }); } @@ -48,53 +49,84 @@ 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] - public unsafe void Invalidate_throws_if_address_mismatch() + [TestCase(true)] + [TestCase(false)] + public unsafe void Ctor_preserves_value_is_scoped_value(bool valueIsScoped) { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); - Assert.Throws(() => + var result = new ByRefLikeReference(typeof(ReadOnlySpan), &local, valueIsScoped: valueIsScoped); + Assert.AreEqual(valueIsScoped, result.ValueIsScoped); + } + + [Test] + 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.Dispose(&local); + } + + [Test] + public unsafe void Dispose_throws_when_access_from_other_thread() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); - reference.Invalidate(&local); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); + var address = reference.GetPtr(typeof(ReadOnlySpan)); + var task = Task.Run(() => reference.Dispose(address)); + var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; + StringAssert.Contains("thread", msg); } [Test] public unsafe void GetPtr_throws_if_type_mismatch() { ReadOnlySpan local = default; - var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local); - Assert.Throws(() => reference.GetPtr(typeof(bool))); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); + Assert.Throws(() => reference.GetPtr(typeof(bool))); } [Test] 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); } [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); - reference.Invalidate(&local); - Assert.Throws(() => reference.GetPtr(typeof(ReadOnlySpan))); + var reference = new ByRefLikeReference(typeof(ReadOnlySpan), &local, false); + reference.Dispose(&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, false); + var task = Task.Run(() => reference.GetPtr(typeof(ReadOnlySpan))); + var msg = Assert.Throws(() => task.GetAwaiter().GetResult()).Message; + StringAssert.Contains("thread", msg); } #endregion @@ -109,35 +141,71 @@ 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); }); } - public unsafe void ReadOnlySpanReference_Value_returns_equal_span() + [Test] + [TestCase(true)] + [TestCase(false)] + public unsafe void ReadOnlySpanReference_ctor_preserves_value_is_scoped_value(bool valueIsScoped) { - ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); - Assert.True(reference.Value == "foo".AsSpan()); + ReadOnlySpan local = default; + var result = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, valueIsScoped: valueIsScoped); + Assert.AreEqual(valueIsScoped, result.ValueIsScoped); } -#if NET9_0_OR_GREATER - [Test] - public unsafe void ReadOnlySpanReference_Value_returns_same_span() + public unsafe void ReadOnlySpanReference_GetValue_returns_equal_span() { ReadOnlySpan local = "foo".AsSpan(); - var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local); - Assert.True(Unsafe.AreSame(ref reference.Value, ref local)); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); + Assert.True(reference.GetValue() == "foo".AsSpan()); + } + + public unsafe void ReadOnlySpanReference_UseValue_returns_equal_span() + { + ReadOnlySpan local = "foo".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); + 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); - reference.Value = "bar".AsSpan(); + var reference = new ReadOnlySpanReference(typeof(ReadOnlySpan), &local, false); + reference.SetValue(() => "bar".AsSpan()); Assert.True(local == "bar".AsSpan()); } + + [Test] + 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.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.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeSupport/CallerToInterceptorTestCase.cs index 3877297a7..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); }); } @@ -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 @@ -149,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)); }); } @@ -166,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)); }); } @@ -183,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)); }); } @@ -204,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)); }); } @@ -221,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)); }); } @@ -238,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 079168945..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,25 @@ 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")); + }); + } + + /// + /// 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); }); } @@ -105,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()); }); } @@ -122,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()); }); } @@ -139,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()); }); } @@ -161,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()); }); } @@ -178,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()); }); } @@ -195,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/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/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.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 457f8f470..af1811104 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,49 +41,56 @@ 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.) // - // *) 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. + // *) 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. 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 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 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)] private readonly Type type; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private nint ptr; - - /// - /// Do not use! This constructor should only be called by DynamicProxy internals. - /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public ByRefLikeReference(Type type, void* ptr) + private void* ptr; + + private Thread ownerThread; + + internal bool ValueIsScoped { get; } + + internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) { if (type.IsByRefLikeSafe() == false) { @@ -97,48 +103,45 @@ public ByRefLikeReference(Type type, void* ptr) } this.type = type; - this.ptr = (nint)ptr; + this.ptr = ptr; + this.ValueIsScoped = 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(); + if (checkType != type) { - throw new AccessViolationException(); + throw new ArgumentException($"The reference type {type.FullName} does not match the expected type {checkType.FullName}"); } - return GetPtrNocheck(); + if (this.ptr == null) + { + throw new ObjectDisposedException("This reference was already disposed"); + } + + return this.ptr; } - internal void* GetPtrNocheck() + internal void Dispose(void* checkPtr) { - var ptr = (void*)Volatile.Read(ref this.ptr); - - if (ptr == null) + AssertCurrentThread(); + + if (this.ptr == null || this.ptr != checkPtr) { - throw new AccessViolationException(); + throw new InvalidOperationException($"BUG: Pointer mismatch on reference disposal. Expected: {(nint)checkPtr:X16}, Actual: {(nint)this.ptr:X16}"); } - return ptr; + this.ptr = null; } - /// - /// Do not use! This method should only be called by DynamicProxy internals. - /// - [CLSCompliant(false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public void Invalidate(void* checkPtr) + private void AssertCurrentThread() { - var ptr = (void*)Interlocked.CompareExchange(ref this.ptr, (nint)null, (nint)checkPtr); - - if (ptr == null || checkPtr != ptr) + if (this.ownerThread != Thread.CurrentThread) { - throw new AccessViolationException(); + throw new InvalidOperationException("This reference cannot be used from another thread"); } } } @@ -162,13 +165,8 @@ public void Invalidate(void* checkPtr) 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) - : base(type, ptr) + internal ByRefLikeReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(TByRefLike)) { @@ -176,13 +174,44 @@ public ByRefLikeReference(Type type, void* ptr) } } - public ref TByRefLike Value + private ref TByRefLike Value { get { - return ref *(TByRefLike*)GetPtrNocheck(); + 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 @@ -207,13 +236,8 @@ 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) - : base(type, ptr) + internal ReadOnlySpanReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(ReadOnlySpan)) { @@ -222,13 +246,44 @@ public ReadOnlySpanReference(Type type, void* ptr) } #if !NET9_0_OR_GREATER - public ref ReadOnlySpan Value + private ref ReadOnlySpan Value { get { - return ref *(ReadOnlySpan*)GetPtrNocheck(); + 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 } @@ -253,13 +308,8 @@ 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) - : base(type, ptr) + internal SpanReference(Type type, void* ptr, bool valueIsScoped) + : base(type, ptr, valueIsScoped) { if (type != typeof(Span)) { @@ -268,13 +318,44 @@ public SpanReference(Type type, void* ptr) } #if !NET9_0_OR_GREATER - public ref Span Value + private ref Span Value { get { - return ref *(Span*)GetPtrNocheck(); + 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 } } diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs new file mode 100644 index 000000000..742b00263 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/ByRefLikeReferenceUnsafe.cs @@ -0,0 +1,83 @@ +// 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); + } + + /// + /// Returns information if argument value has `scoped` lifetime + /// + public static bool IsReferenceValueScoped(ByRefLikeReference reference) + { + return reference.ValueIsScoped; + } +} + +#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 9ec97f9f6..e01943740 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -303,15 +303,20 @@ 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. + var valueIsScoped = parameters[i].GetCustomAttribute() != null && !parameters[i].IsByRef; method.CodeBuilder.AddStatement( new AssignStatement( reference, - new NewInstanceExpression( - referenceCtor, + new MethodInvocationExpression( + instance: null, + referenceFactory, new TypeTokenExpression(dereferencedArgumentType), - new AddressOfExpression(dereferencedArgument)))); + new AddressOfExpression(dereferencedArgument), + new LiteralBoolExpression(valueIsScoped)))); dereferencedArgument = reference; } @@ -379,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`. @@ -411,15 +417,18 @@ 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)))); + new AddressOfExpression(returnValueBuffer), + // Return values are never scoped + new LiteralBoolExpression(false)))); #else returnValueBuffer = null; #endif @@ -435,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. @@ -490,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) { @@ -504,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