2

I want to emit dynamic method which is exactly the same as below:

void Foo(TimeSpan ts = default(TimeSpan))

By using ildasm, i can see it has been complied as nullref. However from what i can get, if i want to achieve the same by Emit code, the method i can call named ParameterBuilder.SetConstant, and it will throw exception when the optional value type is TimeSpan. I even decompiled SetConstant method, it handles DateTime explicitly(but not TimeSpan). Nullref is also not acceptable. From that code, it seems there's no way to set default(TimeSpan) as a default value. Can anyone help?

piyushj
  • 1,546
  • 5
  • 21
  • 29
  • Add your code - a minimal example that shows your problem and actually compiles and runs. Note that the documentation for `ParameterBuilder.SetConstant` *explicitly* mentions the supported types, and `TimeSpan` simply isn't included (it's not a standard type as per the specification, just like e.g. `decimal`). – Luaan Jun 01 '16 at 11:51
  • 1
    Also, why are you trying to do this? How is the dynamic method supposed to be used? Are you using this to compile to an assembly you reference later? Default values are a compile-time feature, so unless you call the method from normal C# code (no reflection etc.), the default value has no meaning. What are you trying to do, and why do you think default arguments are a good way to do that? – Luaan Jun 01 '16 at 11:55
  • I found you question really interesting, but you should indeed add a code sample of what you tried, and the exception you get when invoking SetConstant. Good questions tend to attract good and numerous answers, and I personally think this is a really interesting question. – Regis Portalez Jun 01 '16 at 14:06
  • 1
    Please think to accept any of the below answers if one of them actually answers your question. This will save time for next guy visiting this oage – Regis Portalez Jun 01 '16 at 18:10

2 Answers2

2

Depending on what you want to achieve, exactly, there's possibly a simpler way. If you call ParameterBuilder.DefineParameter(1, ParameterAttributes.Optional, "Foo"), the resulting parameter will be declared as optional, but without an explicit default value. When using this assembly in C#, you will not get IntelliSense for the default value, but the compiler will nevertheless allow you to call the method without explicitly supplying a value, and if you do so, it will pass default(TimeSpan).

The resulting IL is not identical to what the C# compiler would produce (since the parameter initialization is missing), and I could only guess what other .NET languages would do with such a declaration, but it does save on some very ugly groveling inside the internals of System.Reflection.Emit (and the resulting IL passes verification -- the runtime itself does nothing with the default declaration).

Note that, precisely because the runtime does nothing with default value declarations (requiring any tools to do so) emitting default values in dynamic methods is a curious practice that should have little to no actual applications, because any code that knows to call the method also ought to know what value is to be passed (defining them in assemblies that are saved to disk is meaningful, compilers could read that).

If the method truly is dynamic, you may want to generate multiple overloads of the method instead, one with parameter, one without (and the one without can call the other one). This achieves the same effect as a method with an optional parameter and is simpler for dynamic callers to handle as well.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
1

This is quite hard, and needs heavy usage of reflection to workaround limitations of .net framework.

As you pointed, you can disassemble ParameterBuilder.setConstant. This methods invokes an internal method:

[SecuritySafeCritical]
public virtual void SetConstant(object defaultValue)
{
    TypeBuilder.SetConstantValue(this.m_methodBuilder.GetModuleBuilder(), this.m_pdToken.Token, (this.m_iPosition == 0) ? this.m_methodBuilder.ReturnType : this.m_methodBuilder.m_parameterTypes[this.m_iPosition - 1], defaultValue);
}

which you can also disassemble, and see where the exception is thrown from (when type is a value type):

if (destType.IsValueType && (!destType.IsGenericType || !(destType.GetGenericTypeDefinition() == typeof(Nullable<>))))
        {
            throw new ArgumentException(Environment.GetResourceString("Argument_ConstantNull"));
        }
        TypeBuilder.SetConstantValue(module.GetNativeHandle(), tk, 18, null);

Fortunately, you can invoke the same methods here, but dynamically from mscorlib:

AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
        AssemblyBuilder ab =
            AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
        ModuleBuilder mb =
            ab.DefineDynamicModule(aName.Name, aName.Name + ".dll");

        TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Public);

        MethodBuilder meb = tb.DefineMethod("Foo", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, typeof(void), new Type[] { typeof(TimeSpan) });

        ParameterBuilder pb = meb.DefineParameter(1, ParameterAttributes.Optional | ParameterAttributes.HasDefault, "ts");

        MethodInfo getNativeHandle = typeof(ModuleBuilder).GetMethod("GetNativeHandle", BindingFlags.NonPublic | BindingFlags.Instance);
        object nativeHandle = getNativeHandle.Invoke(mb, new object[0]);

        int tk = pb.GetToken().Token;

        MethodInfo setConstantValue = typeof(TypeBuilder).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(mi => mi.Name == "SetConstantValue" && mi.GetParameters().Last().ParameterType.IsPointer).First();

        setConstantValue.Invoke(pb, new object[] { nativeHandle, tk, /* CorElementType.Class: */ 18, null });

        ILGenerator ilgen = meb.GetILGenerator();

        FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
        fi.SetValue(ilgen, 8);

        ilgen.Emit(OpCodes.Ret);


        tb.CreateType();
        ab.Save("DynamicAssemblyExample.dll");

Setting default value this way won't update the stacksize, which means you have to set it manually (again through reflection), right after getting the ILGenerator:

FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
            fi.SetValue(ilgen, 8);

This generates the following IL:

.method public hidebysig static 
    void Foo (
        [opt] valuetype [mscorlib]System.TimeSpan ts
    ) cil managed 
{
    .param [1] = nullref
    // Method begins at RVA 0x2050
    // Code size 1 (0x1)
    .maxstack 8

    IL_0000: ret
} // end of method MyClass::Foo

which is the same thing as what the C# you provided compiles to.

Regis Portalez
  • 4,675
  • 1
  • 29
  • 41