Skip to content

Conversation

@AArnott
Copy link
Collaborator

@AArnott AArnott commented Oct 11, 2024

NativeAOT support in MessagePack would be awesome for performance and to appeal to the .NET 9 crowd marching over from use of BinaryFormatter.

@AArnott AArnott added this to the v3.0 milestone Oct 11, 2024
@AArnott
Copy link
Collaborator Author

AArnott commented Oct 11, 2024

This doesn't work today. Besides having warnings that remain, the various attributes that "allow" portions of the library to include dynamic code (either Ref.Emit or closing generic types at runtime) or that require unreferenced code is viral and bubbles up the most of the library. It's unclear whether a path to success exists.

I have a meeting scheduled for next week with the .NET folks who help with such migrations and will report back on what I learn.

@AArnott AArnott linked an issue Oct 11, 2024 that may be closed by this pull request
displayName: 🧪 dotnet test
condition: and(succeeded(), ${{ parameters.RunTests }})

- script: dotnet publish -c Release -r ${{ parameters.osRID }}-x64 -warnaserror

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need to pass -r (or -c for that matter, as Release is the default for publish). The RID is calculated based on the current machine.

Copy link
Collaborator Author

@AArnott AArnott Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping the -r switch produces a build error:

error NETSDK1191: A runtime identifier for the property 'PublishAot' couldn't be inferred. Specify a rid explicitly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that happen all the time? Or only for specific operating systems?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It happened on Windows when I tested it locally. That's as far as I got.

Comment on lines +29 to 30
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = Constants.AvoidDynamicCodeRuntimeCheck)]
private static readonly IFormatterResolver[] Resolvers = DynamicAssembly.AvoidDynamicCode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbomer - Is it possible to add the FeatureGuardAttribute to PollyfillAttributes.cs and apply it to DynamicAssembly.AvoidDynamicCode? That way we wouldn't have to suppress these warnings?

I guess it may not be possible yet unless we are using a .NET 9 ILC and Roslyn Analyzers. Thoughts here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could polyfill and add FeatureGuard if you made it a property and inverted it (UseDynamicCode for example). That would guarantee that it gets substituted to false in an AOT'd app that targets net9.0 or above. (And would also silence analyzer warnings if MessagePack were built for net9.0).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it only silences analyzer warnings when targeting net9.0, that won't work since I'm also targeting net8.0 and need to suppress warnings there anyway.

{
this.Key = key;
this.SubType = Type.GetType(subType, throwOnError: true);
this.SubType = Type.GetType(subType, throwOnError: true) ?? throw new Exception("SubType not found");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since throwOnError is true, when would the new code ever get invoked?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps never. But I added it to suppress a warning. Probably because Type.GetType(string, bool) is documented as returning Type?.

#if NET8_0_OR_GREATER
[RequiresUnreferencedCode("The type named in 'subType' may be trimmed.")]
#endif
public UnionAttribute(int key, string subType)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to mark the subType with a DynamicallyAccessedMembers attribute, which will tell the trimmer that the string is a Type. And so any Type that is passed in on this string will be preserved.

I'm not sure on exactly which DynamicallyAccessedMemberTypes to use, it may work with just .None, or you may need to add PublicParameterlessConstructor. @sbomer and @agocke would know for sure. PublicParameterlessConstructor would be the safest to ensure it is kept.

Suggested change
public UnionAttribute(int key, string subType)
public UnionAttribute(int key, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string subType)

This would allow you to remove the RequiresUnreferencedCode attribute on this constructor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None didn't work. But PublicParameterlessConstructor worked. There's no guarantee that such a constructor is declared on this type, so I hope it works either way.

public class MessagePackFormatterAttribute : Attribute
{
#if NET8_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.Interfaces)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you also need this attribute on the Type parameters passed into the constructors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable. I wonder why the analyzer missed that.

Deserializers.TryAdd(typeof(object), _ => (object p1, ref MessagePackReader p2, MessagePackSerializerOptions p3) => new object());
}

#if NET8_0_OR_GREATER

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) Why are these are sometimes guarded with #if NET8 and sometimes not?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, just because I didn't switch to polyfill style attributes until I was midway through the work, and I missed removing the #if from some places. I removed it from here.

{
// Note:This implementation is 'not' fastest, should more improve.
public sealed class EnumAsStringFormatter<T> : IMessagePackFormatter<T>
public sealed class EnumAsStringFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T> : IMessagePackFormatter<T>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in net9 we changed our trimming rules such that when T is guaranteed to be an Enum (like it is here), it no longer warns when you try to get the fields of the enum type. I don't think adding this attribute is causing you problems now, but just an FYI. See dotnet/runtime#100814 for more info.

@glennawatson
Copy link

I'd imagine you might want to go the way of System Text Json and provide a source generator in the future and basically unroll the serialization loop?

Would that be something you'd be interested if I can help contribute to?

I think this PR goes a long way but for proper NativeAOT support in the future having a source generator that reads/writes direclty might be useful.

@AArnott
Copy link
Collaborator Author

AArnott commented Nov 8, 2024

@glennawatson Thanks for volunteering. We have a source generator. And it's very complex, and also relies on some runtime resolvers that are tainted with dynamic dependencies. I suspect we could eventually achieve NativeAOT safety (certified by satisfied analyzers) but it will be so much work that I started fresh at https://github.com/aarnott/nerdbank.messagepack. It is very nearly feature complete and is NativeAOT ready from the ground up. Much simpler code and much simpler to use, and still very fast. I'd love to hear what you think.

@AArnott
Copy link
Collaborator Author

AArnott commented Dec 3, 2024

Aborting this effort. Explanation here: #1997 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NativeAOT assertion for v3

5 participants