Is it possible to indirectly load a value type on the stack
Asked Answered
B

4

8

In Microsoft IL, to call a method on a value type you need an indirect reference. Lets say we have an ILGenerator named "il" and that currently we have a Nullable on top of the stack, if we want to check whether it has a value then we could emit the following:

var local = il.DeclareLocal(typeof(Nullable<int>));
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

However it would be nice to skip saving it as a local variable, and simply call the method on the address of the variable already on the stack, something like:

il.Emit(/* not sure */);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

The ldind family of instructions looks promising (particularly ldind_ref) but I can't find sufficient documentation to know whether this would cause boxing of the value, which I suspect it might.

I've had a look at the C# compiler output, but it uses local variables to achieve this, which makes me believe the first way may be the only way. Anyone have any better ideas?

**** Edit: Additional Notes ****

Attempting to call the method directly, as in the following program with the lines commented out, doesn't work (the error will be "Operation could destabilise the runtime"). Uncomment the lines and you'll see that it does work as expected, returning "True".

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
//var local = il.DeclareLocal(typeof(Nullable<int>));
//il.Emit(OpCodes.Stloc, local);
//il.Emit(OpCodes.Ldloca, local);
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

So you can't simply call the method with the value on the stack because it's a value type (though you could if it was a reference type).

What I'd like to achieve (or to know whether it is possible) is to replace the three lines that are shown commented out, but keep the program working, without using a temporary local.

Boardman answered 16/9, 2008 at 19:57 Comment(0)
M
2

If the variable is already on the stack, you can go ahead and just emit the method call.

It seems that the constructor doesn't push the variable on the stack in a typed form. After digging into the IL a bit, it appears there are two ways of using the variable after constructing it.

You can load the variable that will store the reference onto the evaluation stack before calling the constructor, and then load that variable again after calling the constructor like so:

DynamicMethod method = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
Type nullable = typeof(Nullable<int>);
ConstructorInfo ctor = nullable.GetConstructor(new Type[] { typeof(int) });
MethodInfo getValue = nullable.GetProperty("HasValue").GetGetMethod();
LocalBuilder value = il.DeclareLocal(nullable);         

// load the variable to assign the value from the ctor to
il.Emit(OpCodes.Ldloca_S, value);
// load constructor args
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Call, ctor);
il.Emit(OpCodes.Ldloca_S, value);

il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(method.Invoke(null, null));

The other option is doing it the way you have shown. The only reason for this that I can see is that the ctor methods return void, so they don't put their value on the stack like other methods. It does seem strange that you can call Setloc if the new object isn't on the stack.

Maxillary answered 16/9, 2008 at 20:2 Comment(4)
Yeah this also works perfectly, but still requires a local. I think this is starting to confirm my original suspicion that it isn't possible to do this without a local. I think Stloc works because the local has the additional metadata of the type being stored... but then again so does Call... Odd.Boardman
Yeah, I found it very strange that it didn't work like I expected it to. If your dynamic method takes the Nullable<int> as an argument, all you need to do is call ldarg_0, but I am assuming you actually need to create the value type in the method.Maxillary
Indeed, I do need to create the values within the method. The trouble I have is that there are a fair number of them per method (of different types) which means a fair number of locals used purely for temporary operations. It's not a huge deal, and all works fine, but just seems a bit messy.Boardman
"If the variable is already on the stack, you can go ahead and just emit the method call." - this is quite misleading, as it's actually the core of the issue - you CAN'T use the call on a valuetype on the stack - you must first store it to a local variable, then push the address of that valuetype on the stack, then you can use the call.Hernia
A
3

I figured it out! Luckily I was reading about the unbox opcode and noticed that it pushes the address of the value. unbox.any pushes the actual value. So, in order to call a method on a value type without having to store it in a local variable and then load its address, you can simply box followed by unbox. Using your last example:

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Box, typeof(Nullable<int>)); // box followed by unbox
il.Emit(OpCodes.Unbox, typeof(Nullable<int>));
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

The downside to this is that boxing causes memory allocation for the boxed object, so it is a bit slower than using local variables (which would already be allocated). But, it saves you from having to determine, declare, and reference all of the local variables you need.

Allies answered 18/3, 2016 at 5:8 Comment(0)
M
2

If the variable is already on the stack, you can go ahead and just emit the method call.

It seems that the constructor doesn't push the variable on the stack in a typed form. After digging into the IL a bit, it appears there are two ways of using the variable after constructing it.

You can load the variable that will store the reference onto the evaluation stack before calling the constructor, and then load that variable again after calling the constructor like so:

DynamicMethod method = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
Type nullable = typeof(Nullable<int>);
ConstructorInfo ctor = nullable.GetConstructor(new Type[] { typeof(int) });
MethodInfo getValue = nullable.GetProperty("HasValue").GetGetMethod();
LocalBuilder value = il.DeclareLocal(nullable);         

// load the variable to assign the value from the ctor to
il.Emit(OpCodes.Ldloca_S, value);
// load constructor args
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Call, ctor);
il.Emit(OpCodes.Ldloca_S, value);

il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(method.Invoke(null, null));

The other option is doing it the way you have shown. The only reason for this that I can see is that the ctor methods return void, so they don't put their value on the stack like other methods. It does seem strange that you can call Setloc if the new object isn't on the stack.

Maxillary answered 16/9, 2008 at 20:2 Comment(4)
Yeah this also works perfectly, but still requires a local. I think this is starting to confirm my original suspicion that it isn't possible to do this without a local. I think Stloc works because the local has the additional metadata of the type being stored... but then again so does Call... Odd.Boardman
Yeah, I found it very strange that it didn't work like I expected it to. If your dynamic method takes the Nullable<int> as an argument, all you need to do is call ldarg_0, but I am assuming you actually need to create the value type in the method.Maxillary
Indeed, I do need to create the values within the method. The trouble I have is that there are a fair number of them per method (of different types) which means a fair number of locals used purely for temporary operations. It's not a huge deal, and all works fine, but just seems a bit messy.Boardman
"If the variable is already on the stack, you can go ahead and just emit the method call." - this is quite misleading, as it's actually the core of the issue - you CAN'T use the call on a valuetype on the stack - you must first store it to a local variable, then push the address of that valuetype on the stack, then you can use the call.Hernia
T
1

After looking at the options some more and further consideration, I think you're right in assuming it can't be done. If you examine the stack behaviour of MSIL instructions, you can see that no op leaves its operand(s) on the stack. Since this would be a requirement for a 'get address of stack entry' op, I'm fairly confident one doesn't exist.

That leaves you with either dup+box or stloc+ldloca. As you've pointed out, the latter is likely more efficient.

@greg: Many instructions leave their result on the stack, but no instructions leave any of their operands on the stack, which would be required for a 'get stack element address' instruction.

Twoseater answered 18/9, 2008 at 7:10 Comment(2)
The newobj instruction leaves its result on the stack - if this was a reference type I could call a method on it immediately. Essentially I think you're right, there is the value of the struct on the stack but I need the address of that value to call the method on it.Boardman
Actually, apparently the C# team did briefly consider the idea of optionally allowing 'ref' return values, which is what this discussion is alluding to: blogs.msdn.com/b/ericlippert/archive/2011/06/23/…Guess
M
0

Just wrote a class that does what the OP is asking... here's the IL code that C# compiler produces:

  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_000f:  stfld      valuetype [mscorlib]System.Nullable`1<int32> ConsoleApplication3.Temptress::_X
  IL_0014:  nop
  IL_0015:  ret
Magnific answered 4/6, 2012 at 15:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.