diff --git a/CHANGELOG.md b/CHANGELOG.md index b015dd13a..b3a9219e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,12 @@ Enhancements: - Now target frameworks are: .NET 8, .NET Framework 4.6.2, .NET Standard 2.0 - Tests platforms now are: .NET Framework 4.6.2, .NET 8, .NET 9 +- Minimally improved support for methods having `ref struct` parameter and return types, such as `Span`: Intercepting such methods caused the runtime to throw `InvalidProgramException` and `NullReferenceException` due to forbidden conversions of `ref struct` values when transferring them into & out of `IInvocation` instances. To prevent these exceptions from being thrown, such values now get replaced with `null` in `IInvocation`, and with `default` values in return values and `out` arguments. When proceeding to a target, the target methods likewise receive such nullified values. (@stakx, #665) - Dependencies were updated +Bugfixes: +- `InvalidProgramException` when proxying `MemoryStream` with .NET 7 (@stakx, #651) + Deprecations: - .NET Core 2.1, .NET Core 3.1, .NET 6, and mono tests - .NET Standard 2.1 tfm diff --git a/README.md b/README.md index c2ff79842..52b0b5006 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,16 @@ build.cmd The following conditional compilation symbols (vertical) are currently defined for each of the build configurations (horizontal): -Symbol | .NET 4.6.2 | .NET Standard 2.x and .NET 8+ ------------------------------------ | ------------------ | ----------------------------- -`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: -`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: -`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: -`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: +Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8 +----------------------------------- | ------------------ | ----------------- | ------------------ +`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: +`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: * `FEATURE_APPDOMAIN` - enables support for features that make use of an AppDomain in the host. * `FEATURE_ASSEMBLYBUILDER_SAVE` - enabled support for saving the dynamically generated proxy assembly. +* `FEATURE_BYREFLIKE` - enables support for by-ref-like (`ref struct`) types such as `Span` and `ReadOnlySpan`. * `FEATURE_SERIALIZATION` - enables support for serialization of dynamic proxies and other types. * `FEATURE_SYSTEM_CONFIGURATION` - enables features that use System.Configuration and the ConfigurationManager. diff --git a/buildscripts/common.props b/buildscripts/common.props index 81a3e26d1..b2989c095 100644 --- a/buildscripts/common.props +++ b/buildscripts/common.props @@ -66,6 +66,10 @@ $(NetStandard20Constants) + + $(DefineConstants);FEATURE_BYREFLIKE + + diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs new file mode 100644 index 000000000..0c1a1195e --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs @@ -0,0 +1,421 @@ +// Copyright 2004-2025 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.Tests +{ + using System; + + using Castle.DynamicProxy.Tests.Interceptors; + + using NUnit.Framework; + + /// + /// Tests for by-ref-like ( ) method parameter and return types. + /// + [TestFixture] + public class ByRefLikeTestCase : BasePEVerifyTestCase + { + [TestCase(typeof(IHaveMethodWithByRefLikeParameter))] + [TestCase(typeof(IHaveMethodWithByRefLikeInParameter))] + [TestCase(typeof(IHaveMethodWithByRefLikeRefParameter))] + [TestCase(typeof(IHaveMethodWithByRefLikeOutParameter))] + [TestCase(typeof(IHaveMethodWithByRefLikeReturnType))] + public void Can_proxy_type(Type interfaceType) + { + _ = generator.CreateInterfaceProxyWithoutTarget(interfaceType); + } + + #region Can methods with by-ref-like parameters be intercepted without crashing? + + [Test] + public void Can_invoke_method_with_by_ref_like_parameter() + { + var proxy = generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); + var arg = default(ByRefLike); + proxy.Method(arg); + } + + [Test] + public void Can_invoke_method_with_by_ref_like_in_parameter() + { + var proxy = generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); + ByRefLike arg = default; + proxy.Method(in arg); + } + + [Test] + public void Can_invoke_method_with_by_ref_like_ref_parameter() + { + var proxy = generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); + ByRefLike arg = default; + proxy.Method(ref arg); + } + + [Test] + public void Can_invoke_method_with_by_ref_like_out_parameter() + { + var proxy = generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); + proxy.Method(out _); + } + + [Test] + public void Can_invoke_method_with_by_ref_like_return_type() + { + var proxy = generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); + _ = proxy.Method(); + } + + #endregion + + #region Can methods with by-ref-like parameters be proceeded to without crashing? + + [Test] + public void Can_proceed_to_target_method_with_by_ref_like_parameter() + { + var target = new HasMethodWithByRefLikeParameter(); + var proxy = generator.CreateInterfaceProxyWithTarget(target, new StandardInterceptor()); + ByRefLike arg = default; + proxy.Method(arg); + } + + [Test] + public void Can_proceed_to_target_method_with_by_ref_like_in_parameter() + { + var target = new HasMethodWithByRefLikeInParameter(); + var proxy = generator.CreateInterfaceProxyWithTarget(target, new StandardInterceptor()); + ByRefLike arg = default; + proxy.Method(in arg); + } + + [Test] + public void Can_proceed_to_target_method_with_by_ref_like_ref_parameter() + { + var target = new HasMethodWithByRefLikeRefParameter(); + var proxy = generator.CreateInterfaceProxyWithTarget(target, new StandardInterceptor()); + ByRefLike arg = default; + proxy.Method(ref arg); + } + + [Test] + public void Can_proceed_to_target_method_with_by_ref_like_out_parameter() + { + var target = new HasMethodWithByRefLikeOutParameter(); + var proxy = generator.CreateInterfaceProxyWithTarget(target, new StandardInterceptor()); + proxy.Method(out _); + } + + [Test] + public void Can_proceed_to_target_method_with_by_ref_like_return_type() + { + var target = new HasMethodWithByRefLikeReturnType(); + var proxy = generator.CreateInterfaceProxyWithTarget(target, new StandardInterceptor()); + _ = proxy.Method(); + } + + #endregion + + public ref struct ByRefLike + { + } + + public interface IHaveMethodWithByRefLikeParameter + { + void Method(ByRefLike arg); + } + + public class HasMethodWithByRefLikeParameter : IHaveMethodWithByRefLikeParameter + { + public virtual void Method(ByRefLike arg) + { + } + } + + public interface IHaveMethodWithByRefLikeInParameter + { + void Method(in ByRefLike arg); + } + + public class HasMethodWithByRefLikeInParameter : IHaveMethodWithByRefLikeInParameter + { + public virtual void Method(in ByRefLike arg) + { + } + } + + public interface IHaveMethodWithByRefLikeRefParameter + { + void Method(ref ByRefLike arg); + } + + public class HasMethodWithByRefLikeRefParameter : IHaveMethodWithByRefLikeRefParameter + { + public virtual void Method(ref ByRefLike arg) + { + } + } + + public interface IHaveMethodWithByRefLikeOutParameter + { + void Method(out ByRefLike arg); + } + + public class HasMethodWithByRefLikeOutParameter : IHaveMethodWithByRefLikeOutParameter + { + public virtual void Method(out ByRefLike arg) + { + arg = default; + } + } + + public interface IHaveMethodWithByRefLikeReturnType + { + ByRefLike Method(); + } + + public class HasMethodWithByRefLikeReturnType : IHaveMethodWithByRefLikeReturnType + { + public virtual ByRefLike Method() + { + return default; + } + } + + #region What values do interceptors see for by-ref-like arguments? + + [Test] + public void By_ref_like_arguments_are_replaced_with_null_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(arg); + Assert.IsNull(interceptor.ObservedArg); + } + + [Test] + public void By_ref_like_in_arguments_are_replaced_with_null_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assert.IsNull(interceptor.ObservedArg); + } + + [Test] + public void By_ref_like_ref_arguments_are_replaced_with_null_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.IsNull(interceptor.ObservedArg); + } + + // Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values + // of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter"). + // DynamicProxy does not distinguish between `ref` and `out` in this regard. + [Test] + public void By_ref_like_out_arguments_are_replaced_with_null_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(out arg); + Assert.IsNull(interceptor.ObservedArg); + } + + #endregion + + #region What values do proceeded-to targets see for by-ref-like arguments? + + // This test merely describes the status quo, and not the behavior we'd ideally want. + [Test] + public void By_ref_like_arguments_arrive_reset_to_default_value_at_target() + { + var target = new HasMethodWithSpanParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(arg); + Assert.AreEqual("", target.RecordedArg); + } + + // This test merely describes the status quo, and not the behavior we'd ideally want. + [Test] + public void By_ref_like_in_arguments_arrive_reset_to_default_value_at_target() + { + var target = new HasMethodWithSpanInParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assert.AreEqual("", target.RecordedArg); + } + + // This test merely describes the status quo, and not the behavior we'd ideally want. + [Test] + public void By_ref_like_ref_arguments_arrive_reset_to_default_value_at_target() + { + var target = new HasMethodWithSpanRefParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.AreEqual("", target.RecordedArg); + } + + #endregion + + #region How are by-ref-like by-ref arguments changed by interception? + + [Test] + public void By_ref_like_in_arguments_are_left_unchanged() + { + var proxy = generator.CreateClassProxy(new DoNothingInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assert.AreEqual("original", arg.ToString()); + } + + [Test] + public void By_ref_like_in_arguments_are_left_unchanged_if_interception_includes_proceed_to_target() + { + var target = new HasMethodWithSpanInParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assert.AreEqual("original", arg.ToString()); + } + + [Test] + public void By_ref_like_ref_arguments_are_left_unchanged() + { + var proxy = generator.CreateClassProxy(new DoNothingInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.AreEqual("original", arg.ToString()); + } + + [Test] + public void By_ref_like_ref_arguments_are_left_unchanged_if_interception_includes_proceed_to_target() + { + var target = new HasMethodWithSpanRefParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new DoNothingInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.AreEqual("original", arg.ToString()); + } + + // This test merely describes the status quo, and not the behavior we'd ideally want: + // DynamicProxy records the initial values of all by-ref arguments in `IInvocation.Arguments` and, + // unless changed during interception, writes out the final value for both `ref` and `out` parameters + // from there... meaning all non-by-ref-like by-ref arguments are by default left unchanged. + // This cannot work for by-ref-likes, since their initial value cannot be preserved in `Arguments`. + // To honor the semantics of `out` parameters DynamicProxy *does* write a value (unlike with `ref`, + // above, where it is free to choose not to). + [Test] + public void By_ref_like_out_arguments_are_reset_to_default_value() + { + var proxy = generator.CreateClassProxy(new DoNothingInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(out arg); + Assert.AreEqual("", arg.ToString()); + } + + // Once we manage to change the implementation so that `out` arguments aren't reset, + // and the above test is replaced with an `are_left_unchanged` version, then we should also add + // an additional `are_left_unchanged_if_interception_includes_proceed_to_target` test variant. + + #endregion + + #region Can interception targets set by-ref-like by-ref arguments? + + // This test merely describes the status quo, and not the behavior we'd ideally want. + [Test] + public void By_ref_like_ref_arguments_cannot_be_set_by_target() + { + var target = new HasMethodWithSpanRefParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.AreEqual("original", arg.ToString()); // ideally, would be equal to "set" + } + + // This test merely describes the status quo, and not the behavior we'd ideally want. + [Test] + public void By_ref_like_out_arguments_cannot_be_set_by_target() + { + var target = new HasMethodWithSpanOutParameter(); + var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor()); + var arg = "original".AsSpan(); + proxy.Method(out arg); + Assert.AreEqual("", arg.ToString()); // ideally, would be equal to "set" + } + + #endregion + + public class HasMethodWithSpanParameter + { + public string? RecordedArg; + + public virtual void Method(ReadOnlySpan arg) + { + RecordedArg = arg.ToString(); + } + } + + public class HasMethodWithSpanInParameter + { + public string? RecordedArg; + + public virtual void Method(in ReadOnlySpan arg) + { + RecordedArg = arg.ToString(); + } + } + + public class HasMethodWithSpanRefParameter + { + public string? RecordedArg; + + public virtual void Method(ref ReadOnlySpan arg) + { + RecordedArg = arg.ToString(); + arg = "set".AsSpan(); + } + } + + public class HasMethodWithSpanOutParameter + { + public virtual void Method(out ReadOnlySpan arg) + { + arg = "set".AsSpan(); + } + } + + public class ObservingInterceptor : IInterceptor + { + public object? ObservedArg; + + public void Intercept(IInvocation invocation) + { + ObservedArg = invocation.Arguments[0]; + } + } + } +} + +#endif diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs index dea87ef9c..a611fd54f 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs @@ -18,6 +18,8 @@ namespace Castle.DynamicProxy.Generators.Emitters.SimpleAST using System.Reflection; using System.Reflection.Emit; + using Castle.DynamicProxy.Internal; + internal class ReferencesToObjectArrayExpression : IExpression { private readonly TypeReference[] args; @@ -42,6 +44,20 @@ public void Emit(ILGenerator gen) var reference = args[i]; +#if FEATURE_BYREFLIKE + if (reference.Type.IsByRefLikeSafe()) + { + // The by-ref-like argument value cannot be put into the `object[]` array, + // because it cannot be boxed. We need to replace it with some other value. + + // For now, we just erase it by substituting `null`: + gen.Emit(OpCodes.Ldnull); + gen.Emit(OpCodes.Stelem_Ref); + + continue; + } +#endif + ArgumentsUtil.EmitLoadOwnerAndReference(reference, gen); if (reference.Type.IsByRef) diff --git a/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs b/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs index eb9ff841a..97a221a6a 100644 --- a/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs +++ b/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs @@ -20,6 +20,7 @@ namespace Castle.DynamicProxy.Generators using Castle.DynamicProxy.Generators.Emitters; using Castle.DynamicProxy.Generators.Emitters.SimpleAST; + using Castle.DynamicProxy.Internal; using Castle.DynamicProxy.Tokens; internal static class GeneratorUtil @@ -41,7 +42,32 @@ public static void CopyOutAndRefParameters(TypeReference[] dereferencedArguments arguments = StoreInvocationArgumentsInLocal(emitter, invocation); } - emitter.CodeBuilder.AddStatement(AssignArgument(dereferencedArguments, i, arguments)); +#if FEATURE_BYREFLIKE + var dereferencedParameterType = parameters[i].ParameterType.GetElementType(); + if (dereferencedParameterType.IsByRefLikeSafe()) + { + // The argument value in the invocation `Arguments` array is an `object` + // and cannot be converted back to its original by-ref-like type. + // We need to replace it with some other value. + + // For now, we just substitute the by-ref-like type's default value: + if (parameters[i].IsOut) + { + emitter.CodeBuilder.AddStatement(new AssignStatement(dereferencedArguments[i], new DefaultValueExpression(dereferencedParameterType))); + } + else + { + // ... except when we're dealing with a `ref` parameter. Unlike with `out`, + // where we would be expected to definitely assign to it, we are free to leave + // the original incoming value untouched. For now, that's likely the better + // interim solution than unconditionally resetting. + } + } + else +#endif + { + emitter.CodeBuilder.AddStatement(AssignArgument(dereferencedArguments, i, arguments)); + } } } diff --git a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs index 15b6584a3..0065584b7 100644 --- a/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/InvocationTypeGenerator.cs @@ -127,24 +127,53 @@ protected virtual void ImplementInvokeMethodOnTarget(AbstractTypeEmitter invocat if (paramType.IsByRef) { var localReference = invokeMethodOnTarget.CodeBuilder.DeclareLocal(paramType.GetElementType()); - invokeMethodOnTarget.CodeBuilder - .AddStatement( - new AssignStatement(localReference, - new ConvertExpression(paramType.GetElementType(), - new MethodInvocationExpression(SelfReference.Self, - InvocationMethods.GetArgumentValue, - new LiteralIntExpression(i))))); + IExpression localValue; + +#if FEATURE_BYREFLIKE + if (paramType.GetElementType().IsByRefLikeSafe()) + { + // The argument value in the invocation `Arguments` array is an `object` + // and cannot be converted back to its original by-ref-like type. + // We need to replace it with some other value. + + // For now, we just substitute the by-ref-like type's default value: + localValue = new DefaultValueExpression(localReference.Type); + } + else +#endif + { + localValue = new ConvertExpression(paramType.GetElementType(), + new MethodInvocationExpression(SelfReference.Self, + InvocationMethods.GetArgumentValue, + new LiteralIntExpression(i))); + } + + invokeMethodOnTarget.CodeBuilder.AddStatement(new AssignStatement(localReference, localValue)); var byRefReference = new ByRefReference(localReference); args[i] = byRefReference; byRefArguments[i] = localReference; } else { - args[i] = - new ConvertExpression(paramType, - new MethodInvocationExpression(SelfReference.Self, - InvocationMethods.GetArgumentValue, - new LiteralIntExpression(i))); +#if FEATURE_BYREFLIKE + if (paramType.IsByRefLikeSafe()) + { + // The argument value in the invocation `Arguments` array is an `object` + // and cannot be converted back to its original by-ref-like type. + // We need to replace it with some other value. + + // For now, we just substitute the by-ref-like type's default value: + args[i] = new DefaultValueExpression(paramType); + } + else +#endif + { + args[i] = + new ConvertExpression(paramType, + new MethodInvocationExpression(SelfReference.Self, + InvocationMethods.GetArgumentValue, + new LiteralIntExpression(i))); + } } } @@ -171,10 +200,27 @@ protected virtual void ImplementInvokeMethodOnTarget(AbstractTypeEmitter invocat if (callbackMethod.ReturnType != typeof(void)) { + IExpression retVal; + +#if FEATURE_BYREFLIKE + if (returnValue.Type.IsByRefLikeSafe()) + { + // The by-ref-like return value cannot be put into the `ReturnValue` property, + // because it cannot be boxed. We need to replace it with some other value. + + // For now, we just erase it by substituting `null`: + retVal = NullExpression.Instance; + } + else +#endif + { + retVal = new ConvertExpression(typeof(object), returnValue.Type, returnValue); + } + var setRetVal = new MethodInvocationExpression(SelfReference.Self, InvocationMethods.SetReturnValue, - new ConvertExpression(typeof(object), returnValue.Type, returnValue)); + retVal); invokeMethodOnTarget.CodeBuilder.AddStatement(setRetVal); } @@ -194,15 +240,30 @@ private void AssignBackByRefArguments(MethodEmitter invokeMethodOnTarget, Dictio { var index = byRefArgument.Key; var localReference = byRefArgument.Value; + IExpression localValue; + +#if FEATURE_BYREFLIKE + if (localReference.Type.IsByRefLikeSafe()) + { + // The by-ref-like value in the local buffer variable cannot be put back + // into the invocation `Arguments` array, because it cannot be boxed. + // We need to replace it with some other value. + + // For now, we just erase it by substituting `null`: + localValue = NullExpression.Instance; + } + else +#endif + { + localValue = new ConvertExpression(typeof(object), localReference.Type, localReference); + } + invokeMethodOnTarget.CodeBuilder.AddStatement( new MethodInvocationExpression( SelfReference.Self, InvocationMethods.SetArgumentValue, new LiteralIntExpression(index), - new ConvertExpression( - typeof(object), - localReference.Type, - localReference))); + localValue)); } invokeMethodOnTarget.CodeBuilder.AddStatement(new EndExceptionBlockStatement()); } diff --git a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs index ccf636a39..184c0ce48 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MethodWithInvocationGenerator.cs @@ -138,20 +138,37 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C if (MethodToOverride.ReturnType != typeof(void)) { - var getRetVal = new MethodInvocationExpression(invocationLocal, InvocationMethods.GetReturnValue); + IExpression retVal; - // Emit code to ensure a value type return type is not null, otherwise the cast will cause a null-deref - if (emitter.ReturnType.IsValueType && !emitter.ReturnType.IsNullableType()) +#if FEATURE_BYREFLIKE + if (emitter.ReturnType.IsByRefLikeSafe()) { - LocalReference returnValue = emitter.CodeBuilder.DeclareLocal(typeof(object)); - emitter.CodeBuilder.AddStatement(new AssignStatement(returnValue, getRetVal)); + // The return value in the `ReturnValue` property is an `object` + // and cannot be converted back to the original by-ref-like return type. + // We need to replace it with some other value. - emitter.CodeBuilder.AddStatement(new IfNullExpression(returnValue, new ThrowStatement(typeof(InvalidOperationException), - "Interceptors failed to set a return value, or swallowed the exception thrown by the target"))); + // For now, we just substitute the by-ref-like type's default value: + retVal = new DefaultValueExpression(emitter.ReturnType); + } + else +#endif + { + retVal = new MethodInvocationExpression(invocationLocal, InvocationMethods.GetReturnValue); + + // Emit code to ensure a value type return type is not null, otherwise the cast will cause a null-deref + if (emitter.ReturnType.IsValueType && !emitter.ReturnType.IsNullableType()) + { + LocalReference returnValue = emitter.CodeBuilder.DeclareLocal(typeof(object)); + emitter.CodeBuilder.AddStatement(new AssignStatement(returnValue, retVal)); + + emitter.CodeBuilder.AddStatement(new IfNullExpression(returnValue, new ThrowStatement(typeof(InvalidOperationException), + "Interceptors failed to set a return value, or swallowed the exception thrown by the target"))); + } + + retVal = new ConvertExpression(emitter.ReturnType, retVal); } - // Emit code to return with cast from ReturnValue - emitter.CodeBuilder.AddStatement(new ReturnStatement(new ConvertExpression(emitter.ReturnType, getRetVal))); + emitter.CodeBuilder.AddStatement(new ReturnStatement(retVal)); } else { diff --git a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs index 5a004ff1f..30159bf81 100644 --- a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs +++ b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs @@ -172,6 +172,22 @@ internal static Type[] AsTypeArray(this GenericTypeParameterBuilder[] typeInfos) return types; } +#if FEATURE_BYREFLIKE + internal static bool IsByRefLikeSafe(this Type type) + { + try + { + return type.IsByRefLike; + } + catch (NotSupportedException) + { + // Certain System.Reflection.Emit implementations of `Type.IsByRefLike` throw this exception + // with the message "Derived classes must provide an implementation." This might be a bug in the runtime. + return false; + } + } +#endif + internal static bool IsFinalizer(this MethodInfo methodInfo) { return string.Equals("Finalize", methodInfo.Name) && methodInfo.GetBaseDefinition().DeclaringType == typeof(object);