Можно ли косвенно загрузить тип значения в стек
-
09-06-2019 - |
Вопрос
В Microsoft IL для вызова метода для типа значения вам нужна косвенная ссылка.Допустим, у нас есть ILGenerator с именем "il" и что в настоящее время у нас есть значение Null в верхней части стека, если мы хотим проверить, имеет ли оно значение, мы могли бы выдать следующее:
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);
Однако было бы неплохо пропустить сохранение его как локальной переменной и просто вызвать метод по адресу переменной, уже находящейся в стеке, что-то вроде:
il.Emit(/* not sure */);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);
Семейство инструкций ldind выглядит многообещающе (особенно ldind_ref), но я не могу найти достаточной документации, чтобы узнать, приведет ли это к блокировке значения, что, как я подозреваю, возможно.
Я просмотрел выходные данные компилятора C #, но для достижения этой цели он использует локальные переменные, что заставляет меня поверить, что первый способ может быть единственным.У кого-нибудь есть идеи получше?
**** Редактировать:Дополнительные примечания ****
Попытка вызвать метод напрямую, как в следующей программе с закомментированными строками, не сработает (ошибка будет "Операция может дестабилизировать среду выполнения").Раскомментируйте строки, и вы увидите, что это работает так, как ожидалось, возвращая "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));
Таким образом, вы не можете просто вызвать метод со значением в стеке, потому что это тип значения (хотя вы могли бы, если бы это был ссылочный тип).
Чего я хотел бы достичь (или узнать, возможно ли это), так это заменить три строки, которые показаны закомментированными, но сохранить работу программы без использования временной локальной.
Решение
Если переменная уже находится в стеке, вы можете пойти дальше и просто выполнить вызов метода.
Похоже, что конструктор не помещает переменную в стек в типизированной форме.Немного покопавшись в IL, выясняется, что есть два способа использования переменной после ее создания.
Вы можете загрузить переменную, которая будет хранить ссылку, в стек вычисления перед вызовом конструктора, а затем снова загрузить эту переменную после вызова конструктора следующим образом:
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));
Другой вариант - делать это так, как вы показали.Единственная причина этого, которую я вижу, заключается в том, что методы ctor возвращают void, поэтому они не помещают свое значение в стек, как другие методы.Кажется странным, что вы можете вызвать Setloc, если нового объекта нет в стеке.
Другие советы
После еще одного рассмотрения вариантов и дальнейшего рассмотрения, я думаю, вы правы, предполагая, что это невозможно сделать.Если вы изучите поведение стека команд MSIL, вы увидите, что ни одна операция не оставляет своих операндов в стеке.Поскольку это было бы требованием для операции "получить адрес записи стека", я совершенно уверен, что таковой не существует.
Это оставляет вас либо с dup + box, либо с stloc + ldloca.Как вы уже отметили, последнее, скорее всего, более эффективно.
@грег:Многие инструкции оставляют свои Результат в стеке, но никакие инструкции не оставляют ни одной из своих операнды в стеке, что потребовалось бы для инструкции "получить адрес элемента стека".
Я все понял!К счастью, я читал о unbox
код операции и заметил, что он выталкивает адрес о ценности. unbox.any
выводит фактическое значение.Итак, чтобы вызвать метод для типа значения без необходимости сохранять его в локальной переменной и затем загружать его адрес, вы можете просто box
за которым следует unbox
.Используя ваш последний пример:
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));
Недостатком этого является то, что упаковка приводит к выделению памяти для упакованного объекта, поэтому это немного медленнее, чем использование локальных переменных (которые уже были бы выделены).Но это избавляет вас от необходимости определять, объявлять и ссылаться на все необходимые вам локальные переменные.
Просто написал класс, который делает то, о чем просит OP...вот IL-код, который создает компилятор C #:
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