Friday, April 24, 2009

Static ctors in D.NET (Part 2)

D allows multiple static constructors per class (all sharing the same signature). For example, the following code is legal:

version(D_NET)
{
import System;
alias Console.WriteLine println;
}
else
{
import std.stdio;
alias writefln println;
}
class A
{
static int i = 42;
static this()
{
println("static A.this 1");
}
static this()
{
println("static A.this 2");
}
}
void main()
{
println(A.i);
}

The program prints:

static A.this 1
static A.this 2
42

Because IL does not allow duplicate methods with the same signature, instead of mapping static constructors directly to .cctor methods, my compiler generates one .cctor per class (where needed) that makes function calls to the static this() constructors. The .cctor is not called if the class is never referenced -- this behavior is different from the native Digital Mars D compiler. If we comment out the one line in main, it will still print the constructor messages in native mode, but not under the .net compiler.

D classes may also have one or more static destructors, as in this example:

class A
{
static int i = 42;
static this()
{
println("static A.this 1");
}
static this()
{
println("static A.this 2");
}
static ~this()
{
println("static A.~this 1");
}
static ~this()
{
println("static A.~this 2");
}
}


Unlike with the class constructors, there is no special IL method to map static destructors to. My compiler supports them with AppDomain.ProcessExit event handlers, registered in reverse order of their lexical occurrences. IL allows non-member .cctor methods, and the compiler takes advantage of this feature to synthesize code that registers the static destructors as ProcessExit handlers.

It is interesting to observe that the global .cctor does reference the class when it constructs the event handler delegates:

.method static private void .cctor()
{
// register static dtor as ProcessExit event handler
call class [mscorlib]System.AppDomain [mscorlib]System.AppDomain::get_CurrentDomain()
ldnull
ldftn void 'example.A'::'_staticDtor4'(object, class [mscorlib]System.EventArgs)
newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
callvirt instance void [mscorlib]System.AppDomain::add_ProcessExit(class [mscorlib]System.EventHandler)
// register static dtor as ProcessExit event handler
call class [mscorlib]System.AppDomain [mscorlib]System.AppDomain::get_CurrentDomain()
ldnull
ldftn void 'example.A'::'_staticDtor3'(object, class [mscorlib]System.EventArgs)
newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
callvirt instance void [mscorlib]System.AppDomain::add_ProcessExit(class [mscorlib]System.EventHandler)
ret
}

This means that the .cctor of the class will be called, even if no user code ever references it.

In addition to class static constructors and destructors, D also features module static constructors and destructors. These are expressed as non-member functions with the signature static this() and static ~this(), respectively.
For example:

//file b.d
import a;
version(D_NET)
{
import System;
alias Console.WriteLine println;
}
else
{
import std.stdio;
alias writefln println;
}

static this()
{
println("module B");
map["foo"] = "bar";
}
static this()
{
println("boo");
}
static ~this()
{
println("~boo");
}

//file a.d
version(D_NET)
{
import System;
alias Console.WriteLine println;
}
else
{
import std.stdio;
alias writefln println;
}

string map[string];

static this()
{
println("module A");
}
static ~this()
{
println("~module A");
}

void main()
{
foreach (k, v; map)
{
version(D_NET)
{
Console.WriteLine("{0} -> {1}".sys, k, v.sys);
}
else
{
writefln("%s -> %s", k, v);
}
}
}

It is noteworthy that regardless in which order the two files above are compiled the resulting program prints the same output:

module A
module B
boo
foo -> bar
~boo
~module A

The explanation lay in the D language rules: if a module B imports a module A, the imported module (A) must be statically initialized first (before B).

As in the case of static constructors and destructors for classes, the compiler uses the global, free-standing .cctor method to stick calls to module ctors and register ProcessExit events that call the module's static dtors.


Thanks to BCSd for prompting this post with his comment and code sample.

4 comments:

BCSd said...

Am I reading that correctly in that I can only be sure that static constructors will be called if the class also has a static destructor?

If that is the case, how about make that non-member .cctor touch any class with a static constructor even if it has no destructor. Then they run no mater what.

Cristache said...

The static ctors are called if a) the class also has a static dtor or b) the class is referenced (either by invoking any of its methods, or accessing its static members).

The fact that the static ctors of a class that is never actually used are not invoked comes as an optimization. The only reason I would want to do what you are suggesting is to ensure 100% compatibility with the native compiler, but I do not see any benefits in that.

BCSd said...

I have a few project in mind where a static constructor is used to inject code into the main program. That way the nothing needs be done except link the a module to effect somthing:

module main;

void function(Context)[char[]] actions;

void main(char[][] args)
{
Context cx;
foreach(arg; args[1..$])
if(auto dg = arg in actions)
(*dg)(cx);
}

/////
module plugin;
import main;

static this()
{
actions["-plugin"] = function void(Context cx){somthing();};
}

static ~this(){} // just to make things work???

Cristache said...

I think that there is a small confusion here: what I said about static constructors being called only when there is a reference applies to CLASSES.

The static constructor of a MODULE is always executed. Your example will work without the need for a static module dtor.