Skip to content
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

Add support for module initializers #992

Draft
wants to merge 5 commits into
base: draft-v9
Choose a base branch
from

Conversation

RexJaeschke
Copy link
Contributor

  1. Note that the requirements bullet list in the description of "The ModuleInitializer attribute" refers to the grammar rule parameter_list, which is defined by the V9 feature "Records."

  2. We say, "A module may have multiple initializers, which are called in an implementation-defined order." However, we do not say anything about their order w.r.t execution of static constructors. Should we? My (limited) testing showed that a static constructor in a type T executed before any module initializers in that type.

@RexJaeschke RexJaeschke added the type: feature This issue describes a new feature label Nov 14, 2023
@RexJaeschke RexJaeschke added this to the C# 9.0 milestone Nov 14, 2023
@RexJaeschke RexJaeschke marked this pull request as draft November 14, 2023 22:58
- A *return_type* of `void`.
- No *type_parameter_list*.
- Not be declared inside a *class_declaration* having a *type_parameter_list*.
- Be accessible from the containing module (that is, have an access modifier `internal` or `public`).
Copy link
Contributor

Choose a reason for hiding this comment

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

Does that allow protected internal?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it does. The rules were chosen to describe "callable from <Module>..cctor while following .NET accessibility rules." You can simulate it by asking whether the method would be accessible from a regular C# or .NET IL from a regular top-level class's static constructor.

Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Feb 9, 2025

Choose a reason for hiding this comment

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

I see, protected internal is not an access modifier; it is a sequence of two access modifiers.

Copy link
Contributor

Choose a reason for hiding this comment

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

But how about:

public class C {
    private class D {
        [System.Runtime.CompilerServices.ModuleInitializer]
        public static void M() {
        }
    }
}

The declaration of C.D.M() has the public access modifier but SharpLab.io tells me "error CS8814: Module initializer method 'M' must be accessible at the module level". So the wording in this PR does not seem quite accurate.

The wording might have to be changed again in the future when C# 11 file-local types are specified.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a really good point.

@@ -860,6 +861,22 @@ For invocations that occur within field or event initializers, the member name u

For invocations that occur within declarations of instance constructors, static constructors, finalizers and operators the member name used is implementation-dependent.

### §module-init-attr The ModuleInitializer attribute

The attribute `ModuleInitializer` is used to mark a method as a ***module initializer***. Such a method is called during initialization of the containing module. A module may have multiple initializers, which are called in an implementation-defined order.
Copy link
Contributor

@jnm2 jnm2 Feb 9, 2025

Choose a reason for hiding this comment

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

Not sure how much you want to repeat, but from https://github.com/dotnet/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#module-initializer:

Module Initializer

All modules may have a module initializer. A module initializer is defined as the type initializer (§ II.10.5.3) of the <Module> type (§ II.10.8).

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.

Module Initialization Guarantees

In addition to the guarantees that apply to all type initializers, the CLI shall provide the following guarantees for module initializers:

  1. A module initializer is executed at, or sometime before, first access to any static field or first invocation of any method defined in the module.

  2. A module initializer shall run exactly once for any given module unless explicitly called by user code.

  3. No method other than those called directly or indirectly from the module initializer will be able to access the types, methods, or data in a module before its initializer completes execution.

Copy link
Contributor

@jnm2 jnm2 Feb 9, 2025

Choose a reason for hiding this comment

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

This part specifically is interesting:

A module initializer is executed at, or sometime before, first access to any static field or first invocation of any method defined in the module.

Because there is no guarantee that the runtime will execute the initializer before using reflection to access types in the module, and in fact it does not, and this means that libraries can't use module initializers to hook up to AppDomain.AssemblyResolve in time to load assemblies required by such reflection. (Examples of wanting to do this is when assemblies are stored in nonstandard locations such as embedded resources or other places on disk, or when you need to handle assembly versioning differences and you can't use binding redirects. What all of these have in common are scenarios where a .NET library is a plugin to a third-party application.)

@jnm2
Copy link
Contributor

jnm2 commented Feb 9, 2025

  1. We say, "A module may have multiple initializers, which are called in an implementation-defined order." However, we do not say anything about their order w.r.t execution of static constructors. Should we? My (limited) testing showed that a static constructor in a type T executed before any module initializers in that type.

This is a great point. It seems like any combo of compiler and .NET runtime implementation would imply calling the containing class's .cctor before the initializer method:

public class Outer
{
    [ModuleInitializer]
    public static void Test()
    {
        Console.WriteLine("In module initializer"); // This runs second
    }

    static Outer()
    {
        Console.WriteLine("In Outer..cctor"); // This runs first
    }
}

Otherwise, the Test method would be able to observe that Outer..cctor did not run, e.g. by the field initializers not having run.

I believe this is implied by knowing that the compiler implements this by inserting IL call opcodes into the module initializer, and knowing what such an opcode means in the runtime.

To be clear, the static constructor(s) that are running are running as part of the call to the annotated module initializer method.

- No *parameter_list*.
- A *return_type* of `void`.
- No *type_parameter_list*.
- Not be declared inside a *class_declaration* having a *type_parameter_list*.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not within a struct_declaration having a type_parameter_list, either.

I think it's better to specify this as a semantic constraint "Not be a member of a generic type" than as a grammar constraint.

Comment on lines +870 to +878
A module initializer shall have the following characteristics:

- The *method_modifier* `static`.
- No *parameter_list*.
- A *return_type* of `void`.
- No *type_parameter_list*.
- Not be declared inside a *class_declaration* having a *type_parameter_list*.
- Be accessible from the containing module (that is, have an access modifier `internal` or `public`).
- Not be a local function.
Copy link
Contributor

Choose a reason for hiding this comment

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

In C# 11, this spec will also have to disallow virtual and abstract, which otherwise become allowed in static methods of interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature This issue describes a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants