Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/Castle.Core.Tests/DynamicProxy.Tests/ArgumentsUtilTestCase.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
14 changes: 1 addition & 13 deletions src/Castle.Core.Tests/DynamicProxy.Tests/ClassEmitterTestCase.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -41,18 +41,6 @@ public void AutomaticDefaultConstructorGenerationWithClosedGenericType()
Activator.CreateInstance(t);
}

[Test]
public void StaticMethodArguments()
{
ClassEmitter emitter = new ClassEmitter(generator.ProxyBuilder.ModuleScope, "Foo", typeof (List<object>),
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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
142 changes: 99 additions & 43 deletions src/Castle.Core/DynamicProxy/Generators/Emitters/ArgumentsUtil.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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(
Expand Down
Loading