stride3d / stride

Stride (formerly Xenko), a free and open-source cross-platform C# game engine.
https://stride3d.net
MIT License
6.62k stars 957 forks source link

Use C# [ModuleInitializer] #1836

Open cNoNim opened 1 year ago

cNoNim commented 1 year ago

Is your feature request related to a problem? Please describe. C# 9 has support of [ModuleInitializer] https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/proposals/csharp-9.0/module-initializers. And as part of https://github.com/stride3d/stride/issues/1835 it's good idea to migrate to them.

Describe the solution you'd like Use C# [ModuleInitializer] and get rid of ModuleInitializerProcessor stuff in AssemblyProcessor.

Describe alternatives you've considered C# does not support order of initialization. But don't sure in disadvantages of it.

Ethereal77 commented 1 year ago

I experimented some time ago with replacing the Stride's ModuleInitializer with that of .NET and C# 9, but as you have seen, Stride specifies sometimes an initialization order that the AssemblyProcessor must take into account when weaving all of them into the true module initializer. I suppose that order parameter must be important for some things (like those of the Asset plugins for example).

I created a very simple Source Generator that gathered all methods marked with the Stride's attribute and generated a module initializer respecting the order. I can look for it if you are interested.

My only concern back then was that some module initializers make use of other things that are also resolved or generated by the AssemblyProcessor, like serializers for example, and SourceGens are executed with no knowledge of any other SourceGen nor way of communicating between them.

cNoNim commented 1 year ago

@Ethereal77, yes.

And after some experiments, I found yet another issue. C# [ModuleInitializer] also generate Module static constructor at compile time. And [ModuleInitializer] generated by AssemblyProcessor not called.

And I also think that the big problem is that source code generators cannot operate with artifacts that are produced by other generators. In fact, we will get the same problem if we try to transfer everything to SourceGen.

At the moment, I can finalize the solution and expand the module constructor generated by the compiler. And even maintain order in this decision. But it seems that this will not be a step towards the goal of getting rid of the AssemblyProcessor.

Ethereal77 commented 1 year ago

What the Stride's [ModuleInitializer] does is composing a list of init functions that are called in order by the module static constructor. The .NET / C# [ModuleInitializer] also does that, but without respecting any ordering (the order is undetermined).

My experiment does exactly the same AssemblyProcessor does today with a Source Generator.

Note the problem with SourceGens is not that they can't operate with artifacts of other SourceGens. They indeed can. If a generator A creates a class A and generator B creates a method that does new A() , when the compilation is invoked, the types will be generated and the compilation will succeed. The problem however is neither of the two will know about each other nor be able to communicate or know what the other is generating while they are executing.

Ethereal77 commented 1 year ago

Another thing I thought back when I was experimenting with this is that for all these little generated things that have to know each other or depends on each other that now are generated by the AssemblyProcessor (like serialization, animation engine routines and accessors, etc), at least some of them would need to be generated at once by a single SourceGen (like the current AssemblyProcessor does) or generated somehow knowing the needed pieces will be there (done by other generators) even if no communication can happen.

As an example (so it is better understood), think all [AssemblyScan] generating a static void RegisterGeneratedAssemblyScan() { ... }, the serializers generating a static void RegisterGeneratedSerializers() { ... }, etc., and the [ModuleInitializer] generating a function like this:

[ModuleInitializer] // The .NET one
file static void GeneratedModuleInit()
{
    RegisterGeneratedAssemblyScan();
    RegisterGeneratedSerializers();
    // ...
}

Even though it is not guaranteed those methods are not empty.

Just an idea. I don't know if that would work.

xen2 commented 1 year ago

Thanks for the research!

To start with, I would be totally fine if it was all generated by one (or two) source generator, just like the assembly processor is organized today. Then we can see if we can better separate some stuff, but as you said, in some cases we might not be able to. Note that even if it's a single code generator, it can generate multiple separate files.

Also, if later we still want to have a little bit of reusability/extensibility for plugins, we can do that inside that source generator with interfaces (i.e. something similar to IAssemblyDefinitionProcessor). We would still expose a single Roslyn source generator, but inside it we can still have a plugin-based approach.

BldgBlocks commented 1 year ago

Hi! I was browsing and found this is relevant to me. It seems you have some knowledge on an undocumented feature.

I spent some time today trying to figure out how this ModuleInitializerAttribute works and is handled, and fill in the missing documentation. I am close but could use some details as it sounds like this custom implementation will remain in use and there is a little more to it than I thought.

/// <summary> /// A module initializer is designed to easily implement one time code to run when an assembly is loaded without /// the need for any explicit calls or static constructor usage. /// There are no limitations on what code is permitted in a module initializer. /// Module initializers are permitted to run and call both managed and unmanaged code /// and are guaranteed to run before any static fields or methods are accessed. /// </summary> /// <remarks> /// The <see cref="ModuleInitializerAttribute"/> is recognized and processed by the Common Language Runtime (CLR) /// during assembly loading, no matter where it is declared. It was formally introduced as a C# 9 language feature /// as part of the System.Runtime.CompilerServices namespace. /// </remarks> [AttributeUsage(AttributeTargets.Method)] public class ModuleInitializerAttribute : Attribute

Ethereal77 commented 1 year ago

@BldgBlocks That is the documentation for the .NET version of the [ModuleInitializer] attribute.

The Stride one is exactly the same (functionally identical), with the added bonus that you can define an order in the form of an arbitrary integer, and the methods you annotate with that attribute are ordered by that order value you specified and are invoked in sequence. This is done at assembly processing time (after compilation). That is the main difference between the .NET and the Stride attribute.

Also, the .NET one is resolved by the C# Roslyn compiler at compile-time. The Stride one is "resolved" post-compilation by the Assembly Processor (that analyzes and modifies the compiled IL of the assembly). In the future, it hopefully will be processed by a source generator instead.

Ethereal77 commented 1 year ago

As promised, I've uploaded my little experiment to GitHub:

InitializeAtStartup-SourceGen

To avoid confusion with the .NET's [ModuleInit] attribute, in the experiment I've named the attibute [InitializeAtStartup]. It accepts an order parameter, and the SourceGen will weave the calls in that order.

IXLLEGACYIXL commented 1 year ago

As promised, I've uploaded my little experiment to GitHub:

InitializeAtStartup-SourceGen

To avoid confusion with the .NET's [ModuleInit] attribute, in the experiment I've named the attibute [InitializeAtStartup]. It accepts an order parameter, and the SourceGen will weave the calls in that order.

the link is invalid , its a 404, is it private?

Ethereal77 commented 1 year ago

It was. Sorry.

Ethereal77 commented 1 year ago

Btw, the experiment is using the old-style Source Generators. It should be implemented as an iterative source generator for better efficiency.

IXLLEGACYIXL commented 1 year ago

Btw, the experiment is using the old-style Source Generators. It should be implemented as an iterative source generator for better efficiency.

i can help you with that, when diagnostics is merged we can add source generators to it