Skip to content

Commit d4e05fb

Browse files
committed
[dotnet-linker] Use [DynamicDependency] attributes instead of manual marking when preserving smart enum methods.
This makes it easier to move this code out of a custom linker step in the future. Contributes towards #17693.
1 parent 1fc82b6 commit d4e05fb

File tree

5 files changed

+279
-119
lines changed

5 files changed

+279
-119
lines changed

dotnet/targets/Xamarin.Shared.Sdk.targets

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@
548548
<PropertyGroup>
549549
<_UseDynamicDependenciesInsteadOfMarking Condition="'$(_UseDynamicDependenciesInsteadOfMarking)' == ''">true</_UseDynamicDependenciesInsteadOfMarking>
550550
<_UseDynamicDependenciesForProtocolPreservation Condition="'$(_UseDynamicDependenciesForProtocolPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForProtocolPreservation>
551+
<_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForSmartEnumPreservation>
551552
</PropertyGroup>
552553

553554
<PropertyGroup>
@@ -750,6 +751,7 @@
750751
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.CoreTypeMapStep" />
751752
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.ProcessExportedFields" />
752753
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.PreserveProtocolsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' == 'true'" />
754+
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" />
753755
<!-- The final decision to remove/keep the dynamic registrar must be done before the linking step -->
754756
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" />
755757
<!-- TODO: these steps should probably run after mark. -->
@@ -765,7 +767,7 @@
765767
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' != 'true'" Type="Xamarin.Linker.MarkIProtocolHandler" />
766768
<!-- MarkDispatcher substeps will run for all marked assemblies. -->
767769
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" />
768-
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />
770+
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />
769771

770772
<!--
771773
pre-sweep custom steps

tools/dotnet-linker/AppBundleRewriter.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,17 @@ public MethodReference Dictionary2_Add {
467467
}
468468
}
469469

470+
public MethodReference DynamicDependencyAttribute_ctor__String {
471+
get {
472+
return GetMethodReference (CorlibAssembly,
473+
System_Diagnostics_CodeAnalysis_DynamicDependencyAttribute,
474+
".ctor",
475+
".ctor(String)",
476+
isStatic: false,
477+
System_String);
478+
}
479+
}
480+
470481
public MethodReference DynamicDependencyAttribute_ctor__String_Type {
471482
get {
472483
return GetMethodReference (CorlibAssembly,
@@ -1246,6 +1257,33 @@ public void ClearCurrentAssembly ()
12461257
field_map.Clear ();
12471258
}
12481259

1260+
// We only need to add dependency attributes if the target dependency is in a trimmed assembly,
1261+
// otherwise the target dependency won't be trimmed away.
1262+
bool IsAssemblyTrimmed (IMemberDefinition member)
1263+
{
1264+
var assembly = member is TypeDefinition td ? td.Module.Assembly : member.DeclaringType.Module.Assembly;
1265+
var action = configuration.Context.Annotations.GetAction (assembly);
1266+
return action == AssemblyAction.Link;
1267+
}
1268+
1269+
public bool AddDynamicDependencyAttribute (MethodDefinition addToMethod, MethodDefinition dependsOn)
1270+
{
1271+
if (!IsAssemblyTrimmed (dependsOn))
1272+
return false;
1273+
1274+
if (addToMethod.DeclaringType == dependsOn.DeclaringType) {
1275+
var attribute = new CustomAttribute (DynamicDependencyAttribute_ctor__String);
1276+
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_String, DocumentationComments.GetSignature (dependsOn)));
1277+
return AddAttributeOnlyOnce (addToMethod, attribute);
1278+
} else if (addToMethod.DeclaringType.Module == dependsOn.DeclaringType.Module) {
1279+
var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn), dependsOn.DeclaringType);
1280+
return AddAttributeOnlyOnce (addToMethod, attribute);
1281+
} else {
1282+
var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn), dependsOn.DeclaringType, dependsOn.DeclaringType.Module.Assembly);
1283+
return AddAttributeOnlyOnce (addToMethod, attribute);
1284+
}
1285+
}
1286+
12491287
public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, TypeDefinition type)
12501288
{
12511289
if (type.HasGenericParameters)
@@ -1307,6 +1345,9 @@ public bool AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onT
13071345
/// <returns>Whether an attribute was added or not.</returns>
13081346
public bool AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onType, TypeDefinition forType)
13091347
{
1348+
if (!IsAssemblyTrimmed (forType))
1349+
return false;
1350+
13101351
var placeholderName = "__linker_preserve__";
13111352
FieldDefinition? placeholderMember = null;
13121353
if (forType.HasFields)

tools/dotnet-linker/PreserveProtocolsStep.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ protected override bool IsActiveFor (AssemblyDefinition assembly)
3939
if (DerivedLinkContext.App.Registrar != Bundler.RegistrarMode.Dynamic)
4040
return false;
4141

42+
// We only care about assemblies that are being linked.
4243
if (Annotations.GetAction (assembly) != AssemblyAction.Link)
4344
return false;
4445

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2017 Xamarin Inc.
2+
3+
using System.Linq;
4+
5+
using Mono.Cecil;
6+
using Mono.Linker;
7+
using Mono.Linker.Steps;
8+
using Mono.Tuner;
9+
10+
using Xamarin.Bundler;
11+
using Xamarin.Tuner;
12+
13+
#nullable enable
14+
15+
namespace Xamarin.Linker.Steps {
16+
public class PreserveSmartEnumConversionsStep : AssemblyModifierStep {
17+
protected override string Name { get; } = "Smart Enum Conversion Preserver";
18+
protected override int ErrorCode { get; } = 2200;
19+
20+
PreserveSmartEnumConversion? preserver;
21+
PreserveSmartEnumConversion Preserver {
22+
get {
23+
if (preserver is null)
24+
preserver = new PreserveSmartEnumConversion (DerivedLinkContext, Preserve);
25+
return preserver;
26+
}
27+
}
28+
29+
protected override bool IsActiveFor (AssemblyDefinition assembly)
30+
{
31+
// We have to process assemblies that aren't linked, because type A from the unlinked assembly X
32+
// might reference the smart enum B from the linked assembly Y,
33+
// and we need to make sure that smart enum B's conversion methods aren't trimmed away - which
34+
// means adding dynamic dependency attributes to the methods in the unlinked assembly X,
35+
// which means we need to process the unlinked assembly X.
36+
37+
// Hot Reload: we can't modify user assemblies when Hot Reload is enabled (otherwise Hot Reload won't work),
38+
// so we'll have to come up with a different solution (emit xml definition instead maybe?)
39+
40+
// Unless an assembly is or references our platform assembly, then it won't have anything we need to preserve
41+
if (!Configuration.Profile.IsOrReferencesProductAssembly (assembly))
42+
return false;
43+
44+
return true;
45+
}
46+
47+
bool Preserve (Tuple<MethodDefinition, MethodDefinition> pair, bool alreadyProcessed, params MethodDefinition? [] conditions)
48+
{
49+
var conds = conditions.Where (v => v is not null).Cast<MethodDefinition> ().ToArray ();
50+
if (conds.Length == 0)
51+
return false;
52+
53+
var modified = false;
54+
foreach (var condition in conds) {
55+
modified |= abr.AddDynamicDependencyAttribute (condition, pair.Item1);
56+
modified |= abr.AddDynamicDependencyAttribute (condition, pair.Item2);
57+
}
58+
59+
return modified;
60+
}
61+
62+
protected override bool ProcessType (TypeDefinition type)
63+
{
64+
var modified = false;
65+
66+
if (type.HasNestedTypes) {
67+
foreach (var nested in type.NestedTypes)
68+
modified |= ProcessType (nested);
69+
}
70+
71+
if (!type.HasMethods)
72+
return modified;
73+
74+
foreach (var method in type.Methods)
75+
modified |= ProcessMethod (method);
76+
77+
return modified;
78+
}
79+
80+
bool ProcessMethod (MethodDefinition method)
81+
{
82+
static bool IsPropertyMethod (MethodDefinition method)
83+
{
84+
return method.IsGetter || method.IsSetter;
85+
}
86+
87+
var modified = false;
88+
modified |= Preserver.ProcessAttributeProvider (method, method);
89+
modified |= Preserver.ProcessAttributeProvider (method.MethodReturnType, method);
90+
91+
if (method.HasParameters) {
92+
foreach (var p in method.Parameters)
93+
modified |= Preserver.ProcessAttributeProvider (p, method);
94+
}
95+
if (IsPropertyMethod (method)) {
96+
foreach (var property in method.DeclaringType.Properties)
97+
if (property.GetMethod == method || property.SetMethod == method) {
98+
modified |= Preserver.ProcessAttributeProvider (property, property.GetMethod, property.SetMethod);
99+
break;
100+
}
101+
}
102+
return modified;
103+
}
104+
}
105+
106+
class PreserveSmartEnumConversion {
107+
Dictionary<TypeDefinition, Tuple<MethodDefinition, MethodDefinition>> cache = new ();
108+
109+
public DerivedLinkContext LinkContext { get; private set; }
110+
111+
public Func<Tuple<MethodDefinition, MethodDefinition>, bool, MethodDefinition? [], bool> preserve { get; set; }
112+
113+
public PreserveSmartEnumConversion (DerivedLinkContext linkContext, Func<Tuple<MethodDefinition, MethodDefinition>, bool, MethodDefinition? [], bool> preserve)
114+
{
115+
LinkContext = linkContext;
116+
this.preserve = preserve;
117+
}
118+
119+
bool Preserve (Tuple<MethodDefinition, MethodDefinition> pair, bool alreadyProcessed, params MethodDefinition? [] conditions)
120+
{
121+
return preserve (pair, alreadyProcessed, conditions);
122+
}
123+
124+
public bool ProcessAttributeProvider (ICustomAttributeProvider provider, params MethodDefinition [] conditions)
125+
{
126+
var modified = false;
127+
128+
if (provider?.HasCustomAttributes != true)
129+
return modified;
130+
131+
foreach (var ca in provider.CustomAttributes) {
132+
var tr = ca.Constructor.DeclaringType;
133+
134+
if (!tr.Is ("ObjCRuntime", "BindAsAttribute"))
135+
continue;
136+
137+
if (ca.ConstructorArguments.Count != 1) {
138+
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 4124, provider, Errors.MT4124_E, provider.AsString (), ca.ConstructorArguments.Count));
139+
continue;
140+
}
141+
142+
var managedType = ca.ConstructorArguments [0].Value as TypeReference;
143+
var managedEnumType = managedType?.GetElementType ().Resolve ();
144+
if (managedEnumType is null) {
145+
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 4124, provider, Errors.MT4124_H, provider.AsString (), managedType?.FullName ?? "(null)"));
146+
continue;
147+
}
148+
149+
// We only care about enums, BindAs attributes can be used for other types too.
150+
if (!managedEnumType.IsEnum)
151+
continue;
152+
153+
if (cache.TryGetValue (managedEnumType, out var pair)) {
154+
// The pair was already marked if it was cached.
155+
Preserve (pair, true, conditions);
156+
continue;
157+
}
158+
159+
// Find the Extension type
160+
TypeDefinition? extensionType = null;
161+
var extensionName = managedEnumType.Name + "Extensions";
162+
foreach (var type in managedEnumType.Module.Types) {
163+
if (type.Namespace != managedEnumType.Namespace)
164+
continue;
165+
if (type.Name != extensionName)
166+
continue;
167+
extensionType = type;
168+
break;
169+
}
170+
if (extensionType is null) {
171+
Driver.Log (1, $"Could not find a smart extension type for the enum {managedEnumType.FullName} (due to BindAs attribute on {provider.AsString ()}): most likely this is because the enum isn't a smart enum.");
172+
continue;
173+
}
174+
175+
// Find the GetConstant/GetValue methods
176+
MethodDefinition? getConstant = null;
177+
MethodDefinition? getValue = null;
178+
179+
foreach (var method in extensionType.Methods) {
180+
if (!method.IsStatic)
181+
continue;
182+
if (!method.HasParameters || method.Parameters.Count != 1)
183+
continue;
184+
if (method.Name == "GetConstant") {
185+
if (!method.ReturnType.Is ("Foundation", "NSString"))
186+
continue;
187+
if (method.Parameters [0].ParameterType != managedEnumType)
188+
continue;
189+
getConstant = method;
190+
} else if (method.Name == "GetValue") {
191+
if (!method.Parameters [0].ParameterType.Is ("Foundation", "NSString"))
192+
continue;
193+
if (method.ReturnType != managedEnumType)
194+
continue;
195+
getValue = method;
196+
}
197+
}
198+
199+
if (getConstant is null) {
200+
Driver.Log (1, $"Could not find the GetConstant method on the supposedly smart extension type {extensionType.FullName} for the enum {managedEnumType.FullName} (due to BindAs attribute on {provider.AsString ()}): most likely this is because the enum isn't a smart enum.");
201+
continue;
202+
}
203+
204+
if (getValue is null) {
205+
Driver.Log (1, $"Could not find the GetValue method on the supposedly smart extension type {extensionType.FullName} for the enum {managedEnumType.FullName} (due to BindAs attribute on {provider.AsString ()}): most likely this is because the enum isn't a smart enum.");
206+
continue;
207+
}
208+
209+
pair = new Tuple<MethodDefinition, MethodDefinition> (getConstant, getValue);
210+
cache.Add (managedEnumType, pair);
211+
modified |= Preserve (pair, false, conditions);
212+
}
213+
214+
return modified;
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)