Можно ли косвенно загрузить тип значения в стек

В Microsoft IL для вызова метода для типа значения требуется косвенная ссылка. Допустим, у нас есть ILGenerator с именем «il», и в настоящее время у нас есть Nullable на вершине стека, если мы хотим проверить, имеет ли он значение, мы можем выдать следующее:

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));

Таким образом, вы не можете просто вызвать метод со значением в стеке, потому что это тип значения (хотя вы могли бы, если бы это был ссылочный тип).

Чего я хотел бы добиться (или узнать, возможно ли это), так это заменить три строки, которые показаны закомментированными, но сохранить работу программы без использования временного локального файла.


person Greg Beech    schedule 16.09.2008    source источник


Ответы (4)


Если переменная уже находится в стеке, вы можете просто выполнить вызов метода.

Кажется, что конструктор не помещает переменную в стек в типизированном виде. Немного покопавшись в 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, если нового объекта нет в стеке.

person Abe Heidebrecht    schedule 16.09.2008
comment
Да, это тоже отлично работает, но все еще требует локального. Я думаю, что это начинает подтверждать мое первоначальное подозрение, что это невозможно сделать без местного. Я думаю, что Stloc работает, потому что у локального есть дополнительные метаданные сохраняемого типа... но опять же, то же самое и с Call... Странно. - person Greg Beech; 17.09.2008
comment
Да, мне показалось очень странным, что это не сработало так, как я ожидал. Если ваш динамический метод принимает Nullable‹int› в качестве аргумента, все, что вам нужно сделать, это вызвать ldarg_0, но я предполагаю, что вам действительно нужно создать тип значения в методе. - person Abe Heidebrecht; 18.09.2008
comment
Действительно, мне нужно создать значения внутри метода. У меня проблема в том, что для каждого метода (разных типов) их довольно много, что означает, что достаточное количество локальных пользователей используется исключительно для временных операций. Это не имеет большого значения, и все работает нормально, но кажется немного грязным. - person Greg Beech; 18.09.2008
comment
Если переменная уже находится в стеке, вы можете просто выполнить вызов метода. - это вводит в заблуждение, так как на самом деле это суть проблемы - вы НЕ МОЖЕТЕ использовать вызов для типа значения в стеке - вы должны сначала сохранить его в локальной переменной, а затем поместить адрес этого типа значения в стек, тогда вы можете использовать вызов. - person Sebastian; 16.12.2020

Я понял! К счастью, я читал об опкоде 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));

Недостатком этого является то, что бокс вызывает выделение памяти для упакованного объекта, поэтому он немного медленнее, чем использование локальных переменных (которые уже были бы выделены). Но это избавляет вас от необходимости определять, объявлять и ссылаться на все необходимые вам локальные переменные.

person Mayoor    schedule 18.03.2016

Посмотрев на варианты еще немного и поразмыслив, я думаю, вы правы, предполагая, что это невозможно сделать. Если вы изучите поведение инструкций MSIL в стеке, вы увидите, что ни одна операция не оставляет свои операнды в стеке. Поскольку это было бы требованием для операции «получить адрес записи стека», я вполне уверен, что ее не существует.

Это оставляет вам либо dup+box, либо stloc+ldloca. Как вы указали, последний, вероятно, более эффективен.

@greg: Многие инструкции оставляют свой результат в стеке, но никакие инструкции не оставляют ни одного из своих операндов в стеке, что требуется для «получения адреса элемента стека». инструкция.

person Nick Johnson    schedule 18.09.2008
comment
Инструкция newobj оставляет свой результат в стеке — если бы это был ссылочный тип, я мог бы немедленно вызвать для него метод. По сути, я думаю, вы правы, в стеке есть значение структуры, но мне нужен адрес этого значения, чтобы вызвать для него метод. - person Greg Beech; 18.09.2008
comment
На самом деле, по-видимому, команда C# вкратце рассмотрела идею опционального разрешения возвращаемых значений ref, на что и намекает это обсуждение: blogs.msdn.com/b/ericlippert/archive/2011/06/23/ - person Glenn Slayden; 04.11.2013

Только что написал класс, который делает то, о чем просит OP... вот код IL, который создает компилятор С#:

  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
person Mark    schedule 04.06.2012