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
4 changes: 3 additions & 1 deletion dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@
<PropertyGroup>
<_UseDynamicDependenciesInsteadOfMarking Condition="'$(_UseDynamicDependenciesInsteadOfMarking)' == ''">true</_UseDynamicDependenciesInsteadOfMarking>
<_UseDynamicDependenciesForProtocolPreservation Condition="'$(_UseDynamicDependenciesForProtocolPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForProtocolPreservation>
<_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForSmartEnumPreservation>
</PropertyGroup>

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

<!--
pre-sweep custom steps
Expand Down
42 changes: 20 additions & 22 deletions tests/monotouch-test/ObjCRuntime/StrongEnumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,57 +33,55 @@ public void GetConstant ()
if (getValue is null || getConstant.IsDefined (typeof (ObsoleteAttribute)))
continue;

#pragma warning disable IL3050 // Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues<TEnum> overload or the GetValuesAsUnderlyingType method instead.
foreach (var enumValue in Enum.GetValues (type)) {
#pragma warning restore IL3050
foreach (var enumValue in Enum.GetValuesAsUnderlyingType (type)) {
var obj = getConstant.Invoke (null, new object [] { enumValue });

if (valuesToSkip.Remove ((Enum) enumValue))
if (valuesToSkip.Remove ((type, enumValue)))
continue;

if (obj is not null) {
var rtrip = getValue.Invoke (null, new object [] { obj });
Assert.AreEqual (enumValue, rtrip, $"{type.FullName}.{enumValue}: Round trip failed: {enumValue}.GetConstant () -> {obj} but GetValue ({obj}) -> {rtrip}");
Assert.AreEqual (Enum.ToObject (type, enumValue), rtrip, $"{type.FullName}.{enumValue}: Round trip failed: {enumValue}.GetConstant () -> {obj} but GetValue ({obj}) -> {rtrip}");
}
}
}

// Only very that all the skipped values are correct if nothing has been trimmed away.
// Only verify that all the skipped values are correct if nothing has been trimmed away.
if (!TestRuntime.IsLinkAny)
Assert.That (valuesToSkip, Is.Empty, "All values to be skipped were actually skipped");
});
}

Enum [] GetSkippedEnumValues ()
(Type EnumType, object UnderlyingValue) [] GetSkippedEnumValues ()
{
var rv = new List<Enum> () {
var rv = new List<(Type EnumType, object UnderlyingValue)> () {
#if !XAMCORE_5_0
global::AVFoundation.AVCaptureDeviceType.BuiltInDualCamera,
(typeof (global::AVFoundation.AVCaptureDeviceType), (int) global::AVFoundation.AVCaptureDeviceType.BuiltInDualCamera),
#if __MACOS__
global::AVFoundation.AVCaptureDeviceType.External,
(typeof (global::AVFoundation.AVCaptureDeviceType), (int) global::AVFoundation.AVCaptureDeviceType.External),
#endif
global::AVFoundation.AVCaptureDeviceType.Microphone,
global::Foundation.NSLinguisticTag.OtherPunctuation,
global::Foundation.NSLinguisticTag.OtherWhitespace,
global::Foundation.NSRunLoopMode.Other,
(typeof (global::AVFoundation.AVCaptureDeviceType), (int) global::AVFoundation.AVCaptureDeviceType.Microphone),
(typeof (global::Foundation.NSLinguisticTag), (int) global::Foundation.NSLinguisticTag.OtherPunctuation),
(typeof (global::Foundation.NSLinguisticTag), (int) global::Foundation.NSLinguisticTag.OtherWhitespace),
(typeof (global::Foundation.NSRunLoopMode), (int) global::Foundation.NSRunLoopMode.Other),
#if !__TVOS__
global::HealthKit.HKCategoryTypeIdentifier.EnvironmentalAudioExposureEvent,
(typeof (global::HealthKit.HKCategoryTypeIdentifier), (int) global::HealthKit.HKCategoryTypeIdentifier.EnvironmentalAudioExposureEvent),
#endif
#if __MACOS__
global::iTunesLibrary.ITLibPlaylistProperty.Primary,
global::ImageKit.IKToolMode.SelectRect,
(typeof (global::iTunesLibrary.ITLibPlaylistProperty), (int) global::iTunesLibrary.ITLibPlaylistProperty.Primary),
(typeof (global::ImageKit.IKToolMode), (int) global::ImageKit.IKToolMode.SelectRect),
#endif
global::Security.SecKeyType.ECSecPrimeRandom,
(typeof (global::Security.SecKeyType), (int) global::Security.SecKeyType.ECSecPrimeRandom),
#if !__MACOS__
global::UIKit.UIWindowSceneSessionRole.ExternalDisplayNonInteractive,
(typeof (global::UIKit.UIWindowSceneSessionRole), (int) global::UIKit.UIWindowSceneSessionRole.ExternalDisplayNonInteractive),
#endif
#endif // !XAMCORE_5_0
};

#if __TVOS__ && !XAMCORE_5_0
if (Runtime.Arch == Arch.SIMULATOR) {
rv.AddRange (Enum.GetValues<global::BrowserEngineKit.BEAccessibilityTrait> ().Cast<Enum> ()); // BrowserEngineKit isn't available in the simulator
rv.AddRange (Enum.GetValues<global::BrowserEngineKit.BEAccessibilityNotification> ().Cast<Enum> ()); // BrowserEngineKit isn't available in the simulator
rv.AddRange (Enum.GetValues<global::BrowserEngineKit.BEAccessibilityTrait> ().Select (v => (typeof (global::BrowserEngineKit.BEAccessibilityTrait), (object) (int) v))); // BrowserEngineKit isn't available in the simulator
rv.AddRange (Enum.GetValues<global::BrowserEngineKit.BEAccessibilityNotification> ().Select (v => (typeof (global::BrowserEngineKit.BEAccessibilityNotification), (object) (int) v))); // BrowserEngineKit isn't available in the simulator
}
#endif // __TVOS__ && !XAMCORE_5_0

Expand All @@ -92,7 +90,7 @@ Enum [] GetSkippedEnumValues ()
// NewScene and NewItem both return 'com.apple.menu.new-item' so
// Round trip failed: NewItem.GetConstant () -> com.apple.menu.new-item but GetValue (com.apple.menu.new-item) -> NewScene
// That said NewItem is the one that should be used and NewScene is deprecated in Xcode 26.0.
rv.Add (global::UIKit.UIMenuIdentifier.NewItem);
rv.Add ((typeof (global::UIKit.UIMenuIdentifier), (int) global::UIKit.UIMenuIdentifier.NewItem));
}
#endif // !__MACOS__

Expand Down
41 changes: 41 additions & 0 deletions tools/dotnet-linker/AppBundleRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,17 @@ public MethodReference Dictionary2_Add {
}
}

public MethodReference DynamicDependencyAttribute_ctor__String {
get {
return GetMethodReference (CorlibAssembly,
System_Diagnostics_CodeAnalysis_DynamicDependencyAttribute,
".ctor",
".ctor(String)",
isStatic: false,
System_String);
}
}

public MethodReference DynamicDependencyAttribute_ctor__String_Type {
get {
return GetMethodReference (CorlibAssembly,
Expand Down Expand Up @@ -1246,6 +1257,33 @@ public void ClearCurrentAssembly ()
field_map.Clear ();
}

// We only need to add dependency attributes if the target dependency is in a trimmed assembly,
// otherwise the target dependency won't be trimmed away.
bool IsAssemblyTrimmed (IMemberDefinition member)
{
var assembly = member is TypeDefinition td ? td.Module.Assembly : member.DeclaringType.Module.Assembly;
var action = configuration.Context.Annotations.GetAction (assembly);
return action == AssemblyAction.Link;
}

public bool AddDynamicDependencyAttribute (MethodDefinition addToMethod, MethodDefinition dependsOn)
{
if (!IsAssemblyTrimmed (dependsOn))
return false;

if (addToMethod.DeclaringType == dependsOn.DeclaringType) {
var attribute = new CustomAttribute (DynamicDependencyAttribute_ctor__String);
attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_String, DocumentationComments.GetSignature (dependsOn)));
return AddAttributeOnlyOnce (addToMethod, attribute);
} else if (addToMethod.DeclaringType.Module == dependsOn.DeclaringType.Module) {
var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn), dependsOn.DeclaringType);
return AddAttributeOnlyOnce (addToMethod, attribute);
} else {
var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn), dependsOn.DeclaringType, dependsOn.DeclaringType.Module.Assembly);
return AddAttributeOnlyOnce (addToMethod, attribute);
}
}

public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, TypeDefinition type)
{
if (type.HasGenericParameters)
Expand Down Expand Up @@ -1307,6 +1345,9 @@ public bool AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onT
/// <returns>Whether an attribute was added or not.</returns>
public bool AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onType, TypeDefinition forType)
{
if (!IsAssemblyTrimmed (forType))
return false;

var placeholderName = "__linker_preserve__";
FieldDefinition? placeholderMember = null;
if (forType.HasFields)
Expand Down
1 change: 1 addition & 0 deletions tools/dotnet-linker/PreserveProtocolsStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ protected override bool IsActiveFor (AssemblyDefinition assembly)
if (DerivedLinkContext.App.Registrar != Bundler.RegistrarMode.Dynamic)
return false;

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

Expand Down
212 changes: 212 additions & 0 deletions tools/dotnet-linker/PreserveSmartEnumConversionsStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// Copyright 2017 Xamarin Inc.

using System.Linq;

using Mono.Cecil;
using Mono.Linker;
using Mono.Linker.Steps;
using Mono.Tuner;

using Xamarin.Bundler;
using Xamarin.Tuner;

Comment thread
rolfbjarne marked this conversation as resolved.
#nullable enable

namespace Xamarin.Linker.Steps {
public class PreserveSmartEnumConversionsStep : AssemblyModifierStep {
protected override string Name { get; } = "Smart Enum Conversion Preserver";
protected override int ErrorCode { get; } = 2200;

PreserveSmartEnumConversion? preserver;
PreserveSmartEnumConversion Preserver {
get {
if (preserver is null)
preserver = new PreserveSmartEnumConversion (DerivedLinkContext, Preserve);
return preserver;
}
}

protected override bool IsActiveFor (AssemblyDefinition assembly)
{
// We have to process assemblies that aren't linked, because type A from the unlinked assembly X
// might reference the smart enum B from the linked assembly Y,
// and we need to make sure that smart enum B's conversion methods aren't trimmed away - which
// means adding dynamic dependency attributes to the methods in the unlinked assembly X,
// which means we need to process the unlinked assembly X.

// Hot Reload: we can't modify user assemblies when Hot Reload is enabled (otherwise Hot Reload won't work),
// so we'll have to come up with a different solution (emit xml definition instead maybe?)

// Unless an assembly is or references our platform assembly, then it won't have anything we need to preserve
if (!Configuration.Profile.IsOrReferencesProductAssembly (assembly))
return false;

return true;
}

bool Preserve (Tuple<MethodDefinition, MethodDefinition> pair, bool alreadyProcessed, params MethodDefinition? [] conditions)
{
var conds = conditions.Where (v => v is not null).Cast<MethodDefinition> ().ToArray ();
if (conds.Length == 0)
return false;

var modified = false;
foreach (var condition in conds) {
modified |= abr.AddDynamicDependencyAttribute (condition, pair.Item1);
modified |= abr.AddDynamicDependencyAttribute (condition, pair.Item2);
}

return modified;
}

protected override bool ProcessType (TypeDefinition type)
{
var modified = false;

if (!type.HasMethods)
return modified;

foreach (var method in type.Methods)
modified |= ProcessMethod (method);

return modified;
}

bool ProcessMethod (MethodDefinition method)
{
static bool IsPropertyMethod (MethodDefinition method)
{
return method.IsGetter || method.IsSetter;
}

var modified = false;
modified |= Preserver.ProcessAttributeProvider (method, method);
modified |= Preserver.ProcessAttributeProvider (method.MethodReturnType, method);

if (method.HasParameters) {
foreach (var p in method.Parameters)
modified |= Preserver.ProcessAttributeProvider (p, method);
}
if (IsPropertyMethod (method)) {
foreach (var property in method.DeclaringType.Properties)
if (property.GetMethod == method || property.SetMethod == method) {
modified |= Preserver.ProcessAttributeProvider (property, property.GetMethod, property.SetMethod);
break;
}
}
return modified;
}
}

class PreserveSmartEnumConversion {
Dictionary<TypeDefinition, Tuple<MethodDefinition, MethodDefinition>> cache = new ();

public DerivedLinkContext LinkContext { get; private set; }

Func<Tuple<MethodDefinition, MethodDefinition>, bool, MethodDefinition? [], bool> preserve { get; set; }

public PreserveSmartEnumConversion (DerivedLinkContext linkContext, Func<Tuple<MethodDefinition, MethodDefinition>, bool, MethodDefinition? [], bool> preserve)
{
LinkContext = linkContext;
this.preserve = preserve;
}

bool Preserve (Tuple<MethodDefinition, MethodDefinition> pair, bool alreadyProcessed, params MethodDefinition? [] conditions)
{
return preserve (pair, alreadyProcessed, conditions);
}

public bool ProcessAttributeProvider (ICustomAttributeProvider provider, params MethodDefinition [] conditions)
{
var modified = false;

if (provider?.HasCustomAttributes != true)
return modified;

foreach (var ca in provider.CustomAttributes) {
var tr = ca.Constructor.DeclaringType;

if (!tr.Is ("ObjCRuntime", "BindAsAttribute"))
continue;

if (ca.ConstructorArguments.Count != 1) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 4124, provider, Errors.MT4124_E, provider.AsString (), ca.ConstructorArguments.Count));
continue;
}

var managedType = ca.ConstructorArguments [0].Value as TypeReference;
var managedEnumType = managedType?.GetElementType ().Resolve ();
if (managedEnumType is null) {
ErrorHelper.Show (ErrorHelper.CreateWarning (LinkContext.App, 4124, provider, Errors.MT4124_H, provider.AsString (), managedType?.FullName ?? "(null)"));
continue;
}

// We only care about enums, BindAs attributes can be used for other types too.
if (!managedEnumType.IsEnum)
continue;

if (cache.TryGetValue (managedEnumType, out var pair)) {
// The pair was already marked if it was cached.
modified |= Preserve (pair, true, conditions);
continue;
}

// Find the Extension type
TypeDefinition? extensionType = null;
var extensionName = managedEnumType.Name + "Extensions";
foreach (var type in managedEnumType.Module.Types) {
if (type.Namespace != managedEnumType.Namespace)
continue;
if (type.Name != extensionName)
continue;
extensionType = type;
break;
}
if (extensionType is null) {
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.");
continue;
}

// Find the GetConstant/GetValue methods
MethodDefinition? getConstant = null;
MethodDefinition? getValue = null;

foreach (var method in extensionType.Methods) {
if (!method.IsStatic)
continue;
if (!method.HasParameters || method.Parameters.Count != 1)
continue;
if (method.Name == "GetConstant") {
if (!method.ReturnType.Is ("Foundation", "NSString"))
continue;
if (method.Parameters [0].ParameterType != managedEnumType)
continue;
getConstant = method;
} else if (method.Name == "GetValue") {
if (!method.Parameters [0].ParameterType.Is ("Foundation", "NSString"))
continue;
if (method.ReturnType != managedEnumType)
continue;
getValue = method;
}
}

if (getConstant is null) {
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.");
continue;
}

if (getValue is null) {
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.");
continue;
}

pair = new Tuple<MethodDefinition, MethodDefinition> (getConstant, getValue);
cache.Add (managedEnumType, pair);
modified |= Preserve (pair, false, conditions);
}

return modified;
}
}
}
Loading
Loading