TL;DR: Yes, but not semantically, and only for non-built-in value types.
The following is based on my own reverse engineering of the internal workings of a CLR application.
The answers provided are not entirely correct, in fact, quite misleading.
This is an interesting one. It depends.
Built in types (Supported by the VES directly) like ints, floats, etc. are stored raw at the address of the static variable.
But interestingly, non-built in types like System.Decimal, System.DateTime, and user defined value types are boxed.
But interestingly, they actually get kinda, sorta... double boxed. Imagine:
public struct MyStruct
{
public int A;
}
public static class Program
{
public static MyStruct X;
public static void Main()
{
Program.X.A = 1337;
Program.DoIt();
}
public static void DoIt()
{
Program.PrintA(Program.X);
Program.PrintType(Program.X);
}
private static void PrintType(object obj)
{
Console.WriteLine(obj.GetType().FullName);
}
public static void PrintA(MyStruct myStruct)
{
Console.WriteLine(myStruct.A);
}
}
Now, this will work as you'd expect, MyStruct will be boxed for PrintType, and not boxed for PrintA.
However, Program.X does not actually contain the MyStruct instance directly like it does in an instance variable or a local variable. Instead it contains a reference to it on the heap, where the instance exists as an object with an object header and all.
As mentioned initially, this does not apply to built in types. So if you have a static variable containing an int, the static variable will take up 4 bytes. But if you have a static variable of a user defined type, eg. struct IntWrapper{public int A;}
, then the static variable will take up 4 bytes in a 32-bit process and 8 bytes in a 64-bit process to store the address of a boxed version of the IntWrapper struct, where it takes up 8 bytes in a 32-bit process and 12 bytes in a 64-bit process (4/8 bytes for the object header pointer and 4 bytes for the int), ignoring any potential padding.
However, semantically it works like you'd expect. When calling PrintA(Program.X), the program will copy the struct part (the data after the object header) in the object pointed to by Program.X and pass that to PrintA.
When calling PrintType(Program.X) it indeed boxes the instance. The code creates a new MyStruct object with an object header, then copies the A field from the object referenced by Program.X into the newly created object, and that object is then passed to PrintType.
In summary, Program.X contains the address of a boxed MyStruct (If we define boxing as converting a value type to a reference type), but will still box (or clone) that object as if it was a value type, so the semantics remain the same as if it was stored in the static variable directly as a value type.
Like I said, I'm not sure why they're doing it this way, but they are.
I've included the JIT'ed disassembly of the C# code above and commented it.
Note, I've come up with all the names in the disassembly.
A comment on the calls: All calls to managed methods happen through pointers. On the first call, the pointer points to the code that takes care of JIT compiling the method. After JIT compiling, the pointer is replaced with the address of the JIT compiled code, so any subsequent calls are fast.
Program.Main:
MOV EAX, DWORD PTR DS:[<Program.X>] ; Move the address stored in static variable Program.X into register EAX.
MOV DWORD PTR DS:[EAX + 4], 539h ; Set field at offset 4 (Offset 0 is the object header pointer) to 1337.
CALL DWORD PTR DS:[<Program.DoIt Ptr>] ; Call Program.DoIt.
RET ; Return and exit the program.
Program.DoIt:
PUSH EBP ; Function prologue.
MOV EBP, ESP ; Function prologue.
MOV EAX, DWORD PTR DS:[<Program.X>] ; Move the address stored in static variable Program.X into register EAX.
MOV ECX, DWORD PTR DS:[EAX + 4] ; Copy the struct part (the dword after the object header pointer) into ECX (first argument (this)), essentially an unboxing.
CALL DWORD PTR DS:[<Program.PrintA Ptr>] ; Call Program.PrintA.
; Here, the MyStruct stored in the static value is cloned to maintain value semantics (Essentially boxing the already boxed MyStruct instance).
MOV ECX, <MyStructObjectHeader> ; Boxing for PrintType: Copy the address of the object header for MyStruct into ECX (First argument).
CALL <CreateObject> ; Boxing for PrintType: Create a new object (reference type) for MyStruct.
MOV ECX, EAX ; Copy the address of the new object into ECX (first argument for Program.PrintType).
MOV EAX, DWORD PTR DS:[<Program.X>] ; Boxing for PrintType: Move the address stored in static variable Program.X into register EAX.
MOV EAX, DWORD PTR DS:[EAX + 4] ; Boxing for PrintType: Get value of MyStruct.A from the object stored in Program.X (MyStruct.A is at offset 4, since the object header is at offset 0).
MOV DWORD PTR DS:[ECX + 4], EAX ; Boxing for PrintType: Store that value in the newly created object (MyStruct.A is at offset 4, since the object header is at offset 0).
CALL DWORD PTR DS:[<Program.PrintType Ptr>] ; Call Program.PrintType.
POP EBP ; Function epilogue.
RET ; Return to caller.
Program.PrintA:
PUSH EAX ; Allocate local variable.
MOV DWORD PTR SS:[ESP], ECX ; Store argument 1 (the MyStruct) in the local variable.
MOV ECX, DWORD PTR SS:[ESP] ; Copy the MyStruct instance from the local variable into ECX (first argument to WriteLine).
CALL <mscorlib.ni.System.Console.WriteLine(object)> ; Call WriteLine(object) overload.
POP ECX ; Deallocate local variable.
RET ; Return to caller.
Program.PrintType:
PUSH EBP ; Function prologue.
MOV EBP, ESP ; Function prologue.
CMP DWORD PTR DS:[ECX], ECX ; Cause an access violation if 'this' is null, so the CLR can throw a null reference exception.
CALL <GetType> ; GetType.
MOV ECX, EAX ; Copy the returned System.Type object address into ECX (first argument).
MOV EAX, DWORD PTR DS:[ECX] ; Dereference object header pointer.
MOV EAX, DWORD PTR DS:[EAX + 38h] ; Retrieve virtual function table.
CALL DWORD PTR DS:[EAX + 10h] ; Call virtual function at offset 10h (get_FullName method).
MOV ECX, EAX ; Copy returned System.String into ECX (first argument).
CALL <mscorlib.ni.System.Console.WriteLine(int)> ; Call WriteLine.
POP EBP ; Function epilogue.
RET ; Return to caller.
Here's a comparison of the difference between built-in types like long and other value types.
public static class Program
{
public static long X;
public static void Main()
{
Program.X = 1234567887654321;
}
}
Compiles to:
Program.Main:
PUSH EBP ; Function prologue.
MOV EBP, ESP ; Function prologue.
MOV DWORD PTR DS:[DD4408], 3C650DB1 ; Store low DWORD of 1234567887654321.
MOV DWORD PTR DS:[DD440C], 462D5 ; Store high DWORD of 1234567887654321.
POP EBP ; Function epilogue.
RET ; Return.
In this example MyStruct wraps a long.
public static class Program
{
public static MyStruct X;
public static void Main()
{
Program.X.A = 1234567887654321;
}
}
Compiles to:
Program.Main:
PUSH EBP ; Function prologue.
MOV EBP, ESP ; Function prologue.
MOV EAX, DWORD PTR DS:[3BD354C] ; Retrieve the address of the MyStruct object stored at the address where Program.X resides.
MOV DWORD PTR DS:[EAX + 4], 3C650DB1 ; Store low DWORD of 1234567887654321 (The long begins at offset 4 since offset 0 is the object header pointer).
MOV DWORD PTR DS:[EAX + 8], 462D5 ; Store high DWORD of 1234567887654321 (High DWORD of course is offset 4 more from the low DWORD).
POP EBP ; Function epilogue.
RET ; Return.
On a side note: These struct objects are allocated for all value type static variables for the class, the first time a method is called that accesses any static variable in the class.
Perhaps that's why they're doing it. To save memory. If you have a lot of structs in static classes, but you're not calling any methods on those classes that use them, you use less memory. If they were inlined in the static classes, then even if your program never accesses them, each struct would take up their size in memory for no reason. By allocating them on the heap as objects the first time they're accessed, you only take up their size in memory (+ pointer for the object header) when accessing them, and at most 8 bytes per variable when not accessing them. This also makes libraries smaller. But that's just speculation from my side as to why they might be doing it this way.