Skip to content

Commit

Permalink
[NativeAOT] Add support for [Preserve] attributes (#18666)
Browse files Browse the repository at this point in the history
Add partial support for the `[Preserve]` attribute for NativeAOT. This
is done by injecting an equivalent `[DynamicDependency]` attribute. The
partial support comes from the fact that there's no way to map a
conditional preserve attribute (`[Preserve (Conditional = true)]`) to a
`[DynamicDependency]` attribute, so we report a warning instead.

For non-conditional `[Preserve]` attributes, we'll now add a
`[DynamicDependency]` attribute to the assembly's module constructor for
the type/member in question, effectively rooting it.

This makes it possible to fully link all our test suites when NativeAOT
(otherwise NativeAOT would just link out all the tests, leaving the test
suites empty - and unfortunately green, so this was a rather accidental
discovery).
  • Loading branch information
rolfbjarne authored Aug 18, 2023
1 parent 90cc165 commit 19b2b37
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 24 deletions.
3 changes: 3 additions & 0 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,9 @@
<!-- Enable serialization discovery. Ref: https://github.com/xamarin/xamarin-macios/issues/15676 -->
<_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --enable-serialization-discovery</_ExtraTrimmerArgs>

<!-- If we're using NativeAOT, tell ILLink to not remove dependency attributes (DynamicDependencyAttribute), because NativeAOT's trimmer also needs to see them. -->
<_ExtraTrimmerArgs Condition="'$(_UseNativeAot)' == 'true'">$(_ExtraTrimmerArgs) --keep-dep-attributes</_ExtraTrimmerArgs>

<!-- We always want the linker to process debug symbols, even when building in Release mode, because the AOT compiler uses the managed debug symbols to output DWARF debugging symbols -->
<TrimmerRemoveSymbols Condition="'$(TrimmerRemoveSymbols)' == ''">false</TrimmerRemoveSymbols>

Expand Down
37 changes: 37 additions & 0 deletions tools/dotnet-linker/AppBundleRewriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

using Mono.Cecil;
Expand Down Expand Up @@ -309,6 +310,12 @@ public TypeReference System_Diagnostics_CodeAnalysis_DynamicDependencyAttribute
}
}

public TypeReference System_Diagnostics_CodeAnalysis_DynamicallyAccessedMemberTypes {
get {
return GetTypeReference (CorlibAssembly, "System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes", out var _);
}
}

public TypeReference System_Reflection_MethodBase {
get {
return GetTypeReference (CorlibAssembly, "System.Reflection.MethodBase", out var _);
Expand Down Expand Up @@ -466,12 +473,25 @@ public MethodReference DynamicDependencyAttribute_ctor__String_Type {
return GetMethodReference (CorlibAssembly,
System_Diagnostics_CodeAnalysis_DynamicDependencyAttribute,
".ctor",
".ctor(String,Type)",
isStatic: false,
System_String,
System_Type);
}
}

public MethodReference DynamicDependencyAttribute_ctor__DynamicallyAccessedMemberTypes_Type {
get {
return GetMethodReference (CorlibAssembly,
System_Diagnostics_CodeAnalysis_DynamicDependencyAttribute,
".ctor",
".ctor(DynamicallyAccessedMemberTypes,Type)",
isStatic: false,
System_Diagnostics_CodeAnalysis_DynamicallyAccessedMemberTypes,
System_Type);
}
}

public MethodReference RuntimeTypeHandle_Equals {
get {
return GetMethodReference (CorlibAssembly, System_RuntimeTypeHandle, "Equals", isStatic: false, System_RuntimeTypeHandle);
Expand Down Expand Up @@ -1147,5 +1167,22 @@ public void ClearCurrentAssembly ()
method_map.Clear ();
field_map.Clear ();
}

public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, TypeDefinition type)
{
var attribute = new CustomAttribute (DynamicDependencyAttribute_ctor__String_Type);
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_String, memberSignature));
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_Type, type));
return attribute;
}

public CustomAttribute CreateDynamicDependencyAttribute (DynamicallyAccessedMemberTypes memberTypes, TypeDefinition type)
{
var attribute = new CustomAttribute (DynamicDependencyAttribute_ctor__DynamicallyAccessedMemberTypes_Type);
// typed as 'int' because that's how the linker expects it: https://github.com/dotnet/runtime/blob/3c5ad6c677b4a3d12bc6a776d654558cca2c36a9/src/tools/illink/src/linker/Linker/DynamicDependency.cs#L97
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_Diagnostics_CodeAnalysis_DynamicallyAccessedMemberTypes, (int) memberTypes));
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_Type, type));
return attribute;
}
}
}
121 changes: 107 additions & 14 deletions tools/dotnet-linker/ApplyPreserveAttributeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;

using Mono.Linker;
using Mono.Linker.Steps;

using Mono.Cecil;
using Mono.Cecil.Cil;

using Xamarin.Bundler;
using Xamarin.Linker;
using Xamarin.Utils;

#nullable enable

namespace Mono.Tuner {

public abstract class ApplyPreserveAttributeBase : BaseSubStep {
public abstract class ApplyPreserveAttributeBase : ConfigurationAwareSubStep {

AppBundleRewriter? abr;

protected override string Name { get => "Apply Preserve Attribute"; }

protected override int ErrorCode { get => 2450; }

// set 'removeAttribute' to true if you want the preserved attribute to be removed from the final assembly
protected abstract bool IsPreservedAttribute (ICustomAttributeProvider provider, CustomAttribute attribute, out bool removeAttribute);
Expand All @@ -30,36 +43,44 @@ public override SubStepTargets Targets {
}
}

public override void Initialize (LinkContext context)
{
base.Initialize (context);

if (Configuration.Application.XamarinRuntime == XamarinRuntime.NativeAOT)
abr = Configuration.AppBundleRewriter;
}

public override bool IsActiveFor (AssemblyDefinition assembly)
{
return Annotations.GetAction (assembly) == AssemblyAction.Link;
}

public override void ProcessType (TypeDefinition type)
protected override void Process (TypeDefinition type)
{
TryApplyPreserveAttribute (type);
}

public override void ProcessField (FieldDefinition field)
protected override void Process (FieldDefinition field)
{
foreach (var attribute in GetPreserveAttributes (field))
Mark (field, attribute);
}

public override void ProcessMethod (MethodDefinition method)
protected override void Process (MethodDefinition method)
{
MarkMethodIfPreserved (method);
}

public override void ProcessProperty (PropertyDefinition property)
protected override void Process (PropertyDefinition property)
{
foreach (var attribute in GetPreserveAttributes (property)) {
MarkMethod (property.GetMethod, attribute);
MarkMethod (property.SetMethod, attribute);
}
}

public override void ProcessEvent (EventDefinition @event)
protected override void Process (EventDefinition @event)
{
foreach (var attribute in GetPreserveAttributes (@event)) {
MarkMethod (@event.AddMethod, attribute);
Expand Down Expand Up @@ -103,6 +124,7 @@ void PreserveConditional (IMetadataTokenProvider provider)
}

Annotations.AddPreservedMethod (method.DeclaringType, method);
AddConditionalDynamicDependencyAttribute (method.DeclaringType, method);
}

static bool IsConditionalAttribute (CustomAttribute? attribute)
Expand All @@ -120,6 +142,7 @@ static bool IsConditionalAttribute (CustomAttribute? attribute)
void PreserveUnconditional (IMetadataTokenProvider provider)
{
Annotations.Mark (provider);
AddDynamicDependencyAttribute (provider);

var member = provider as IMemberDefinition;
if (member is null || member.DeclaringType is null)
Expand All @@ -131,14 +154,7 @@ void PreserveUnconditional (IMetadataTokenProvider provider)
void TryApplyPreserveAttribute (TypeDefinition type)
{
foreach (var attribute in GetPreserveAttributes (type)) {
Annotations.Mark (type);

if (!attribute.HasFields)
continue;

foreach (var named_argument in attribute.Fields)
if (named_argument.Name == "AllMembers" && (bool) named_argument.Argument.Value)
Annotations.SetPreserve (type, TypePreserve.All);
PreserveType (type, attribute);
}
}

Expand All @@ -165,5 +181,82 @@ List<CustomAttribute> GetPreserveAttributes (ICustomAttributeProvider provider)

return attrs;
}

protected void PreserveType (TypeDefinition type, CustomAttribute preserveAttribute)
{
var allMembers = false;
if (preserveAttribute.HasFields) {
foreach (var named_argument in preserveAttribute.Fields)
if (named_argument.Name == "AllMembers" && (bool) named_argument.Argument.Value)
allMembers = true;
}

PreserveType (type, allMembers);
}

protected void PreserveType (TypeDefinition type, bool allMembers)
{
Annotations.Mark (type);
if (allMembers)
Annotations.SetPreserve (type, TypePreserve.All);
AddDynamicDependencyAttribute (type, allMembers);
}

MethodDefinition GetOrCreateModuleConstructor (ModuleDefinition @module)
{
var moduleType = @module.GetModuleType ();
var moduleConstructor = moduleType.GetTypeConstructor ();
if (moduleConstructor is null) {
moduleConstructor = moduleType.AddMethod (".cctor", MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.Static, abr!.System_Void);
moduleConstructor.CreateBody (out var il);
il.Emit (OpCodes.Ret);
}
return moduleConstructor;
}

void AddDynamicDependencyAttribute (TypeDefinition type, bool allMembers)
{
if (abr is null)
return;

abr.ClearCurrentAssembly ();
abr.SetCurrentAssembly (type.Module.Assembly);

var moduleConstructor = GetOrCreateModuleConstructor (type.GetModule ());
var attrib = abr.CreateDynamicDependencyAttribute (allMembers ? DynamicallyAccessedMemberTypes.All : DynamicallyAccessedMemberTypes.None, type);
moduleConstructor.CustomAttributes.Add (attrib);

abr.ClearCurrentAssembly ();
}

void AddConditionalDynamicDependencyAttribute (TypeDefinition onType, MethodDefinition forMethod)
{
if (abr is null)
return;

// I haven't found a way to express a conditional Preserve attribute using DynamicDependencyAttribute :/
ErrorHelper.Warning (2112, Errors.MX2112 /* Unable to apply the conditional [Preserve] attribute on the member {0} */, forMethod.FullName);
}

void AddDynamicDependencyAttribute (IMetadataTokenProvider provider)
{
if (abr is null)
return;

var member = provider as IMemberDefinition;
if (member is null)
throw ErrorHelper.CreateError (99, $"Unable to add dynamic dependency attribute to {provider.GetType ().FullName}");

var module = member.GetModule ();
abr.ClearCurrentAssembly ();
abr.SetCurrentAssembly (module.Assembly);

var moduleConstructor = GetOrCreateModuleConstructor (module);
var signature = DocumentationComments.GetSignature (member);
var attrib = abr.CreateDynamicDependencyAttribute (signature, member.DeclaringType);
moduleConstructor.CustomAttributes.Add (attrib);

abr.ClearCurrentAssembly ();
}
}
}
23 changes: 23 additions & 0 deletions tools/dotnet-linker/CecilExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Linq;

using Mono.Cecil;
using Mono.Cecil.Cil;

using Xamarin.Bundler;

#nullable enable

namespace Xamarin.Linker {
Expand Down Expand Up @@ -127,5 +130,25 @@ public static MethodDefinition AddDefaultConstructor (this TypeDefinition type,
il.Emit (OpCodes.Ret);
return defaultCtor;
}

public static ModuleDefinition GetModule (this IMetadataTokenProvider provider)
{
if (provider is TypeDefinition td)
return td.Module;

if (provider is IMemberDefinition md)
return md.DeclaringType.Module;

throw ErrorHelper.CreateError (99, $"Unable to get the module of {provider.GetType ().FullName}");
}

public static TypeDefinition GetModuleType (this ModuleDefinition @module)
{
var moduleType = @module.Types.SingleOrDefault (v => v.Name == "<Module>");
if (moduleType is null)
throw ErrorHelper.CreateError (99, $"No <Module> type found in {@module.Name}");
return moduleType;
}

}
}
80 changes: 80 additions & 0 deletions tools/dotnet-linker/DocumentionComments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Text;

using Mono.Cecil;

using Xamarin.Bundler;

#nullable enable

namespace Xamarin.Utils {
// signature format: https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/documentation-comments.md#d42-id-string-format
public static class DocumentationComments {
public static string GetSignature (IMetadataTokenProvider member)
{
if (member is FieldDefinition fd)
return GetSignature (fd);

if (member is MethodDefinition md)
return GetSignature (md);

if (member is TypeDefinition td)
return GetSignature (td);

throw ErrorHelper.CreateError (99, $"Unable to get the doc signature for {member.GetType ().FullName}");
}

public static string GetSignature (TypeDefinition type)
{
if (type.IsNested)
return type.Name;
return type.FullName;
}

public static string GetSignature (FieldDefinition field)
{
return field.Name.Replace ('.', '#');
}

public static string GetSignature (MethodDefinition method)
{
var sb = new StringBuilder ();
sb.Append (method.Name.Replace ('.', '#'));
sb.Append ('(');
for (var i = 0; i < method.Parameters.Count; i++) {
if (i > 0)
sb.Append (',');

var parameterType = method.Parameters [i].ParameterType;
WriteTypeSignature (sb, parameterType);
}
sb.Append (')');

return sb.ToString ();
}

static void WriteTypeSignature (StringBuilder sb, TypeReference type)
{
if (type is ByReferenceType brt) {
WriteTypeSignature (sb, brt.GetElementType ());
sb.Append ('@');
return;
}

if (type is ArrayType at) {
WriteTypeSignature (sb, at.GetElementType ());
sb.Append ("[]");
return;
}

if (type is PointerType pt) {
WriteTypeSignature (sb, pt.GetElementType ());
sb.Append ('*');
return;
}

sb.Append (type.FullName.Replace ('/', '.'));
}
}
}
Loading

6 comments on commit 19b2b37

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.