Skip to content

Commit 1266c6c

Browse files
committed
Synthesize <Clone>$ method in record class proxies
... instead of emitting interception code for it.
1 parent 1bbeba3 commit 1266c6c

3 files changed

Lines changed: 122 additions & 11 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2004-2025 Castle Project - http://www.castleproject.org/
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Castle.DynamicProxy.Contributors
16+
{
17+
using System;
18+
using System.Reflection;
19+
20+
using Castle.DynamicProxy.Generators;
21+
using Castle.DynamicProxy.Generators.Emitters;
22+
using Castle.DynamicProxy.Generators.Emitters.SimpleAST;
23+
24+
internal sealed class RecordCloningContributor : ITypeContributor
25+
{
26+
private readonly Type targetType;
27+
private MetaMethod cloneMethod;
28+
29+
public RecordCloningContributor(Type targetType)
30+
{
31+
this.targetType = targetType;
32+
}
33+
34+
public void CollectElementsToProxy(IProxyGenerationHook hook, MetaType model)
35+
{
36+
var cloneMethodInfo = targetType.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance);
37+
if (cloneMethodInfo == null)
38+
{
39+
return;
40+
}
41+
42+
cloneMethod = model.FindMethod(cloneMethodInfo);
43+
if (cloneMethod != null)
44+
{
45+
// The target contributor may have chosen to generate interception code for this method.
46+
// We override that decision here. This effectively renders `<Clone>$` uninterceptable,
47+
// in favor of some default behavior provided by DynamicProxy. This may be a bad idea.
48+
cloneMethod.Ignore = true;
49+
}
50+
}
51+
52+
public void Generate(ClassEmitter @class)
53+
{
54+
if (cloneMethod == null)
55+
{
56+
return;
57+
}
58+
59+
ImplementCopyConstructor(@class, out var copyCtor);
60+
ImplementCloneMethod(@class, copyCtor);
61+
}
62+
63+
private void ImplementCopyConstructor(ClassEmitter @class, out ConstructorInfo copyCtor)
64+
{
65+
var other = new ArgumentReference(@class.TypeBuilder);
66+
var copyCtorEmitter = @class.CreateConstructor(other);
67+
var baseCopyCtor = targetType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [ targetType ], null);
68+
69+
copyCtorEmitter.CodeBuilder.AddStatement(
70+
new ConstructorInvocationStatement(
71+
baseCopyCtor,
72+
other));
73+
74+
foreach (var field in @class.GetAllFields())
75+
{
76+
if (field.Reference.IsStatic) continue;
77+
78+
copyCtorEmitter.CodeBuilder.AddStatement(
79+
new AssignStatement(
80+
field,
81+
new FieldReference(
82+
field.Reference,
83+
other)));
84+
}
85+
86+
copyCtorEmitter.CodeBuilder.AddStatement(
87+
new ReturnStatement());
88+
89+
copyCtor = copyCtorEmitter.ConstructorBuilder;
90+
}
91+
92+
private void ImplementCloneMethod(ClassEmitter @class, ConstructorInfo copyCtor)
93+
{
94+
var cloneMethod = @class.CreateMethod(
95+
name: this.cloneMethod.Method.Name,
96+
attrs: (this.cloneMethod.Method.Attributes & MethodAttributes.MemberAccessMask) | MethodAttributes.ReuseSlot | MethodAttributes.Virtual,
97+
returnType: targetType,
98+
argumentTypes: Type.EmptyTypes);
99+
100+
cloneMethod.CodeBuilder.AddStatement(
101+
new ReturnStatement(
102+
new NewInstanceExpression(
103+
copyCtor,
104+
ThisExpression.Instance)));
105+
}
106+
}
107+
}

src/Castle.Core/DynamicProxy/Generators/BaseClassProxyGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ private IEnumerable<Type> GetTypeImplementerMapping(out IEnumerable<ITypeContrib
192192
}
193193
#endif
194194

195+
contributorsList.Add(new RecordCloningContributor(targetType));
196+
195197
var proxyTargetAccessorContributor = GetProxyTargetAccessorContributor();
196198
contributorsList.Add(proxyTargetAccessorContributor);
197199
try

src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/FieldReference.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,28 @@ internal class FieldReference : Reference
2525
{
2626
private readonly FieldInfo field;
2727
private readonly FieldBuilder? fieldBuilder;
28-
private readonly bool isStatic;
28+
private readonly IExpression? instance;
2929

30-
public FieldReference(FieldInfo field)
30+
public FieldReference(FieldInfo field, IExpression? instance = null)
3131
: base(field.FieldType)
3232
{
3333
this.field = field;
34+
this.instance = instance ?? ThisExpression.Instance;
3435
if ((field.Attributes & FieldAttributes.Static) != 0)
3536
{
36-
isStatic = true;
37+
this.instance = null;
3738
}
3839
}
3940

40-
public FieldReference(FieldBuilder fieldBuilder)
41+
public FieldReference(FieldBuilder fieldBuilder, IExpression? instance = null)
4142
: base(fieldBuilder.FieldType)
4243
{
4344
this.fieldBuilder = fieldBuilder;
4445
field = fieldBuilder;
46+
this.instance = instance ?? ThisExpression.Instance;
4547
if ((fieldBuilder.Attributes & FieldAttributes.Static) != 0)
4648
{
47-
isStatic = true;
49+
this.instance = null;
4850
}
4951
}
5052

@@ -60,40 +62,40 @@ public FieldInfo Reference
6062

6163
public override void EmitAddress(ILGenerator gen)
6264
{
63-
if (isStatic)
65+
if (instance == null)
6466
{
6567
gen.Emit(OpCodes.Ldsflda, Reference);
6668
}
6769
else
6870
{
69-
ThisExpression.Instance.Emit(gen);
71+
instance.Emit(gen);
7072
gen.Emit(OpCodes.Ldflda, Reference);
7173
}
7274
}
7375

7476
public override void Emit(ILGenerator gen)
7577
{
76-
if (isStatic)
78+
if (instance == null)
7779
{
7880
gen.Emit(OpCodes.Ldsfld, Reference);
7981
}
8082
else
8183
{
82-
ThisExpression.Instance.Emit(gen);
84+
instance.Emit(gen);
8385
gen.Emit(OpCodes.Ldfld, Reference);
8486
}
8587
}
8688

8789
public override void EmitStore(IExpression value, ILGenerator gen)
8890
{
89-
if (isStatic)
91+
if (instance == null)
9092
{
9193
value.Emit(gen);
9294
gen.Emit(OpCodes.Stsfld, Reference);
9395
}
9496
else
9597
{
96-
ThisExpression.Instance.Emit(gen);
98+
instance.Emit(gen);
9799
value.Emit(gen);
98100
gen.Emit(OpCodes.Stfld, Reference);
99101
}

0 commit comments

Comments
 (0)