From 9d8fff11fc3907aac55d54d7797d3ad674369b41 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 29 Jun 2023 13:53:05 +0200 Subject: [PATCH 1/2] Add MemberClonerListenerList and AssignTokensClonerListener. --- .../Cloning/AssignTokensClonerListener.cs | 33 ++++++++++++ .../Cloning/MemberCloner.Fields.cs | 4 +- .../Cloning/MemberCloner.Methods.cs | 4 +- .../Cloning/MemberCloner.Semantics.cs | 8 +-- .../Cloning/MemberCloner.cs | 34 ++++++++++-- .../Cloning/MemberClonerListenerList.cs | 52 +++++++++++++++++++ .../Cloning/MetadataClonerTest.cs | 25 +++++++-- 7 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs create mode 100644 src/AsmResolver.DotNet/Cloning/MemberClonerListenerList.cs diff --git a/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs b/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs new file mode 100644 index 000000000..dd8ae4a90 --- /dev/null +++ b/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs @@ -0,0 +1,33 @@ +namespace AsmResolver.DotNet.Cloning +{ + /// + /// Provides an implementation of a that pre-emptively assigns new metadata + /// tokens to the cloned metadata members using the target module's . + /// + public class AssignTokensClonerListener : MemberClonerListener + { + /// + /// Creates a new instance of the token allocator listener. + /// + /// The module that will contain the cloned members. + public AssignTokensClonerListener(ModuleDefinition targetModule) + { + TargetModule = targetModule; + } + + /// + /// Gets the module that will contain the cloned members. + /// + public ModuleDefinition TargetModule + { + get; + } + + /// + public override void OnClonedMember(IMemberDefinition original, IMemberDefinition cloned) + { + TargetModule.TokenAllocator.AssignNextAvailableToken((MetadataMember) cloned); + base.OnClonedMember(original, cloned); + } + } +} diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.Fields.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.Fields.cs index 78df400d3..d96da9443 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.Fields.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.Fields.cs @@ -42,8 +42,8 @@ private void DeepCopyFields(MemberCloneContext context) { DeepCopyField(context, field); var clonedMember = (FieldDefinition)context.ClonedMembers[field]; - _clonerListener.OnClonedMember(field, clonedMember); - _clonerListener.OnClonedField(field, clonedMember); + _listeners.OnClonedMember(field, clonedMember); + _listeners.OnClonedField(field, clonedMember); } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.Methods.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.Methods.cs index af6085a14..205ccf9f5 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.Methods.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.Methods.cs @@ -57,8 +57,8 @@ private void DeepCopyMethods(MemberCloneContext context) { DeepCopyMethod(context, method); var clonedMember = (MethodDefinition)context.ClonedMembers[method]; - _clonerListener.OnClonedMember(method, clonedMember); - _clonerListener.OnClonedMethod(method, clonedMember); + _listeners.OnClonedMember(method, clonedMember); + _listeners.OnClonedMethod(method, clonedMember); } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.Semantics.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.Semantics.cs index 7d3e5ea6b..53140cfca 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.Semantics.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.Semantics.cs @@ -18,8 +18,8 @@ private void DeepCopyProperties(MemberCloneContext context) declaringType.Properties.Add(clonedProperty); } var clonedMember = clonedProperty; - _clonerListener.OnClonedMember(property, clonedMember); - _clonerListener.OnClonedProperty(property, clonedMember); + _listeners.OnClonedMember(property, clonedMember); + _listeners.OnClonedProperty(property, clonedMember); } } @@ -55,8 +55,8 @@ private void DeepCopyEvents(MemberCloneContext context) declaringType.Events.Add(clonedEvent); } var clonedMember = clonedEvent; - _clonerListener.OnClonedMember(@event, clonedMember); - _clonerListener.OnClonedEvent(@event, clonedMember); + _listeners.OnClonedMember(@event, clonedMember); + _listeners.OnClonedEvent(@event, clonedMember); } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs index 64acfe3a5..a6e4bde45 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs @@ -17,7 +17,7 @@ namespace AsmResolver.DotNet.Cloning /// public partial class MemberCloner { - private readonly IMemberClonerListener _clonerListener; + private readonly MemberClonerListenerList _listeners; private readonly Func? _importerFactory; private readonly ModuleDefinition _targetModule; @@ -32,7 +32,7 @@ public partial class MemberCloner /// /// The target module to copy the members into. public MemberCloner(ModuleDefinition targetModule) - : this(targetModule, (original, cloned) => { }) + : this(targetModule, null, null) { } @@ -79,7 +79,9 @@ public MemberCloner(ModuleDefinition targetModule, IMemberClonerListener listene { _targetModule = targetModule ?? throw new ArgumentNullException(nameof(targetModule)); _importerFactory = importerFactory; - _clonerListener = clonerListener ?? CallbackClonerListener.EmptyInstance; + _listeners = new MemberClonerListenerList(); + if (clonerListener is not null) + _listeners.Add(clonerListener); } /// @@ -262,6 +264,28 @@ public MemberCloner Include(EventDefinition @event) return this; } + /// + /// Adds a member cloner listener to the cloner. + /// + /// The listener to add. + /// The metadata cloner that the listener is added to. + public MemberCloner WithListener(Action listener) + { + _listeners.Add(new CallbackClonerListener(listener)); + return this; + } + + /// + /// Adds a member cloner listener to the cloner. + /// + /// The listener to add. + /// The metadata cloner that the listener is added to. + public MemberCloner WithListener(IMemberClonerListener listener) + { + _listeners.Add(listener); + return this; + } + /// /// Clones all included members. /// @@ -313,8 +337,8 @@ private void DeepCopyTypes(MemberCloneContext context) { DeepCopyType(context, type); var clonedMember = (TypeDefinition)context.ClonedMembers[type]; - _clonerListener.OnClonedMember(type, clonedMember); - _clonerListener.OnClonedType(type, clonedMember); + _listeners.OnClonedMember(type, clonedMember); + _listeners.OnClonedType(type, clonedMember); } } diff --git a/src/AsmResolver.DotNet/Cloning/MemberClonerListenerList.cs b/src/AsmResolver.DotNet/Cloning/MemberClonerListenerList.cs new file mode 100644 index 000000000..6385b78a1 --- /dev/null +++ b/src/AsmResolver.DotNet/Cloning/MemberClonerListenerList.cs @@ -0,0 +1,52 @@ +using System.Collections.ObjectModel; + +namespace AsmResolver.DotNet.Cloning +{ + /// + /// Wraps a list of s into a single instance of . + /// + public class MemberClonerListenerList : Collection, IMemberClonerListener + { + /// + public void OnClonedMember(IMemberDefinition original, IMemberDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedMember(original, cloned); + } + + /// + public void OnClonedType(TypeDefinition original, TypeDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedType(original, cloned); + } + + /// + public void OnClonedMethod(MethodDefinition original, MethodDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedMethod(original, cloned); + } + + /// + public void OnClonedField(FieldDefinition original, FieldDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedField(original, cloned); + } + + /// + public void OnClonedProperty(PropertyDefinition original, PropertyDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedProperty(original, cloned); + } + + /// + public void OnClonedEvent(EventDefinition original, EventDefinition cloned) + { + for (int i = 0; i < Items.Count; i++) + Items[i].OnClonedEvent(original, cloned); + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs index 050c8991a..34a29847b 100644 --- a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs +++ b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs @@ -76,7 +76,7 @@ private static MethodDefinition CloneMethod(MethodBase methodBase, out MethodDef return clonedMethod; } - private static FieldDefinition CloneIntializerField(FieldInfo field, out FieldDefinition originalFieldDef) + private static FieldDefinition CloneInitializerField(FieldInfo field, out FieldDefinition originalFieldDef) { var sourceModule = ModuleDefinition.FromFile(field.Module.Assembly.Location); originalFieldDef = (FieldDefinition) sourceModule.LookupMember(field.MetadataToken); @@ -279,10 +279,10 @@ public void CloneConstant() public void CloneFieldRva() { var clonedInitializerField = - CloneIntializerField(typeof(InitialValues).GetField(nameof(InitialValues.ByteArray)), out var field); + CloneInitializerField(typeof(InitialValues).GetField(nameof(InitialValues.ByteArray)), out var field); - var originalData = ((IReadableSegment) field.FieldRva).ToArray(); - var newData = ((IReadableSegment) clonedInitializerField.FieldRva).ToArray(); + var originalData = ((IReadableSegment) field.FieldRva!).ToArray(); + var newData = ((IReadableSegment) clonedInitializerField.FieldRva!).ToArray(); Assert.Equal(originalData, newData); } @@ -391,5 +391,22 @@ public void CloneAndInject() Assert.All(result.ClonedTopLevelTypes, t => Assert.Contains(t, targetModule.TopLevelTypes)); } + [Fact] + public void CloneAndInjectAndAssignToken() + { + var sourceModule = ModuleDefinition.FromFile(typeof(Miscellaneous).Assembly.Location); + var targetModule = PrepareTempModule(); + + var type = sourceModule.TopLevelTypes.First(t => t.Name == nameof(Miscellaneous)); + + var result = new MemberCloner(targetModule) + .Include(type) + .WithListener(new InjectTypeClonerListener(targetModule)) + .WithListener(new AssignTokensClonerListener(targetModule)) + .Clone(); + + Assert.All(result.ClonedTopLevelTypes, t => Assert.Contains(t, targetModule.TopLevelTypes)); + Assert.All(result.ClonedMembers, m => Assert.NotEqual(0u, ((IMetadataMember) m).MetadataToken.Rid)); + } } } From 49e2f3c7c7e30d6353d59f70356e955147adf845 Mon Sep 17 00:00:00 2001 From: Washi Date: Thu, 29 Jun 2023 14:16:18 +0200 Subject: [PATCH 2/2] Rename WithListener -> AddListener. Update docs. --- docs/guides/dotnet/cloning.md | 56 +++++++++++++++---- .../Cloning/AssignTokensClonerListener.cs | 2 +- .../Cloning/MemberCloner.cs | 4 +- .../Cloning/MetadataClonerTest.cs | 4 +- 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/docs/guides/dotnet/cloning.md b/docs/guides/dotnet/cloning.md index 913073022..37b54fd21 100644 --- a/docs/guides/dotnet/cloning.md +++ b/docs/guides/dotnet/cloning.md @@ -103,9 +103,8 @@ cloner.Include(rectangleType); cloner.Include(vectorType); ``` -`Include` returns the same `MemberCloner` instance. It is therefore also -possible to create a long method chain of members to include in the -cloning process. +`Include` returns the same `MemberCloner` instance, allowing for more fluent +syntax: ``` csharp cloner @@ -255,7 +254,24 @@ var cloner = new MemberCloner(destinationModule, (original, cloned) => { }); ``` -## Injecting the cloned members +Multiple listeners can also be chained together using the `AddListener` method: + +```csharp +var cloner = new MemberCloner(destinationModule); +cloner.AddListener(new MyListener1()); +cloner.AddListener(new MyListener2()); +``` + +This can also be used in the fluent syntax form: + +```csharp +var cloner = new MemberCloner(destinationModule) + .AddListener(new MyListener1()) + .AddListener(new MyListener2()); +``` + + +## Injecting cloned members The `Clone` method returns a `MemberCloneResult`, which contains a register of all members cloned by the member cloner. @@ -288,17 +304,37 @@ foreach (var clonedType in clonedTypes) destinationModule.TopLevelTypes.Add(clonedType); ``` -However, since injecting the cloned top level types is a very common -use-case for the cloner, AsmResolver defines the -`InjectTypeClonerListener` class that implements a cloner listener that -injects all top-level types automatically into the destination module. -In such a case, the code can be reduced to the following: +Injecting the cloned top level types is a very common use-case for the cloner. +AsmResolver defines the `InjectTypeClonerListener` class that implements a +cloner listener that injects all top-level types automatically into +the destination module. In such a case, the code can be reduced to the following: ``` csharp -new MemberCloner(destinationModule, new InjectTypeClonerListener(destinationModule)) +new MemberCloner(destinationModule) .Include(rectangleType) .Include(vectorType) + .AddListener(new InjectTypeClonerListener(destinationModule)) .Clone(); // `destinationModule` now contains copies of `rectangleType` and `vectorType`. ``` + +AsmResolver provides another built-in listener `AssignTokensClonerListener` that +also allows for preemptively assigning new metadata tokens automatically, should +this be necessary. + +``` csharp +new MemberCloner(destinationModule) + .Include(rectangleType) + .Include(vectorType) + .AddListener(new InjectTypeClonerListener(destinationModule)) + .AddListener(new AssignTokensClonerListener(destinationModule)) + .Clone(); + +// `destinationModule` now contains copies of `rectangleType` and `vectorType` +// and all its members have been assigned a metadata token preemptively. +``` + +The `AssignTokensClonerListener` uses the target module's `TokenAllocator`. See +[Metadata Token Allocation](token-allocation.md) for more information on how this +class operates. \ No newline at end of file diff --git a/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs b/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs index dd8ae4a90..5a1651641 100644 --- a/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs +++ b/src/AsmResolver.DotNet/Cloning/AssignTokensClonerListener.cs @@ -1,7 +1,7 @@ namespace AsmResolver.DotNet.Cloning { /// - /// Provides an implementation of a that pre-emptively assigns new metadata + /// Provides an implementation of a that preemptively assigns new metadata /// tokens to the cloned metadata members using the target module's . /// public class AssignTokensClonerListener : MemberClonerListener diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs index a6e4bde45..cf9862e78 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs @@ -269,7 +269,7 @@ public MemberCloner Include(EventDefinition @event) /// /// The listener to add. /// The metadata cloner that the listener is added to. - public MemberCloner WithListener(Action listener) + public MemberCloner AddListener(Action listener) { _listeners.Add(new CallbackClonerListener(listener)); return this; @@ -280,7 +280,7 @@ public MemberCloner WithListener(Action li /// /// The listener to add. /// The metadata cloner that the listener is added to. - public MemberCloner WithListener(IMemberClonerListener listener) + public MemberCloner AddListener(IMemberClonerListener listener) { _listeners.Add(listener); return this; diff --git a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs index 34a29847b..88b68064c 100644 --- a/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs +++ b/test/AsmResolver.DotNet.Tests/Cloning/MetadataClonerTest.cs @@ -401,8 +401,8 @@ public void CloneAndInjectAndAssignToken() var result = new MemberCloner(targetModule) .Include(type) - .WithListener(new InjectTypeClonerListener(targetModule)) - .WithListener(new AssignTokensClonerListener(targetModule)) + .AddListener(new InjectTypeClonerListener(targetModule)) + .AddListener(new AssignTokensClonerListener(targetModule)) .Clone(); Assert.All(result.ClonedTopLevelTypes, t => Assert.Contains(t, targetModule.TopLevelTypes));