diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ArgumentsUtilTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ArgumentsUtilTestCase.cs new file mode 100644 index 000000000..129266cea --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ArgumentsUtilTestCase.cs @@ -0,0 +1,64 @@ +// 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. + +namespace Castle.DynamicProxy.Tests +{ + using System.Linq; + using System.Reflection; + + using Castle.DynamicProxy.Generators.Emitters; + using Castle.DynamicProxy.Internal; + + using NUnit.Framework; + + [TestFixture] + public class ArgumentsUtilTestCase + { + public interface Methods + { + public void ByValue(int arg); + public void In(in int arg); + public void Out(out int arg); + public void Ref(ref int arg); + public void RefReadonly(ref readonly int arg); + } + + [TestCase(nameof(Methods.ByValue), false)] + [TestCase(nameof(Methods.In), true)] + [TestCase(nameof(Methods.Out), true)] + [TestCase(nameof(Methods.Ref), true)] + [TestCase(nameof(Methods.RefReadonly), true)] + public void IsByRef(string methodName, bool expected) + { + var parameter = GetParameterOf(methodName); + Assert.AreEqual(expected, parameter.IsByRef); + } + + [TestCase(nameof(Methods.In), true)] + [TestCase(nameof(Methods.Out), false)] + [TestCase(nameof(Methods.Ref), false)] + [TestCase(nameof(Methods.RefReadonly), true)] + public void IsReadOnly(string methodName, bool expected) + { + var parameter = GetParameterOf(methodName); + Assume.That(parameter.ParameterType.IsByRef); + Assert.AreEqual(expected, parameter.IsReadOnly); + } + + private ParameterInfo GetParameterOf(string methodName) + { + return typeof(Methods).GetMethod(methodName)!.GetParameters().Single(); + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassEmitterTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassEmitterTestCase.cs index cef0481cc..c36101e60 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassEmitterTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassEmitterTestCase.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -41,18 +41,6 @@ public void AutomaticDefaultConstructorGenerationWithClosedGenericType() Activator.CreateInstance(t); } - [Test] - public void StaticMethodArguments() - { - ClassEmitter emitter = new ClassEmitter(generator.ProxyBuilder.ModuleScope, "Foo", typeof (List), - Type.EmptyTypes); - MethodEmitter methodEmitter = emitter.CreateMethod("StaticMethod", MethodAttributes.Public | MethodAttributes.Static, - typeof (string), typeof (string)); - methodEmitter.CodeBuilder.AddStatement(new ReturnStatement(methodEmitter.Arguments[0])); - Type t = emitter.BuildType(); - Assert.AreEqual("five", t.GetMethod("StaticMethod").Invoke(null, new object[] {"five"})); - } - [Test] public void InstanceMethodArguments() { diff --git a/src/Castle.Core/DynamicProxy/Contributors/CompositeTypeContributor.cs b/src/Castle.Core/DynamicProxy/Contributors/CompositeTypeContributor.cs index 6a165d0d9..7c93efc11 100644 --- a/src/Castle.Core/DynamicProxy/Contributors/CompositeTypeContributor.cs +++ b/src/Castle.Core/DynamicProxy/Contributors/CompositeTypeContributor.cs @@ -139,14 +139,14 @@ private void ImplementMethod(MetaMethod method, ClassEmitter @class, protected static Type[] GetCacheKeyTypes(MetaMethod method) { - Type[] argumentTypes = ArgumentsUtil.GetTypes(method.MethodOnTarget.GetParameters()); - if (argumentTypes.Length < 1) + Type[] parameterTypes = method.MethodOnTarget.GetParameters().GetTypes(); + if (parameterTypes.Length < 1) { return new[] { method.MethodOnTarget.ReturnType }; } - var types = new Type[argumentTypes.Length + 1]; + var types = new Type[parameterTypes.Length + 1]; types[0] = method.MethodOnTarget.ReturnType; - argumentTypes.CopyTo(types, 1); + parameterTypes.CopyTo(types, 1); return types; } diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/ArgumentsUtil.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/ArgumentsUtil.cs index 388ceb028..846c9de1d 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/ArgumentsUtil.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/ArgumentsUtil.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -15,79 +15,135 @@ namespace Castle.DynamicProxy.Generators.Emitters { using System; + using System.Linq; using System.Reflection; - using System.Reflection.Emit; + using System.Runtime.InteropServices; using Castle.DynamicProxy.Generators.Emitters.SimpleAST; - internal abstract class ArgumentsUtil + internal static class ArgumentsUtil { - public static ArgumentReference[] ConvertToArgumentReference(Type[] args) + extension(ParameterInfo parameter) { - var arguments = new ArgumentReference[args.Length]; - - for (var i = 0; i < args.Length; ++i) + public bool IsByRef { - arguments[i] = new ArgumentReference(args[i]); + get + { + return parameter.ParameterType.IsByRef; + } } - return arguments; + public bool IsReadOnly + { + get + { + // C# `in` parameters are also by-ref, but meant to be read-only. + // The section "Metadata representation of in parameters" on the following page + // defines how such parameters are marked: + // + // https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/readonly-ref.md + // + // This poses three problems for detecting them: + // + // * The C# Roslyn compiler marks `in` parameters with an `[in]` IL modifier, + // but this isn't specified, nor is it used uniquely for `in` params. + // + // * `System.Runtime.CompilerServices.IsReadOnlyAttribute` is not defined on all + // .NET platforms, so the compiler sometimes recreates that type in the same + // assembly that contains the method having an `in` parameter. In other words, + // it's an attribute one must check for by name (which is slow, as it implies + // use of a `GetCustomAttributes` enumeration instead of a faster `IsDefined`). + // + // * A required custom modifier `System.Runtime.InteropServices.InAttribute` + // is always present in those cases relevant for DynamicProxy (proxyable methods), + // but not all targeted platforms support reading custom modifiers. Also, + // support for cmods is generally flaky (at this time of writing, mid-2018). + // + // The above points inform the following detection logic: + + // First, fast-guard against non-`in` params by checking for an IL `[in]` modifier: + if ((parameter.Attributes & (ParameterAttributes.In | ParameterAttributes.Out)) != ParameterAttributes.In) + { + return false; + } + + // Second, check for the required modifiers (hoping for good modreq support): + if (parameter.GetRequiredCustomModifiers().Any(x => x == typeof(InAttribute))) + { + return true; + } + + // Third, check for `IsReadOnlyAttribute` by name (see explanation above). + // This check is likely the slowest (despite being accurate) so we do it last: + if (parameter.GetCustomAttributes(false).Any(x => x.GetType().FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute")) + { + return true; + } + + return false; + } + } } - public static ArgumentReference[] ConvertToArgumentReference(ParameterInfo[] args) + extension(ParameterInfo[] parameters) { - var arguments = new ArgumentReference[args.Length]; - - for (var i = 0; i < args.Length; ++i) + public ArgumentReference[] ConvertToArgumentReferences() { - arguments[i] = new ArgumentReference(args[i].ParameterType); - } + if (parameters.Length == 0) return []; - return arguments; - } + var arguments = new ArgumentReference[parameters.Length]; - public static IExpression[] ConvertToArgumentReferenceExpression(ParameterInfo[] args) - { - var arguments = new IExpression[args.Length]; + for (var i = 0; i < parameters.Length; ++i) + { + arguments[i] = new ArgumentReference(parameters[i].ParameterType, i + 1); + } - for (var i = 0; i < args.Length; ++i) - { - arguments[i] = new ArgumentReference(args[i].ParameterType, i + 1); + return arguments; } - return arguments; + public Type[] GetTypes() + { + if (parameters.Length == 0) return []; + + var types = new Type[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + types[i] = parameters[i].ParameterType; + } + return types; + } } - public static Type[] GetTypes(ParameterInfo[] parameters) + extension(Type[] parameterTypes) { - var types = new Type[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) + public ArgumentReference[] ConvertToArgumentReferences() { - types[i] = parameters[i].ParameterType; + if (parameterTypes.Length == 0) return []; + + var arguments = new ArgumentReference[parameterTypes.Length]; + + for (var i = 0; i < parameterTypes.Length; ++i) + { + arguments[i] = new ArgumentReference(parameterTypes[i], i + 1); + } + + return arguments; } - return types; } - public static Type[] InitializeAndConvert(ArgumentReference[] args) + public static Type[] InitializeAndConvert(ArgumentReference[] arguments) { - var types = new Type[args.Length]; + if (arguments.Length == 0) return []; + + var types = new Type[arguments.Length]; - for (var i = 0; i < args.Length; ++i) + for (var i = 0; i < arguments.Length; ++i) { - args[i].Position = i + 1; - types[i] = args[i].Type; + arguments[i].Position = i + 1; + types[i] = arguments[i].Type; } return types; } - - public static void InitializeArgumentsByPosition(ArgumentReference[] args, bool isStatic) - { - var offset = isStatic ? 0 : 1; - for (var i = 0; i < args.Length; ++i) - { - args[i].Position = i + offset; - } - } } } \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs index b77c62b2d..f48d1e27c 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -64,12 +64,12 @@ internal MethodEmitter(ClassEmitter owner, string name, var returnType = methodToUseAsATemplate.ReturnType; var baseMethodParameters = methodToUseAsATemplate.GetParameters(); - var parameters = ArgumentsUtil.GetTypes(baseMethodParameters); + var parameterTypes = baseMethodParameters.GetTypes(); genericTypeParams = GenericUtil.CopyGenericArguments(methodToUseAsATemplate, builder); - SetParameters(parameters); + SetParameters(parameterTypes); SetReturnType(returnType); - SetSignature(returnType, methodToUseAsATemplate.ReturnParameter, parameters, baseMethodParameters); + SetSignature(returnType, methodToUseAsATemplate.ReturnParameter, parameterTypes, baseMethodParameters); DefineParameters(baseMethodParameters); } @@ -115,8 +115,9 @@ public void DefineCustomAttribute(CustomAttributeBuilder attribute) public void SetParameters(Type[] paramTypes) { builder.SetParameters(paramTypes); - arguments = ArgumentsUtil.ConvertToArgumentReference(paramTypes); - ArgumentsUtil.InitializeArgumentsByPosition(arguments, MethodBuilder.IsStatic); + + Debug.Assert(MethodBuilder.IsStatic == false, "The following call to `ConvertToArgumentReferences` assumes the presence of a `this` parameter when assigning parameter positions."); + arguments = paramTypes.ConvertToArgumentReferences(); } public virtual void Generate() diff --git a/src/Castle.Core/DynamicProxy/Generators/ForwardingMethodGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/ForwardingMethodGenerator.cs index 769b0cffe..fe2e0bde1 100644 --- a/src/Castle.Core/DynamicProxy/Generators/ForwardingMethodGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/ForwardingMethodGenerator.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -33,7 +33,7 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C INamingScope namingScope) { var target = getTarget(@class, MethodToOverride); - var arguments = ArgumentsUtil.ConvertToArgumentReferenceExpression(MethodToOverride.GetParameters()); + var arguments = MethodToOverride.GetParameters().ConvertToArgumentReferences(); emitter.CodeBuilder.AddStatement(new ReturnStatement( new MethodInvocationExpression( diff --git a/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs b/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs index 9ece9992b..a9847d664 100644 --- a/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs +++ b/src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -14,9 +14,7 @@ namespace Castle.DynamicProxy.Generators { - using System.Linq; using System.Reflection; - using System.Runtime.InteropServices; using Castle.DynamicProxy.Generators.Emitters; using Castle.DynamicProxy.Generators.Emitters.SimpleAST; @@ -35,7 +33,7 @@ public static void CopyOutAndRefParameters(Reference[] dereferencedArguments, Lo for (var i = 0; i < parameters.Length; i++) { - if (IsByRef(parameters[i]) && !IsReadOnly(parameters[i])) + if (parameters[i].IsByRef && !parameters[i].IsReadOnly) { if (arguments == null) { @@ -70,65 +68,6 @@ public static void CopyOutAndRefParameters(Reference[] dereferencedArguments, Lo } } } - - bool IsByRef(ParameterInfo parameter) - { - return parameter.ParameterType.IsByRef; - } - - bool IsReadOnly(ParameterInfo parameter) - { - // C# `in` parameters are also by-ref, but meant to be read-only. - // The section "Metadata representation of in parameters" on the following page - // defines how such parameters are marked: - // - // https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.2/readonly-ref.md - // - // This poses three problems for detecting them: - // - // * The C# Roslyn compiler marks `in` parameters with an `[in]` IL modifier, - // but this isn't specified, nor is it used uniquely for `in` params. - // - // * `System.Runtime.CompilerServices.IsReadOnlyAttribute` is not defined on all - // .NET platforms, so the compiler sometimes recreates that type in the same - // assembly that contains the method having an `in` parameter. In other words, - // it's an attribute one must check for by name (which is slow, as it implies - // use of a `GetCustomAttributes` enumeration instead of a faster `IsDefined`). - // - // * A required custom modifier `System.Runtime.InteropServices.InAttribute` - // is always present in those cases relevant for DynamicProxy (proxyable methods), - // but not all targeted platforms support reading custom modifiers. Also, - // support for cmods is generally flaky (at this time of writing, mid-2018). - // - // The above points inform the following detection logic: First, we rely on an IL - // `[in]` modifier being present. This is a "fast guard" against non-`in` parameters: - if ((parameter.Attributes & (ParameterAttributes.In | ParameterAttributes.Out)) != ParameterAttributes.In) - { - return false; - } - - // This check allows to make the detection logic more robust on the platforms which support custom modifiers. - // The robustness is achieved by the fact, that usually the `IsReadOnlyAttribute` emitted by the compiler is internal to the assembly. - // Therefore, if clients use Reflection.Emit to create "a copy" of the methods with read-only members, they cannot re-use the existing attribute. - // Instead, they are forced to emit their own `IsReadOnlyAttribute` to mark some argument as immutable. - // The `InAttribute` type OTOH was always available in BCL. Therefore, it's much easier to copy the modreq and be recognized by Castle. - // - // If check fails, resort to the IsReadOnlyAttribute check. - // Check for the required modifiers first, as it's faster. - if (parameter.GetRequiredCustomModifiers().Any(x => x == typeof(InAttribute))) - { - return true; - } - - // The comparison by name is intentional; any assembly could define that attribute. - // See explanation in comment above. - if (parameter.GetCustomAttributes(false).Any(x => x.GetType().FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute")) - { - return true; - } - - return false; - } } private static ConvertExpression Argument(int i, LocalReference invocationArgs, Reference[] arguments) diff --git a/src/Castle.Core/DynamicProxy/Generators/OptionallyForwardingMethodGenerator.cs b/src/Castle.Core/DynamicProxy/Generators/OptionallyForwardingMethodGenerator.cs index 86359be05..b1dedf36e 100644 --- a/src/Castle.Core/DynamicProxy/Generators/OptionallyForwardingMethodGenerator.cs +++ b/src/Castle.Core/DynamicProxy/Generators/OptionallyForwardingMethodGenerator.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// 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. @@ -50,7 +50,7 @@ protected override MethodEmitter BuildProxiedMethodBody(MethodEmitter emitter, C private IStatement IfNotNull(IExpression target) { var statements = new BlockStatement(); - var arguments = ArgumentsUtil.ConvertToArgumentReferenceExpression(MethodToOverride.GetParameters()); + var arguments = MethodToOverride.GetParameters().ConvertToArgumentReferences(); statements.AddStatement(new ReturnStatement( new MethodInvocationExpression(