Variable Parameter passing in C#

Banana leaves - hyderabad

Sometime back I posted about variable parameters in Ruby. C# also supports methods that accepts variable number of arguments (e.g. Console.Writeline). In this post I'll try to cover what happens in the background. This is a long one and so bear with me :)

Consider the following two methods. Both prints out each argument passed to it. However, the first accepts variable arguments using the params keyword.

 static void Print1(params int[] args)
{
    foreach (int arg in args)
    {
        Console.WriteLine(arg);
    }
}

static void Print2(int[] args)
{
    foreach (int arg in args)
    {
        Console.WriteLine(arg);
    }
}

The above methods can be called as follows

 Print1(42, 84, 126); // variable argument passing
int[] a = new int[] { 42, 84, 126 };
Print2(a);           // called with an array

Obviously in the case above, using variable number of parameters is easier.

If we see the generated IL for Print1 and Print2 using ILDASM or Reflector and then do a diff, we will get the following diff

 .method private hidebysig static void Print2(object[] args) cil managed
.method private hidebysig static void Print1(object[] args) cil managed
{

    .param [1]                                                          
    .custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() 
    .maxstack 2
    .locals init (
        [0] object arg,
        [1] object[] CS$6$0000,
        [2] int32 CS$7$0001,
        [3] bool CS$4$0002)
    L_0000: nop 
    L_0001: nop 
    L_0002: ldarg.0 
    L_0003: stloc.1 
    L_0004: ldc.i4.0 
    L_0005: stloc.2 
    L_0006: br.s L_0019
    L_0008: ldloc.1 
    L_0009: ldloc.2 
    L_000a: ldelem.ref 
    L_000b: stloc.0 
    L_000c: nop 
    L_000d: ldloc.0 
    L_000e: call void [mscorlib]System.Console::WriteLine(object)
    L_0013: nop 
    L_0014: nop 
    L_0015: ldloc.2 
    L_0016: ldc.i4.1 
    L_0017: add 
    L_0018: stloc.2 
    L_0019: ldloc.2 
    L_001a: ldloc.1 
    L_001b: ldlen 
    L_001c: conv.i4 
    L_001d: clt 
    L_001f: stloc.3 
    L_0020: ldloc.3 
    L_0021: brtrue.s L_0008
    L_0023: ret 
}

Only the lines in Green are additional in Print1 (which takes variable arguments) and otherwise both methods looks identical. In this context .param[1*] indicates that the first parameter of Print1 (args) is the variable argument. The ParamArrayAttribute is applied to the method to indicate that the method allows variable number of arguments.

Effectively all of the above means that the callee is not really bothered with being invoked with variable number of arguments. It receives an array parameter as it would even without the param keyword usage. The only difference is that the method is decorated with the some directive and attribute when param is used. Now it's the caller-code compiler's duty to read this attribute and generate the correct code so that variable number of parameters are put into a array and Print1 is called with that.

The generated IL for the call Print1(42, 84, 126); is as follows...

 .method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] int32[] CS$0$0000)
    L_0000: nop 
    L_0001: ldc.i4.3        ; <= Array of size 3 is created, int32[3]
    L_0002: newarr int32    ; <=
    L_0007: stloc.0         ; <= the array is stored in the var CS$0$0000
    L_0008: ldloc.0 
    L_0009: ldc.i4.0        ; push 0
    L_000a: ldc.i4.s 0x2a   ; push 42
    L_000c: stelem.i4       ; this makes 42 to be stored at index 0 **
    L_000d: ldloc.0 
    L_000e: ldc.i4.1 
    L_000f: ldc.i4.s 0x54
    L_0011: stelem.i4       ; similarly as above stores 84 at index 1
    L_0012: ldloc.0 
    L_0013: ldc.i4.2 
    L_0014: ldc.i4.s 0x7e
    L_0016: stelem.i4       ; stores 126 at index 2
    L_0017: ldloc.0 
    L_0018: call void VariableArgs.Program::Print1(int32[]) ; call Print1 with array
    L_001d: nop 
    L_001e: ret 
}

This shows that for the call an array is created and all the parameters are placed in it. Then Print1 is called with that array.

Footnote:
*interestingly it starts at 1 and not 0 because 0 is used for the return value.
**stelem takes the stack [..|array|index|value] and replaces the value in array at index with value

Cross posted here