0

let's say we have following C# code:

public static void Main() 
{
   int v = 5;
   Object o = v;
   v = 123;
   Console.WriteLine(v + (Int32) o); // Displays "1235"
}

and the IL code generated is:

.locals init ([0]int32 v, [1] object o)

 // Load 5 into v.
 IL_0000: ldc.i4.5
 IL_0001: stloc.0

 // Box v and store the reference pointer in o.    <------first boxing
 IL_0002: ldloc.0
 IL_0003: box [mscorlib]System.Int32
 IL_0008: stloc.1

 // Load 123 into v.
 IL_0009: ldc.i4.s 123
 IL_000b: stloc.0

 // Box v and leave the pointer on the stack for Concat.  <------second boxing
 IL_000c: ldloc.0
 IL_000d: box [mscorlib]System.Int32

 // Unbox o: Get the pointer to the In32's field on the stack.
 IL_0017: ldloc.1
 IL_0018: unbox.any [mscorlib]System.Int32

 // Box the Int32 and leave the pointer on the stack for Concat.   <------third boxing
 IL_001d: box [mscorlib]System.Int32

 // Call Concat.
 IL_0022: call string [mscorlib]System.String::Concat(object, object) 

we can see that the first boxing and second boxing works as follow:

  1. push the first argument v on stack.

  2. call box CIL

So it looks like when box is called, the "argument" needed is the stack pointer which points to the first field of v.

And the third boxing works as follow:

  1. The preceding unbox create a value type pointer, this value type pointer points to the first field of boxed instance on heap, and then this value type pointer is pushed onto the stack.

  2. call box CIL

So now it looks like when box is called, it first check the stack pointer to get its content(the value type pointer that points to heap) by dereferencing stack pointer.

So my question is that, is box CIL designed as that versatile that sometimes it reads the stack pointer directly while sometimes it dereferences the stack pointer to get another pointer(a pointer to heap in my case)?

  • Note that there is a very good response [here](https://stackoverflow.com/q/3743762/613130) – xanatos Jan 28 '21 at 00:13
  • Does this answer your question? [How boxing a value type work internally in C#?](https://stackoverflow.com/questions/65912313/how-boxing-a-value-type-work-internally-in-c) –  Jan 28 '21 at 08:30

2 Answers2

1

The unbox.any unboxes and loads the value type in the stack (so it copies it):

From MSDN:

The resulting object reference or value type is pushed onto the stack.

When applied to the boxed form of a value type, the unbox.any instruction extracts the value contained within obj (of type O), and is therefore equivalent to unbox followed by ldobj.

What you are thinking is the unbox instruction:

The unbox instruction converts the object reference (type O), the boxed representation of a value type, to a value type pointer (a managed pointer, type &), its unboxed form. The supplied value type (valType) is a metadata token indicating the type of value type contained within the boxed object.

Unlike Box, which is required to make a copy of a value type for use in the object, unbox is not required to copy the value type from the object. Typically it simply computes the address of the value type that is already present inside of the boxed object.

I don't even know how you can force the compiler to use the unbox instruction (reading from here it isn't used in C#, or at least it wasn't used in the compiler in 2010... I've done some tests, mixing ref, boxing and unboxing and I wasn't able to force the compiler to use it)

Mmmh... By taking a look at the ILSpy (they are experts in decompiling C# code), it seems that unbox is used only in the "private" implementation of certain switch (the switch statement is compiled in different ways depending on the number and on the types of conditions it has). The only reference about unbox is in a method called MatchLegacySwitchOnStringWithHashtable... I will say the name is quite clear. Another reference is in the Unsafe.il file... the file is "linked" to the Unsafe class of .NET. See the proposal here about the Unsafe.Unbox<T> method. The method was accepted and is part of .NET now. The corresponding C# code doesn't compile:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T Unbox<T>(object box) where T : struct
{
    return ref (T)box;
}

In truth, by taking a look at the .NET code it is probably implemented as an intrinsic.

Cancel everything.. Charlieface has found how to force the use of unbox:

public struct MyStruct
{
    public int A;

    public int Test()
    {
        object st2 = new MyStruct();
        int a = ((MyStruct)st2).A;
        return a;
    }
}

The method Test() is compiled to:

// Methods
.method public hidebysig 
    instance int32 Test () cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 25 (0x19)
    .maxstack 1
    .locals init (
        [0] valuetype MyStruct
    )

    IL_0000: ldloca.s 0
    IL_0002: initobj MyStruct
    IL_0008: ldloc.0
    IL_0009: box MyStruct
    IL_000e: unbox MyStruct
    IL_0013: ldfld int32 MyStruct::A
    IL_0018: ret
} // end of method MyStruct::Test

From some tests I'll say that the intelligent opcode in the family is ldfld: it can work both with value types (Test1), references to value types (Test2) and directly unboxed value types (Test3).

public struct MyStruct
{
    public int A;

    public int Test1(MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test2(ref MyStruct st)
    {
        int a = st.A;
        return a;
    }

    public int Test3(MyStruct st)
    {
        object st2 = st;
        int a = ((MyStruct)st2).A;
        return a;
    }
}

is compiled to

.method public hidebysig 
    instance int32 Test1 (
        valuetype MyStruct st
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: ldfld int32 MyStruct::A
    IL_0006: ret
} // end of method MyStruct::Test1

.method public hidebysig 
    instance int32 Test2 (
        valuetype MyStruct& st
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: ldfld int32 MyStruct::A
    IL_0006: ret
} // end of method MyStruct::Test2

.method public hidebysig 
    instance int32 Test3 (
        valuetype MyStruct st
    ) cil managed 
{
    // Method begins at RVA 0x2058
    // Code size 17 (0x11)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: box MyStruct
    IL_0006: unbox MyStruct
    IL_000b: ldfld int32 MyStruct::A
    IL_0010: ret
} // end of method MyStruct::Test3

The ldfld opcode is always the same, and it is working with two (three) different types: int32 and reference to int32 (and reference to boxed int32).

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Cast `object` to struct and access a field. Generates an `unbox` – Charlieface Jan 28 '21 at 00:34
  • You are right: [sharplab](https://sharplab.io/#v2:C4LglgNgPgAgTARgLACgAEG0GdgCcCuAxsGgLICeAynkcKpmgN70OYwDMaYAdiQIIBuVC1ZoOXXmgAqAUxwAKAJQjWzdKNYB7AEYArGcWzA4aALxpuMgO5kqNYkqHqNmHiQCGZtPPkVqBYkUcOEUAOkEVFxgAdjR3JxcAXxVklCA) – xanatos Jan 28 '21 at 00:39
  • I get `unbox` for both in LinqPad. The logic behind it is sound: a field access has ref (non-copy) semantics. In fact, not sure why sharplab bothers with an `unbox.any` followed by a `pop`, there's no side effect anyway – Charlieface Jan 28 '21 at 00:40
  • @Charlieface Try Release mode... Some IL code gets cleaned away – xanatos Jan 28 '21 at 00:44
  • Hum, you're right, looks like an optimization artifact from escape analysis e.g. do `a.ToString()` on the next line, and you get `unbox`. I bet the JIT would elide it anyway. – Charlieface Jan 28 '21 at 01:00
0

@xanatos is correct. You are mixing up unbox and unbox.any

From the ECMA-335 spec (the spec for .NET and CIL)

Part III.4.33:

Unlike the unbox instruction, for value types, unbox.any leaves a value, not an address of a value, on the stack.

Incidentally, there are instructions which take either a ref valuetype or an actual value. For example, ldfld (see more on this here)


Furthermore, you say:

So it looks like when box is called, the "argument" needed is the stack pointer which points to the first field of v.

This is not true: ldloc.0 will load the actual value to the stack.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • I know that `ldloc.0` will load the actual value to the stack. let's say the origian stack pointer is 0x10, and the struct instance have two fields, each filed is 4 bytes, so when this struct instance is pushed on stack, its two fields get copy on stack and the stack pointer decremented to 0x08 (16-8 = 8), so in the next `box` CIL instruction, it uses the stack pointer value 0x08 as the starting address to read the first field then second field, so what I understand is, to `box` CIL, it just need the stack pointer as its "argument", is my understanding correct? –  Jan 28 '21 at 03:17
  • You're mixing up the MSIL logical stack with it's implementation on x86/x64. Yes, that is how it's translated into assembler by the JIT, I think. But the `box` instruction is not assembler, it's MSIL, and it's argument is just `top-of-stack value`. There is no mention of a stack pointer in ECMA-335, just a logical stack. – Charlieface Jan 28 '21 at 05:01