Skip to content

AOT by default #1743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 30, 2024
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
31 changes: 8 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1617,36 +1617,21 @@ partial class MyResolver
This partial class, combined with at least one `[MessagePackObject]`-annotated type, will result in some members being added to your resolver class.
These members will _not_ be in your own source file, but they will be emitted during the compilation.
Visual Studio allows you to see these source generated files through a variety of means.
These members include the following properties `Instance` and `InstanceWithStandardAotResolver`.
You can use these members to serialize your object graph.
These members include the property `Instance`.
You can use this property to access the resolver for your specific types' formatters,
but you'll almost certainly need to combine this resolver with the standard resolver to get a fully functional serializer.
In fact the *default* resolver in this library will automatically discover this resolver in your assembly,
so you should only need to interact with this resolver directly for advanced scenarios.

Leveraging this resolver at runtime requires that you opt-in, which typically looks like this:

```cs
/// <summary>Options to use MessagePack with AOT-generated formatters.</summary>
private static readonly MessagePackSerializerOptions SerializerOptions = MessagePackSerializerOptions.Standard
.WithResolver(MyResolver.InstanceWithStandardAotResolver);

// Serialize and deserialize using the AOT option.
byte[] serialized = MessagePackSerializer.Serialize(value, SerializerOptions);
T after = MessagePackSerializer.Deserialize<T>(serialized, SerializerOptions);
```

Alternatively if you run in a highly-focused process, you can set the default options, and then use the simpler overloads to serialize.
Do NOT do this if you're in a shared process where other code may be using MessagePack with their own options.

```cs
MessagePackSerializer.DefaultOptions = SerializerOptions; // WARNING: mutates a static shared by all MessagePack users in the process
byte[] serialized = MessagePackSerializer.Serialize(value);
T after = MessagePackSerializer.Deserialize<T>(serialized);
```
Leveraging this resolver at runtime happens automatically by default,
since the `StandardResolver` includes the `SourceGeneratedFormatterResolver`
which discovers and your source generated resolver.

### Customizations

You can customize the generated source through properties on the `GeneratedMessagePackResolverAttribute`.

When exposing the generated resolver publicly, consumers outside the library should aggregate the resolver using its `Instance` property, which contains *only* the generated formatters.
The `InstanceWithStandardAotResolver` property is a convenience for callers that will not be aggregating the resolver with those from other libraries, since it aggregates built-in AOT friendly resolvers from the MessagePack library itself.

Two assembly-level attributes exist to help with mixing in your own custom formatters with the automatically generated ones:
- `MessagePackKnownFormatterAttribute`
Expand Down
7 changes: 7 additions & 0 deletions src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,13 @@ private bool CheckValidMessagePackFormatterAttribute(AttributeData formatterAttr
return null;
}

// Do not source generate the formatter for this type if the attribute opted out.
if (contractAttr.NamedArguments.FirstOrDefault(kvp => kvp.Key == Constants.SuppressSourceGenerationPropertyName).Value.Value is true)
{
// Skip any source generation
return null;
}

ObjectSerializationInfo info = new(isClass, isOpenGenericType, isOpenGenericType ? type.TypeParameters.Select(ToGenericTypeParameterInfo).ToArray() : Array.Empty<GenericTypeParameterInfo>(), constructorParameters.ToArray(), isIntKey, isIntKey ? intMembers.Values.ToArray() : stringMembers.Values.ToArray(), isOpenGenericType ? GetGenericFormatterClassName(type) : GetMinimallyQualifiedClassName(type), type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToDisplayString(), hasSerializationConstructor, needsCastOnAfter, needsCastOnBefore)
{
};
Expand Down
20 changes: 10 additions & 10 deletions src/MessagePack.SourceGenerator/MessagePackGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

var options = resolverOptions.Combine(customFormattedTypes).Combine(customFormatters).Select(static (input, ct) =>
{
AnalyzerOptions? options = input.Left.Left ?? new();
AnalyzerOptions? options = input.Left.Left ?? new() { IsGeneratingSource = true };

var formattableTypes = input.Left.Right;
var formatterTypes = input.Right.Aggregate(
Expand All @@ -48,12 +48,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var messagePackObjectTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
$"{AttributeNamespace}.{MessagePackObjectAttributeName}",
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (context, _) => (TypeDeclarationSyntax)context.TargetNode);
transform: static (context, _) => (ITypeSymbol)context.TargetSymbol);

var unionTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
$"{AttributeNamespace}.{MessagePackUnionAttributeName}",
predicate: static (node, _) => node is InterfaceDeclarationSyntax,
transform: static (context, _) => (TypeDeclarationSyntax)context.TargetNode);
transform: static (context, _) => (ITypeSymbol)context.TargetSymbol);

var combined =
messagePackObjectTypes.Collect().Combine(unionTypes.Collect());
Expand All @@ -65,28 +65,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
AnalyzerOptions options = s.Right;

if (!ReferenceSymbols.TryCreate(s.Left.Right, out ReferenceSymbols? referenceSymbols))
if (!ReferenceSymbols.TryCreate(s.Left.Right, out ReferenceSymbols? referenceSymbols) || referenceSymbols.MessagePackFormatter is null)
{
return default;
}

List<FullModel> modelPerType = new();
void Collect(TypeDeclarationSyntax typeDecl)
void Collect(ITypeSymbol typeSymbol)
{
if (TypeCollector.Collect(s.Left.Right, options, referenceSymbols, reportAnalyzerDiagnostic: null, typeDecl, ct) is FullModel model)
if (TypeCollector.Collect(s.Left.Right, options, referenceSymbols, reportAnalyzerDiagnostic: null, typeSymbol) is FullModel model)
{
modelPerType.Add(model);
}
}

foreach (TypeDeclarationSyntax typeDecl in s.Left.Left.Left)
foreach (var typeSymbol in s.Left.Left.Left)
{
Collect(typeDecl);
Collect(typeSymbol);
}

foreach (TypeDeclarationSyntax typeDecl in s.Left.Left.Right)
foreach (var typeSymbol in s.Left.Left.Right)
{
Collect(typeDecl);
Collect(typeSymbol);
}

return FullModel.Combine(modelPerType.ToImmutableArray());
Expand Down
42 changes: 12 additions & 30 deletions src/MessagePack.SourceGenerator/Transforms/ResolverTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,27 @@ public partial class ResolverTemplate : ResolverTemplateBase
/// </summary>
public virtual string TransformText()
{
this.Write("\r\n");
this.Write("\r\nusing MsgPack = global::MessagePack;\r\n\r\n[assembly: MsgPack::Internal.GeneratedA" +
"ssemblyMessagePackResolverAttribute(typeof(");
this.Write(this.ToStringHelper.ToStringWithCulture(CodeAnalysisUtilities.QualifyWithOptionalNamespace(ResolverName, ResolverNamespace)));
this.Write("), ");
this.Write(this.ToStringHelper.ToStringWithCulture(Version.Parse(ThisAssembly.AssemblyFileVersion).Major));
this.Write(", ");
this.Write(this.ToStringHelper.ToStringWithCulture(Version.Parse(ThisAssembly.AssemblyFileVersion).Minor));
this.Write(")]\r\n\r\n");
if (ResolverNamespace.Length > 0) {
this.Write("namespace ");
this.Write(this.ToStringHelper.ToStringWithCulture(ResolverNamespace));
this.Write(" {\r\n");
}
this.Write("\r\nusing MsgPack = global::MessagePack;\r\n\r\n/// <summary>A MessagePack resolver tha" +
"t uses generated formatters for types in this assembly.</summary>\r\npartial class" +
" ");
this.Write("\r\n/// <summary>A MessagePack resolver that uses generated formatters for types in" +
" this assembly.</summary>\r\npartial class ");
this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName));
this.Write(" : MsgPack::IFormatterResolver\r\n{\r\n\t/// <summary>An instance of this resolver tha" +
"t only returns formatters specifically generated for types in this assembly.</su" +
"mmary>\r\n\tpublic static readonly MsgPack::IFormatterResolver Instance = new ");
this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName));
this.Write(@"();

/// <summary>An instance of this resolver that returns standard AOT-compatible formatters as well as formatters specifically generated for types in this assembly.</summary>
public static readonly MsgPack::IFormatterResolver InstanceWithStandardAotResolver = new WithStandardAotResolver();

private ");
this.Write("();\r\n\r\n\tprivate ");
this.Write(this.ToStringHelper.ToStringWithCulture(ResolverName));
this.Write(@"()
{
Expand Down Expand Up @@ -91,26 +92,7 @@ static FormatterCache()
this.Write(this.ToStringHelper.ToStringWithCulture(CodeAnalysisUtilities.QualifyWithOptionalNamespace(x.FormatterName, x.Namespace)));
this.Write("();\r\n\t");
}
this.Write(@" default: return null;
}
}
}

private class WithStandardAotResolver : MsgPack::IFormatterResolver
{
public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
{
return FormatterCache<T>.Formatter;
}

private static class FormatterCache<T>
{
internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter = Instance.GetFormatter<T>() ?? MsgPack::Resolvers.StandardAotResolver.Instance.GetFormatter<T>();
}
}
}

");
this.Write("\t\t\t\tdefault: return null;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n");
if (ResolverNamespace.Length > 0) {
this.Write("}\r\n");
}
Expand Down
22 changes: 4 additions & 18 deletions src/MessagePack.SourceGenerator/Transforms/ResolverTemplate.tt
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>

using MsgPack = global::MessagePack;

[assembly: MsgPack::Internal.GeneratedAssemblyMessagePackResolverAttribute(typeof(<#= CodeAnalysisUtilities.QualifyWithOptionalNamespace(ResolverName, ResolverNamespace) #>), <#= Version.Parse(ThisAssembly.AssemblyFileVersion).Major #>, <#= Version.Parse(ThisAssembly.AssemblyFileVersion).Minor #>)]

<# if (ResolverNamespace.Length > 0) { #>
namespace <#= ResolverNamespace #> {
<# } #>

using MsgPack = global::MessagePack;

/// <summary>A MessagePack resolver that uses generated formatters for types in this assembly.</summary>
partial class <#= ResolverName #> : MsgPack::IFormatterResolver
{
/// <summary>An instance of this resolver that only returns formatters specifically generated for types in this assembly.</summary>
public static readonly MsgPack::IFormatterResolver Instance = new <#= ResolverName #>();

/// <summary>An instance of this resolver that returns standard AOT-compatible formatters as well as formatters specifically generated for types in this assembly.</summary>
public static readonly MsgPack::IFormatterResolver InstanceWithStandardAotResolver = new WithStandardAotResolver();

private <#= ResolverName #>()
{
}
Expand Down Expand Up @@ -73,19 +72,6 @@ partial class <#= ResolverName #> : MsgPack::IFormatterResolver
}
}
}

private class WithStandardAotResolver : MsgPack::IFormatterResolver
{
public MsgPack::Formatters.IMessagePackFormatter<T> GetFormatter<T>()
{
return FormatterCache<T>.Formatter;
}

private static class FormatterCache<T>
{
internal static readonly MsgPack::Formatters.IMessagePackFormatter<T> Formatter = Instance.GetFormatter<T>() ?? MsgPack::Resolvers.StandardAotResolver.Instance.GetFormatter<T>();
}
}
}

<# if (ResolverNamespace.Length > 0) { #>
Expand Down
1 change: 1 addition & 0 deletions src/MessagePack.SourceGenerator/Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ internal static class Constants
internal const string MessagePackAssumedFormattableAttributeName = "MessagePackAssumedFormattableAttribute";
internal const string MessagePackObjectAttributeName = "MessagePackObjectAttribute";
internal const string MessagePackUnionAttributeName = "UnionAttribute";
internal const string SuppressSourceGenerationPropertyName = "SuppressSourceGeneration";
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ public MessagePackObjectAttribute(bool keyAsPropertyName = false)
{
this.KeyAsPropertyName = keyAsPropertyName;
}

/// <summary>
/// Gets or sets a value indicating whether the source generator should <em>not</em>
/// generate a formatter for this type at compile-time.
/// </summary>
/// <remarks>
/// By default, source generators will generate a formatter for every type that is annotated with
/// this attribute to improve startup performance.
/// However if this leads to malfunctions during code generation or at runtime,
/// it can be disabled by setting this property to <see langword="true"/>.
/// When no precompiled formatter is found at runtime, the <c>DynamicObjectResolver</c>
/// will generate a formatter at runtime instead.
/// </remarks>
public bool SuppressSourceGeneration { get; set; }
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace MessagePack.Internal
{
/// <summary>
/// An assembly-level attribute that identifies the source-generated resolver for MessagePack for all types in this assembly.
/// </summary>
[AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
public class GeneratedAssemblyMessagePackResolverAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedAssemblyMessagePackResolverAttribute"/> class.
/// </summary>
/// <param name="resolverType">The type that implements <see cref="IFormatterResolver"/>.</param>
/// <param name="majorVersion">
/// The <see cref="Version.Major"/> component of the version of the generator that produced the resolver and formatters.
/// This may be used to determine whether the resolver and formatters are compatible with the current version of MessagePack.
/// </param>
/// <param name="minorVersion">
/// The <see cref="Version.Minor"/> component of the version of the generator that produced the resolver and formatters.
/// This may be used to determine whether the resolver and formatters are compatible with the current version of MessagePack.
/// </param>
public GeneratedAssemblyMessagePackResolverAttribute(Type resolverType, int majorVersion, int minorVersion)
{
ResolverType = resolverType;
MajorVersion = majorVersion;
MinorVersion = minorVersion;
}

public Type ResolverType { get; }

public int MajorVersion { get; }

public int MinorVersion { get; }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using MessagePack.Formatters;
using MessagePack.Internal;

namespace MessagePack.Resolvers
{
/// <summary>
/// A resolver that discovers formatters generated by <c>MessagePack.SourceGenerator</c>.
/// </summary>
public sealed class SourceGeneratedFormatterResolver : IFormatterResolver
{
/// <summary>
/// The singleton instance that can be used.
/// </summary>
public static readonly SourceGeneratedFormatterResolver Instance = new();

private static readonly ConcurrentDictionary<Assembly, IFormatterResolver?> AssemblyResolverCache = new();

private SourceGeneratedFormatterResolver()
{
}

/// <inheritdoc/>
public IMessagePackFormatter<T>? GetFormatter<T>() => FormatterCache<T>.Formatter;

private static class FormatterCache<T>
{
internal static readonly IMessagePackFormatter<T>? Formatter = FindPrecompiledFormatter();

private static IMessagePackFormatter<T>? FindPrecompiledFormatter()
{
IFormatterResolver? resolver = AssemblyResolverCache.GetOrAdd(typeof(T).Assembly, static assembly =>
{
if (typeof(T).Assembly.GetCustomAttributes<GeneratedAssemblyMessagePackResolverAttribute>().FirstOrDefault() is { } att)
{
return (IFormatterResolver?)att.ResolverType.GetField("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
}

return null;
});

return resolver?.GetFormatter<T>();
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading