Skip to content

Commit

Permalink
Fix AOT compatibility issues in Infra (#273)
Browse files Browse the repository at this point in the history
* Enable AOT compatibility analyzers

* AOT compatible design for SettingItem converter

* Add source generation json serializer context for SettingsService

* Mark SettingsCollection as AOT incompatible

* Config serializer context in SettingsService

* Mark dynamically accessed members in Infra.UI

* Add DynamicallyAccessedMembers to WinRTSettingsStorage

* Add DynamicallyAccessedMembers to SettingsContainer

* Mark SettingsCollection as trimming incompatible

* Clean up

* Refactor Infra.Settings using generics for AOT

* Fix SettingItem generator

* Fix JsonStringConverter
  • Loading branch information
gaviny82 committed Sep 8, 2024
1 parent 0f841e5 commit dff219f
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 207 deletions.
17 changes: 16 additions & 1 deletion Natsurainko.FluentLauncher/Services/Settings/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Windows.Storage;

namespace Natsurainko.FluentLauncher.Services.Settings;
Expand Down Expand Up @@ -133,9 +134,11 @@ public partial class SettingsService : SettingsContainer
[SettingItem(Default = 0u)]
public partial uint SettingsVersion { get; set; }


public SettingsService(ISettingsStorage storage) : base(storage)
{
// Configure JsonSerializerContext for NativeAOT-compatible JsonStringConverter
JsonStringConverterConfig.SerializerContext = SetingsJsonSerializerContext.Default;

var appsettings = ApplicationData.Current.LocalSettings;

// Migrate settings data structures from old versions
Expand Down Expand Up @@ -271,3 +274,15 @@ private static void MigrateFrom_2_3_0_0()
appsettings.Values["ActiveInstanceId"] = clientId;
}
}

[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Windows.UI.Color))]
[JsonSerializable(typeof(WinUIEx.WindowState))]
internal partial class SetingsJsonSerializerContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,26 @@ private static void GenerateSettingItem(SettingItemInfo settingItemInfo, StringB

string defaultValue = "";
string converter = "";
string sourceTypeName = "";

// Parse default value and converter
foreach(var item in attribute.NamedArguments)
{
if (item.Key == "Default")
{
defaultValue = $", {item.Value.ToCSharpString()}";
}
else if (item.Key == "Converter")
converter = $", global::FluentLauncher.Infra.Settings.Converters.DataTypeConverters.GetConverter(typeof(global::{item.Value.Value}))";
{
if (item.Value.Value is not ITypeSymbol converterType)
continue;
INamedTypeSymbol? interfaceType = converterType.Interfaces.FirstOrDefault(syn => syn.Name.Contains("IDataTypeConverter"));
if (interfaceType is null) continue;
ITypeSymbol sourceType = interfaceType.TypeArguments[0];

sourceTypeName = $", {sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}";
converter = $", global::{item.Value.Value}.Instance";
}
}

// If default value is not provided, the property is nullable
Expand All @@ -142,8 +154,8 @@ private static void GenerateSettingItem(SettingItemInfo settingItemInfo, StringB
memberBuilder.Append($$"""
public partial {{propTypeName}}{{nullable}} {{propIdentifierName}}
{
get => GetValue<{{propTypeName}}{{nullable}}>(nameof({{propIdentifierName}}){{defaultValue}}{{converter}});
set => SetValue<{{propTypeName}}>(nameof({{propIdentifierName}}), value, {{propIdentifierName}}Changed{{converter}});
get => GetValue<{{propTypeName}}{{sourceTypeName}}>(nameof({{propIdentifierName}}){{defaultValue}}{{converter}});
set => SetValue<{{propTypeName}}{{sourceTypeName}}>(nameof({{propIdentifierName}}), value, {{propIdentifierName}}Changed{{converter}});
}

public event global::FluentLauncher.Infra.Settings.SettingChangedEventHandler? {{propIdentifierName}}Changed;
Expand Down
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 System.Text;
using System.Threading.Tasks;
Expand All @@ -10,49 +11,12 @@ namespace FluentLauncher.Infra.Settings.Converters;
/// ConvertFrom is used when converting an object from the displayed type to the stored type. <br/>
/// Convert is used when converting an object from the stored type to the displayed type.
/// </summary>
public interface IDataTypeConverter
/// <typeparam name="TSource">Type stored in the storage</typeparam>
/// <typeparam name="TTarget">Type used in the application</typeparam>
public interface IDataTypeConverter<TSource, TTarget>
{
/// <summary>
/// Type stored in the storage
/// </summary>
Type SourceType { get; }
/// <summary>
/// Type used in the application
/// </summary>
Type TargetType { get; }
TTarget Convert(TSource source);
TSource ConvertFrom(TTarget target);

object? Convert(object? source);
object? ConvertFrom(object? target);
}

public static class DataTypeConverters
{
public static Dictionary<Type, IDataTypeConverter> Converters { get; } = new();

/// <summary>
/// Returns an instance of the converter for the specified type.
/// </summary>
/// <param name="type">Type of the converter class</param>
/// <remarks>
/// Singletons of the converters are stored in the Converters dictionary.
/// </remarks>
public static IDataTypeConverter GetConverter(Type type)
{
// Checks if type is IDataTypeConverter
if (typeof(IDataTypeConverter).IsAssignableFrom(type))
{
// Checks if the converter is already registered
if (!Converters.ContainsKey(type))
{
// Creates an instance of the converter
var converter = (IDataTypeConverter)Activator.CreateInstance(type)!;
Converters.Add(type, converter);
}
return Converters[type];
}
else
{
throw new ArgumentException("The specified type is not a data type converter.");
}
}
static abstract IDataTypeConverter<TSource, TTarget> Instance { get; }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace FluentLauncher.Infra.Settings.Converters;
Expand All @@ -11,44 +13,44 @@ namespace FluentLauncher.Infra.Settings.Converters;
/// Converts a string to a type T using JSON serialization
/// </summary>
/// <typeparam name="T">Type used in the application</typeparam>
public class JsonStringConverter<T> : IDataTypeConverter
public class JsonStringConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T> : IDataTypeConverter<string, T?>
{
/// <inheritdoc/>
public Type SourceType => typeof(string);
private readonly JsonSerializerContext _serializerContext;

/// <inheritdoc/>
public Type TargetType => typeof(T);
public JsonStringConverter()
{
if (JsonStringConverterConfig.SerializerContext is null)
throw new InvalidOperationException("JsonSerializerContext is not available.");
_serializerContext = JsonStringConverterConfig.SerializerContext;
}

public static IDataTypeConverter<string, T?> Instance { get; } = new JsonStringConverter<T>();

/// <summary>
/// Returns the type used in the application
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public T? Convert(string json) => (T?)((IDataTypeConverter)this).Convert(json);

/// <inheritdoc/>
object? IDataTypeConverter.Convert(object? source)
public T? Convert(string json)
{
if (source is not string json)
return null;
if (json is null || JsonStringConverterConfig.SerializerContext is null)
return default;

return JsonSerializer.Deserialize(json, TargetType);
return (T?)JsonSerializer.Deserialize(json, typeof(T), _serializerContext);
}

/// <summary>
/// Returns the json string of an object
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
public string? Convert(T target) => (string?)((IDataTypeConverter)this).ConvertFrom(target);

/// <inheritdoc/>
object? IDataTypeConverter.ConvertFrom(object? target)
public string ConvertFrom(T? target)
{
if (target is not T)
return null;

return JsonSerializer.Serialize(target, TargetType);
return JsonSerializer.Serialize(target, typeof(T), _serializerContext);
}
}

public static class JsonStringConverterConfig
{
public static JsonSerializerContext? SerializerContext { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

</Project>
7 changes: 4 additions & 3 deletions infra/FluentLauncher.Infra.Settings/Interfaces.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 System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -53,15 +54,15 @@ public interface ISettingsStorage
/// <exception cref="KeyNotFoundException">Throws if the key is not found</exception>
/// <remarks>If a type converter is specified, the storage provider will be responsible for converting the type stored
/// <br/> into T when the item is accessed, and converting T into the type used in storage.</remarks>
T GetValue<T>(string path) where T : notnull;
T GetValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(string path) where T : notnull;

/// <summary>
/// Obtains the value of a setting item from the storage
/// </summary>
/// <param name="path">A unique path that locates the item in the storage</param>
/// <param name="type">Type of the value expected<br/>This may be used when dealing with special types such as arrays.</param>
/// <returns></returns>
object GetValue(string path, Type type);
//object GetValue(string path, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type);

/// <summary>
/// Sets a setting item to a new value in the storage
Expand All @@ -78,5 +79,5 @@ public interface ISettingsStorage
/// <param name="value">The new value</param>
/// <param name="type">Type of the value</param>
/// <returns></returns>
void SetValue(string path, object value, Type type);
//void SetValue(string path, object value, Type type);
}

This file was deleted.

5 changes: 0 additions & 5 deletions infra/FluentLauncher.Infra.Settings/SettingsAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ namespace FluentLauncher.Infra.Settings
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SettingItemAttribute : Attribute
{
/// <summary>
/// Key of the setting item
/// </summary>
[Obsolete]
public string? Key { get; init; }
/// <summary>
/// An optional converter to convert the value stored in the storage to the type of the property.<br/>If not specified, type casting will be used.
/// </summary>
Expand Down
Loading

0 comments on commit dff219f

Please sign in to comment.