Skip to content

Commit

Permalink
[compatibility] IsTrimmable support, fix linker warnings (#3161)
Browse files Browse the repository at this point in the history
To make `Microsoft.Maui.Controls.Compatibility.dll` "trimmable", we
can simply add:

    [assembly: AssemblyMetadata ("IsTrimmable", "True")]

This doesn't mean it actually *works* though! We should enable linker
warnings and fix what it finds.

To view linker warnings in .NET 6, you can do:

    > .\bin\dotnet\dotnet.exe build `
        .\src\Controls\samples\Controls.Sample.SingleProject\Maui.Controls.Sample.SingleProject.csproj
        -f net6.0-android `
        -c Release `
        -p:SuppressTrimAnalysisWarnings=false `
        -p:TrimmerSingleWarn=false `
        -bl

Then open the `msbuild.binlog` file and filter the warnings (that get
upgraded to errors) for `src/Compatibility`:

    src\Compatibility\Core\src\Android\Deserializer.cs(38,6): error IL2026: Microsoft.Maui.Controls.Compatibility.Platform.Android.Deserializer.<>c.<DeserializePropertiesAsync>b__2_0(): Using member 'System.Runtime.Serialization.XmlObjectSerializer.ReadObject(XmlDictionaryReader)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Data Contract Serialization and Deserialization might require types that cannot be statically analyzed. Make sure all of the required types are preserved.
    src\Compatibility\Core\src\Android\Deserializer.cs(71,6): error IL2026: Microsoft.Maui.Controls.Compatibility.Platform.Android.Deserializer.<>c__DisplayClass3_0.<SerializePropertiesAsync>b__0(): Using member 'System.Runtime.Serialization.XmlObjectSerializer.WriteObject(XmlDictionaryWriter,Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Data Contract Serialization and Deserialization might require types that cannot be statically analyzed. Make sure all of the required types are preserved.
    src\Compatibility\Core\src\Android\AndroidAppIndexProvider.cs(14,5): error IL2072: Microsoft.Maui.Controls.Compatibility.Platform.Android.AndroidAppIndexProvider.AndroidAppIndexProvider(Context): '#0' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'System.Object System.Activator::CreateInstance(System.Type,System.Object[],System.Object[])'. The return value of method 'System.Type.GetType(String,Boolean)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
    src\Compatibility\Core\src\AppHostBuilderExtensions.cs(49,4): error IL2091: Microsoft.Maui.Controls.Hosting.MauiAppBuilderExtensions.UseMauiApp<TApp>(MauiAppBuilder): 'TImplementation' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in 'Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton<TService,TImplementation>(IServiceCollection)'. The generic parameter 'TApp' of 'Microsoft.Maui.Controls.Hosting.MauiAppBuilderExtensions.UseMauiApp<TApp>(MauiAppBuilder)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
    src\Compatibility\Core\src\Android\NativeBindingservice.cs(16,4): error IL2075: Microsoft.Maui.Controls.Compatibility.Platform.Android.NativeBindingService.TrySetBinding(Object,String,BindingBase): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'System.Type System.Object::GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
    src\Compatibility\Core\src\Android\ResourceManager.cs(28,4): error IL2026: Microsoft.Maui.Controls.Compatibility.Platform.Android.ResourceManager.FindType(String,String): Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.
    src\Compatibility\Core\src\Android\ResourceManager.cs(419,4): error IL2070: Microsoft.Maui.Controls.Compatibility.Platform.Android.ResourceManager.GetId(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields' in call to 'System.Type.GetFields()'. The parameter 'type' of method 'Microsoft.Maui.Controls.Compatibility.Platform.Android.ResourceManager.GetId(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
    src\Compatibility\Core\src\Android\ResourceManager.cs(432,5): error IL2070: Microsoft.Maui.Controls.Compatibility.Platform.Android.ResourceManager.GetId(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The parameter 'type' of method 'Microsoft.Maui.Controls.Compatibility.Platform.Android.ResourceManager.GetId(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

I went through applying the new linker attributes to resolve these
issues in this assembly.

In most cases, I simply added C# attributes to solve warnings.
However, a couple places needed code changes:

* `Deserializer`, I removed usage of a lambda for a regular method.
  There isn't a way to decorate lambdas with C# attributes to appease
  the linker.

* `ResourceManager.FindType()`, I replaced System.Linq usage with a
  `foreach` loop that the linker could better understand (lambdas
  again!). This should generally improve performance, anyway.

Lastly, I had to actually add some attributes to `DependencyService`,
otherwise we got crashes at startup like:

    android.runtime.JavaProxyThrowable: System.MissingMethodException: Arg_NoDefCTor, Microsoft.Maui.Controls.Compatibility.Platform.Android.NativeValueConverterService
      at System.RuntimeType.CreateInstanceMono(Boolean , Boolean )
      at System.RuntimeType.CreateInstanceDefaultCtor(Boolean , Boolean )
      at System.Activator.CreateInstance(Type , Boolean , Boolean )
      at System.Activator.CreateInstance(Type , Boolean )
      at System.Activator.CreateInstance(Type )
      at Microsoft.Maui.Controls.DependencyService.Get[INativeValueConverterService](DependencyFetchTarget fetchTarget)
      at Microsoft.Maui.Controls.Xaml.TypeConversionExtensions.ConvertTo(Object value, Type toType, Func`1 getConverter, IServiceProvider serviceProvider, Exception& exception)
      at Microsoft.Maui.Controls.Xaml.TypeConversionExtensions.ConvertTo(Object value, Type toType, Func`1 minfoRetriever, IServiceProvider serviceProvider, Exception& exception)
      at Microsoft.Maui.Controls.Xaml.ValueConverterProvider.Convert(Object value, Type toType, Func`1 minfoRetriever, IServiceProvider serviceProvider)
      at Microsoft.Maui.Controls.Xaml.AppThemeBindingExtension.Microsoft.Maui.Controls.Xaml.IMarkupExtension<Microsoft.Maui.Controls.BindingBase>.ProvideValue(IServiceProvider serviceProvider)
      at Maui.Controls.Sample.XamlApp.InitializeComponent()

~~ Results ~~

I built `Maui.Controls.Sample.SingleProject.csproj` with
`-p:AndroidUseAssemblyStore=false`, so we can see the size differences
of individual assemblies.

* Before 30275786 bytes
* After  30181399 bytes

    > apkdiff -f before.apk after.apk
    Size difference in bytes ([*1] apk1 only, [*2] apk2 only):
      +         437 assemblies/Microsoft.Maui.Controls.dll
      +         161 assemblies/x86/System.Private.CoreLib.dll
      +         160 assemblies/arm64-v8a/System.Private.CoreLib.dll
      +         139 assemblies/armeabi-v7a/System.Private.CoreLib.dll
      +         127 assemblies/x86_64/System.Private.CoreLib.dll
      +          28 assemblies/System.Runtime.dll
      +           1 assemblies/Maui.Controls.Sample.SingleProject.dll
      -           1 assemblies/Microsoft.Maui.Controls.Xaml.dll
      -          15 assemblies/System.Xml.ReaderWriter.dll
      -          16 assemblies/System.Threading.dll
      -         231 META-INF/BNDLTOOL.SF
      -         231 META-INF/MANIFEST.MF
      -       2,498 assemblies/System.Runtime.Serialization.Xml.dll *1
      -       3,523 assemblies/System.IO.IsolatedStorage.dll *1
      -       6,341 assemblies/Mono.Android.dll
      -       8,352 lib/armeabi-v7a/libxamarin-app.so
      -       8,352 lib/x86/libxamarin-app.so
      -       8,408 lib/arm64-v8a/libxamarin-app.so
      -       8,408 lib/x86_64/libxamarin-app.so
      -      38,104 classes.dex
      -      72,810 assemblies/Microsoft.Maui.Controls.Compatibility.dll
    Summary:
      -         462 Other entries -0.00% (of 12,105,588)
      -      84,151 Assemblies -0.82% (of 10,293,792)
      -      38,104 Dalvik executables -0.59% (of 6,486,156)
      -      33,520 Shared libraries -0.18% (of 18,486,516)
      -     183,296 Uncompressed assemblies -0.81% (of 22,604,792)
      -      94,387 Package size difference -0.31% (of 30,275,786)

An average of 10 runs on a Pixel 5, seems to show a difference in
startup as well:

    Before:
    Activity: Displayed     1.676s
    After:
    Activity: Displayed     1.642s

I think this might help startup by ~34ms?
  • Loading branch information
jonathanpeppers committed Nov 1, 2021
1 parent 55175b8 commit 3c8e158
Show file tree
Hide file tree
Showing 22 changed files with 682 additions and 188 deletions.
4 changes: 4 additions & 0 deletions src/Compatibility/Core/src/Android/AndroidAppIndexProvider.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Android.Content;

namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
{
public class AndroidAppIndexProvider : IAppIndexingProvider
{
[UnconditionalSuppressMessage ("Trimming", "IL2035", Justification = AppLinksAssemblyName + ".dll is not always present.")]
[UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = AppLinksAssemblyName + ".dll is not always present.")]
[DynamicDependency (DynamicallyAccessedMemberTypes.PublicConstructors, AppLinksAssemblyName + "." + AppLinksClassName, AppLinksAssemblyName)]
public AndroidAppIndexProvider(Context context)
{
var fullyQualifiedName = $"{AppLinksAssemblyName}.{AppLinksClassName}, {AppLinksAssemblyName}";
Expand Down
87 changes: 46 additions & 41 deletions src/Compatibility/Core/src/Android/Deserializer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Xml;
Expand All @@ -17,36 +17,38 @@ internal class Deserializer : IDeserializer
static string GetFilePath()
=> Path.Combine(Essentials.FileSystem.CacheDirectory, PropertyStoreFile);

public Task<IDictionary<string, object>> DeserializePropertiesAsync()
[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
public Task<IDictionary<string, object>> DeserializePropertiesAsync() => Task.Factory.StartNew(DeserializeProperties);

[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
IDictionary<string, object> DeserializeProperties()
{
// Deserialize property dictionary to local storage
// Make sure to use Internal
return Task.Run(() =>
{
var path = GetFilePath();
var path = GetFilePath();

if (!File.Exists(path))
return null;
if (!File.Exists(path))
return null;

using var stream = File.OpenRead(path);
using var xmlReader = XmlReader.Create(stream);
using var reader = XmlDictionaryReader.CreateDictionaryReader(xmlReader);
using var stream = File.OpenRead(path);
using var xmlReader = XmlReader.Create(stream);
using var reader = XmlDictionaryReader.CreateDictionaryReader(xmlReader);

try
{
var dcs = new DataContractSerializer(typeof(Dictionary<string, object>));
return (IDictionary<string, object>)dcs.ReadObject(reader);
}
catch (Exception e)
{
Debug.WriteLine("Could not deserialize properties: " + e.Message);
Log.Warning("Microsoft.Maui.Controls.Compatibility PropertyStore", $"Exception while reading Application properties: {e}");
}
try
{
var dcs = new DataContractSerializer(typeof(Dictionary<string, object>));
return (IDictionary<string, object>)dcs.ReadObject(reader);
}
catch (Exception e)
{
Debug.WriteLine("Could not deserialize properties: " + e.Message);
Log.Warning("Microsoft.Maui.Controls.Compatibility PropertyStore", $"Exception while reading Application properties: {e}");
}

return null;
});
return null;
}

[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
public Task SerializePropertiesAsync(IDictionary<string, object> properties)
{
properties = new Dictionary<string, object>(properties);
Expand All @@ -55,29 +57,32 @@ public Task SerializePropertiesAsync(IDictionary<string, object> properties)
if (properties.Count <= 0)
return Task.CompletedTask;

return Task.Factory.StartNew (SerializeProperties, properties);
}

[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
void SerializeProperties(object properties)
{
// Serialize property dictionary to local storage
// Make sure to use Internal
return Task.Run(() =>
{
var path = GetFilePath();
var path = GetFilePath();

using var stream = File.Create(path);
using var xmlWriter = XmlWriter.Create(stream);
using var writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);
using var stream = File.Create(path);
using var xmlWriter = XmlWriter.Create(stream);
using var writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);

try
{
var dcs = new DataContractSerializer(typeof(Dictionary<string, object>));
dcs.WriteObject(writer, properties);
writer.Flush();
}
catch (Exception e)
{
Debug.WriteLine("Could not serialize properties: " + e.Message);
Log.Warning("Microsoft.Maui.Controls.Compatibility PropertyStore", $"Exception while writing Application properties: {e}");
return;
}
});
try
{
var dcs = new DataContractSerializer(typeof(Dictionary<string, object>));
dcs.WriteObject(writer, properties);
writer.Flush();
}
catch (Exception e)
{
Debug.WriteLine("Could not serialize properties: " + e.Message);
Log.Warning("Microsoft.Maui.Controls.Compatibility PropertyStore", $"Exception while writing Application properties: {e}");
return;
}
}
}
}
2 changes: 2 additions & 0 deletions src/Compatibility/Core/src/Android/NativeBindingservice.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Maui.Controls.Xaml.Internals;
using AView = Android.Views.View;

Expand All @@ -8,6 +9,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
{
class NativeBindingService : INativeBindingService
{
[UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = TrimmerConstants.NativeBindingService)]
public bool TrySetBinding(object target, string propertyName, BindingBase binding)
{
var view = target as AView;
Expand Down
34 changes: 28 additions & 6 deletions src/Compatibility/Core/src/Android/ResourceManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -23,23 +23,39 @@ public static class ResourceManager
static ImageCache GetCache() => _lruCache.Value;

static Assembly _assembly;
[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Resource.designer.cs is in the root application assembly, which should be preserved.")]
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
static Type FindType(string name, string altName)
{
return _assembly?.GetTypes().FirstOrDefault(x => x.Name == name || x.Name == altName);
if (_assembly != null)
{
foreach (var type in _assembly.GetTypes())
{
if (type.Name == name || type.Name == altName)
return type;
}
}
return null;
}
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
static Type _drawableClass;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
static Type _resourceClass;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
static Type _styleClass;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
static Type _layoutClass;

public static Type DrawableClass
{
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
get
{
if (_drawableClass == null)
_drawableClass = FindType("Drawable", "Resource_Drawable");
return _drawableClass;
}
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
set
{
_drawableClass = value;
Expand All @@ -48,12 +64,14 @@ public static Type DrawableClass

public static Type ResourceClass
{
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
get
{
if (_resourceClass == null)
_resourceClass = FindType("Id", "Resource_Id");
return _resourceClass;
}
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
set
{
_resourceClass = value;
Expand All @@ -62,12 +80,14 @@ public static Type ResourceClass

public static Type StyleClass
{
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
get
{
if (_styleClass == null)
_styleClass = FindType("Style", "Resource_Style");
return _styleClass;
}
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
set
{
_styleClass = value;
Expand All @@ -76,12 +96,14 @@ public static Type StyleClass

public static Type LayoutClass
{
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
get
{
if (_layoutClass == null)
_layoutClass = FindType("Layout", "Resource_Layout");
return _layoutClass;
}
[param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
set
{
_layoutClass = value;
Expand Down Expand Up @@ -359,17 +381,17 @@ public static void Init(Assembly mainAssembly)
_assembly = mainAssembly;
}

static int IdFromTitle(string title, Type resourceType, string defType, Resources resource)
static int IdFromTitle(string title, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] Type resourceType, string defType, Resources resource)
{
return IdFromTitle(title, resourceType, defType, resource, Platform.GetPackageName());
}

static int IdFromTitle(string title, Type resourceType, string defType, Context context)
static int IdFromTitle(string title, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] Type resourceType, string defType, Context context)
{
return IdFromTitle(title, resourceType, defType, context?.Resources, context?.PackageName);
}

static int IdFromTitle(string title, Type resourceType, string defType, Resources resource, string packageName)
static int IdFromTitle(string title, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] Type resourceType, string defType, Resources resource, string packageName)
{
int id = 0;
if (title == null)
Expand Down Expand Up @@ -409,7 +431,7 @@ int SearchByIdentifier(string n, string d, Resources r, string p)
return GetId(resourceType, name);
}

static int GetId(Type type, string memberName)
static int GetId([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string memberName)
{
// This may legitimately be null in designer scenarios
if (type == null)
Expand Down
5 changes: 3 additions & 2 deletions src/Compatibility/Core/src/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -43,15 +44,15 @@ namespace Microsoft.Maui.Controls.Hosting
{
public static class MauiAppBuilderExtensions
{
public static MauiAppBuilder UseMauiApp<TApp>(this MauiAppBuilder builder)
public static MauiAppBuilder UseMauiApp<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TApp>(this MauiAppBuilder builder)
where TApp : class, IApplication
{
builder.Services.TryAddSingleton<IApplication, TApp>();
builder.SetupDefaults();
return builder;
}

public static MauiAppBuilder UseMauiApp<TApp>(this MauiAppBuilder builder, Func<IServiceProvider, TApp> implementationFactory)
public static MauiAppBuilder UseMauiApp<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TApp>(this MauiAppBuilder builder, Func<IServiceProvider, TApp> implementationFactory)
where TApp : class, IApplication
{
builder.Services.TryAddSingleton<IApplication>(implementationFactory);
Expand Down
3 changes: 3 additions & 0 deletions src/Compatibility/Core/src/GTK/GtkSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
Expand All @@ -13,6 +14,7 @@ internal sealed class GtkSerializer : IDeserializer
{
const string PropertyStoreFile = "PropertyStore.forms";

[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
public Task<IDictionary<string, object>> DeserializePropertiesAsync()
{
try
Expand Down Expand Up @@ -54,6 +56,7 @@ public Task<IDictionary<string, object>> DeserializePropertiesAsync()
}
}

[RequiresUnreferencedCode(TrimmerConstants.SerializerTrimmerWarning)]
public Task SerializePropertiesAsync(IDictionary<string, object> properties)
{
try
Expand Down
2 changes: 2 additions & 0 deletions src/Compatibility/Core/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
using System.Runtime.CompilerServices;

[assembly: AssemblyMetadata ("IsTrimmable", "True")]
[assembly: InternalsVisibleTo("Compatibility.Windows.UnitTests")]
[assembly: InternalsVisibleTo("Compatibility.Android.UnitTests")]
Loading

0 comments on commit 3c8e158

Please sign in to comment.