-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion: Module Initializers (VS 16.8, .NET 5) #2608
Comments
Would it be better to just stick the attribute on a method that is meant to be used as module initializer? It would allow the method to be called directly, without going through the overhead of The attribute can be potentially allowed on multiple methods in the module and then all of them would be called in some order from the module initializer. |
I'd guess that one factor is the compiler overhead of the attribute search. If the only place to look is the metadata for the module itself, the check will be very quick (avoiding any overhead for the vast majority of folks who never use this feature). If the search has to check every method of every class in module, it will take a lot longer, slowing down compilation for everyone. That said, what about targeting the class itself - like this: [System.Runtime.CompilerServices.ModuleInitializerAttribute]
internal static class MyModuleInitializer
{
static MyModuleInitializer()
{
// put your module initializer here
}
} It's still a far larger search volume than the module metadata (so performance may be a concern), but a far smaller search volume than "every method". |
The feature as proposed is intended to be the simplest possible thing that exposes (and maps almost directly to) the underlying .NET feature. No more is needed for the use cases I've seen. If you did need to have multiple bodies of code run initializers, for example, you could implement that in the language-supported one (use reflection to search for types with your own special attribute and initialize them). I'm not worried about the "overhead" of calling |
Can that be done in a predictable, stable, way? Any precedent for compiler deciding order of execution? Are there any rules about what you can do inside a module constructor? Are you allowed to load other assemblies, make pInvoke calls, call async code, etc? If there are lots of rules I can imagine it being hard to enforce them in the compiler making this a somewhat dangerous feature warranting terms like "unsafe" or "dangerous". |
So, what is the terminology:
This sounds like the opposite of Obsolete :). |
@dsaf it's a trick used to stop older versions of the compiler using something it doesn't understand. Newer versions ignore that exact obsoletion string.
As for what a |
When using Fody, you need to have a method with a special name and signature. Obviously this can't be relied on here and I like the attribute specifying the type with the static constructor (short of special syntax). In that method you can just call whatever other initialization you need. So if for some reason you needed more than one module initializer, you could just refactor that into a single method that explicitly invokes the rest (no need for reflection). The only thing I dislike about the assembly level attribute specifying the type is that it's hidden. A developer looking at a static constructor later may not realize that this is supposed to be a module initializer. This is the same problem you end up having with Fody. Not that it's a bad thing, but you need to make sure you document that method with warnings about removing code that is being relied on as a module initializer. If we could specify the attribute on the class/initializer directly, then it becomes a bit more obvious. The only problem is that now you could potentially have more than one such method and the compiler would have to search for it. In that case perhaps more than one detected attribute could issue a compiler error, though I'm not sure how that would impact then build process (ie slow it down) |
I see two (complementing) solutions for this:
[module: System.Runtime.CompilerServices.ModuleInitializerAttribute(typeof(MyModuleInitializer))]
// Error CS####: No static constructor specified in type 'MyModuleInitializer' to be called as module initializer
internal static class MyModuleInitializer
{
//oops, someone removed this code, but now it can't build
} |
I'm with @Joe4evr , I think that if this is the way in which module initializers are implemented that the compiler should check and enforce that a static constructor exists on the class. However, if the compiler is making such a check I think it would be just as easy for the compiler to attempt to emit a static call to a well known static method rather than using a static class constructor: [module: System.Runtime.CompilerServices.ModuleInitializerAttribute(typeof(MyModuleInitializer))]
internal static class MyModuleInitializer
{
internal static void Initialize() {
// do stuff here
}
} It would be a compiler error if that method is not resolved by the compiler at compile time. In this case the compiler would only have to emit a static call to that method:
In my naive opinion this seems about as difficult as going the static constructor route, assuming that the compiler would check that such a constructor exists. |
This is good to see. One thing I'd really like to see changed, though: make module constructors run eagerly. Right now, module initializers, like class static constructors, run lazily; at some point after a module has loaded but before any code from that module runs, the module initializer will run. Unfortunately, this adds unnecessary coupling and complication to one of the best scenarios for module initializers: plugins. Ideally, you could load a plugin assembly, the module initializer would run, and it would register the plugins with the plugin system (through a known method in a dependent assembly.) But with lazy initialization, you can't do that; you need the plugin system to "reach into" the module somehow in order to activate it, probably with Reflection, and by that point there's no point in having a module initializer at all; you just use Reflection to search for plugins to register. With static constructors, lazy initialization is needed because there's no good way to resolve dependency order eagerly. But with CLR assemblies, we have a well-established dependency order already built into the fundamental concept of assemblies, so that limitation doesn't apply. So it seems to me there's no good reason not to make it eager. |
Seems like something you'd need to take to CoreCLR as the language currently can't influence how the initializers would behave. |
The overhead is in that
Partial classes solved this problem.
There are no special rules. You can do anything in module static constructor as what you would do in regular static constructor. DLLMain != module constructor. |
Is it possible to prioritize this in the 8.1 release? |
@mjsabby There are no current plans for an 8.1 release. |
@jkotas Rather than using reflection, the compiler could implement this by injecting a |
Yes, that would work great and address all my concerns. |
I would vote against that part. Just like we only get one |
One possible use for the multi-module initializers approach is for generated code. Generated code often needs a way to 'register' itself at startup. Having multiple module initializers would allow the generated code to add its own initializer that would perform any specific initialization logic it needs, without the need for the user to manually add an explicit call to it. |
@Grauenwolf If we're going to add this, it makes sense to look elsewhere for similar features and see what works and what doesn't. Probably the best analogue comes from .NET architect Anders Hejlsberg's previous project, Delphi. It allows you to put an That last bit (getting dependency order right) is based on a specific, restrictive property of Delphi's compilation that doesn't apply to C#, so we can't expect to be able to copy that successfully. But the basic principle of putting your initialization code together with the code it's initializing, and then having the compiler gather them into a single overall initializer, is a sound one. What are the alternatives? I can only think of two, and both are bad:
Both of these are significantly worse than having the compiler set it up for you. As for ordering, most of the time it won't be necessary, especially since static constructors will take care of most of the cases of initialization-time dependencies between classes. But for cases where it is, there should be some way to specify an explicit ordering, and any initializer routines that don't have an ordering set will run (in an undefined order) after all the ones that do. |
One example is NUnit's VSTest adapter. We would like it to contain more dependencies as resources which requires us to hook up AssemblyResolve/Resolving event handlers before any of the types in the NUnit VSTest adapter assembly are loaded. However, VSTest loads the assembly and scans all types looking for implementations of VSTest interfaces. Without a module initializer, there's no predictable place where we can hook up AssemblyResolve/Resolving in time to prevent a type load exception. Giving every type in the assembly a static constructor seems problematic. Prototyping shows that module initializers solve this neatly. |
@jnm2 Unfortunately a module initializer is also not a safe solution for that specific case. We tried that already. The module initializer is invoked when the first type of the assembly is constructed. But Nunit analyzes the assembly on reflection level, which causes the dependencies to be resolved, but no type is constructed. |
@TFTomSun Thanks, that was helpful. You are correct. I was able to build examples on .NET Core and .NET Framework which show that Assembly.GetTypes() does not trigger the module initializer. In this case the problem I'm trying to solve is that that VSTest analyzes the NUnit adapter assembly on a reflection level. (VSTest loops through Assembly.GetTypes() looking for VSTest interface implementations.) Since appdomains are not a thing in .NET Core, I think this means we should just ask VSTest to run this code prior to examining assemblies using reflection: RuntimeHelpers.RunModuleConstructor(adapterAssembly.ManifestModule.ModuleHandle); This is still a neat solution compared to anything else I'm aware of that we could do. |
@AlekseyTs brought up a good question. The concept of a module initializer does not appear in ECMA-335. Does anyone know what happened to it? https://blogs.msdn.microsoft.com/junfeng/2005/11/19/module-initializer-a-k-a-module-constructor/ lists what appears to be a section that was intended to follow the Type Initializer section:
|
This probably explains why it's not in ECMA-335.
But are any guarantees actually documented anywhere? |
This is the only mention of module initializers that I can find in CLR docs:
|
Given that a lot of us have been dependent on module initializers working for a number of years, and the pain that @gafter wants to address is real, is the next step to submit a PR to add documentation of it to https://github.com/dotnet/runtime/tree/master/docs/design/features? |
Yes, I think it would be fine to start a ECMA-335 Augments doc where we will collect additions to ECMA-335 we would like to do, but do not have a good way to do currently. We have number of those. |
@jkotas Awesome! Should I start with opening an issue so that someone else can start the document, or should I start with a PR to add e.g. |
You are welcomed to submit a PR to get it started. |
Actually, we have a doc like already: https://github.com/dotnet/runtime/blob/master/src/libraries/System.Reflection.Metadata/specs/Ecma-335-Issues.md . We should build on top of it. Rename it ...Augments, and maybe move this whole directory to a more discoverable place, e.g. docs\design\specs . |
See also #2486
Although the .NET platform has a feature that directly supports writing initialization code for the assembly (technically, the module), it is not exposed in C#. This is a rather niche scenario, but once you run into it the solutions appear to be pretty painful. I have seen reports of a number of customers (inside and outside Microsoft) struggle with the problem, and there are no doubt more undocumented cases.
I suggest that we would add a tiny feature to support this without any explicit syntax, by having the C# compiler recognize a module attribute with a well-known name, like the following:
You would use it like this
and the C# compiler would then emit a module constructor that causes the static constructor of the identified type to be triggered:
Open issues
Alternative Approaches
There are a number of possible ways of exposing this feature in the language:
1. Special global method declaration
A module initializer would be provided by writing a special kind of method in the global scope:
This gives the new language construct its own syntax. However, given how rare and niche the scenario is, this is probably far too heavyweight an approach.
2. Attribute on the type to be initialized
Instead of a module-level attribute, perhaps the attribute would be placed on the type to be initialized
With this approach, we would either need to reject a program that contains more than one application of this attribute, or provide some policy to define the ordering in case it is used multiple times. Either way, it is more complex than the original proposal above.
3. Attribute on a static method to be called
Instead of a module-level attribute, perhaps the attribute would be placed on the method to be called to perform the initialization
As in the previous approach, we would either need to reject a program that contains more than one application of this attribute, or provide some policy to define the ordering in case it is used multiple times. Either way, it is more complex than the original proposal.
4. Original Proposal
The original proposal naturally prevents the user from declaring more than one initializer class without adding any special language rules to accomplish that. It is very lightweight in that it uses existing syntax and semantic rules for attributes.
LDM notes:
The text was updated successfully, but these errors were encountered: