Я хочу предположить, что цель этого вопроса - проверить, существует ли хотя бы один способ, даже если с помощью самого небезопасного взлома, сохранить ссылку на тип не-blittable value. Я знаю, что такой тип дизайна сопоставим с преступлением; Я бы не использовал его в каком-либо практическом случае, кроме обучения. Поэтому, пожалуйста, примите, пожалуйста, читать еретичный небезопасный код.
Мы знаем, что ссылку на blittable-тип можно сохранить и увеличить следующим образом:
unsafe class Foo
{
void* _ptr;
public void Fix(ref int value)
{
fixed (void* ptr = &value) _ptr = ptr;
}
public void Increment()
{
var pointer = (int*) _ptr;
(*pointer)++;
}
}
С точки зрения безопасности, вышеупомянутый класс сопоставим с прыжком в пустоте (каламбур не предназначен), но он работает, как уже упоминалось здесь. Если переменная, выделенная в стеке, передается ему, а затем область метода вызывающего абонента завершается, вы, вероятно, столкнетесь с ошибкой или явной ошибкой нарушения доступа. Однако, если вы выполните такую программу:
static class Program
{
static int _fieldValue = 42;
public static void Main(string[] args)
{
var foo = new Foo();
foo.Fix(ref _fieldValue);
foo.Increment();
}
}
Класс не будет размещен до тех пор, пока не будет выгружен относительный домен приложения, и поэтому применим для этого поля. Я честно не знаю, можно ли перераспределить поля в высокочастотной куче, но лично я не думаю. Но пусть откладывает безопасность еще больше (если возможно). Прочитав этот и этот вопрос, мне было интересно, есть ли способ создать подобный подход для не- и поэтому я сделал эту программу, которая на самом деле работает. Прочитайте комментарии, чтобы узнать, что он делает.
static class Program
{
static Action _event;
public static void Main(string[] args)
{
MakerefTest(ref _event);
//The invocation list is empty again
var isEmpty = _event == null;
}
static void MakerefTest(ref Action multicast)
{
Action handler = () => Console.WriteLine("Hello world.");
//Assigning a handler to the delegate
multicast += handler;
//Executing the delegate invocation list successfully
if (multicast != null) multicast();
//Encapsulating the reference in a TypedReference
var tr = __makeref(multicast);
//Removing the handler
__refvalue(tr, Action) -= handler;
}
}
Актуальная проблема/возможность:
Мы знаем, что компилятор не позволит нам сохранить значение, переданное с помощью ссылки ref, но ключевое слово __makeref
, как и недокументированное и недооцененное, дает возможность инкапсулировать и восстановить ссылку на типы, способные к блужданию. Однако возвращаемое значение __makeref
, TypedReference
хорошо защищено. Вы не можете сохранить его в поле, вы не можете его вставить, вы не можете создать его массив, вы не можете использовать его в анонимных методах или лямбда. Все, что мне удалось сделать, это изменить приведенный выше код следующим образом:
static void* _ptr;
static void MakerefTest(ref Action multicast)
{
Action handler = () => Console.WriteLine("Hello world.");
multicast += handler;
if (multicast != null) multicast();
var tr = __makeref(multicast);
//Storing the address of the TypedReference (which is on the stack!)
//inside of _ptr;
_ptr = (void*) &tr;
//Getting the TypedReference back from the pointer:
var restoredTr = *(TypedReference*) _ptr;
__refvalue(restoredTr, Action) -= handler;
}
Вышеприведенный код работает так же хорошо и выглядит еще хуже, чем раньше, но ради знания я хотел сделать больше с ним, поэтому написал следующее:
unsafe class Horror
{
void* _ptr;
static void Handler()
{
Console.WriteLine("Hello world.");
}
public void Fix(ref Action action)
{
action += Handler;
var tr = __makeref(action);
_ptr = (void*) &tr;
}
public void Clear()
{
var tr = *(TypedReference*) _ptr;
__refvalue(tr, Action) -= Handler;
}
}
Класс Horror
представляет собой комбинацию класса Foo
и указанного выше метода, но, как вы, несомненно, заметите, у него есть одна большая проблема. В методе Fix
объявлен TypedReference
tr
, его адрес копируется внутри общего указателя _ptr
, тогда метод заканчивается и tr
больше не существует. Когда вызывается метод Clear
, "новый" tr
поврежден, потому что _ptr
указывает на область стека, которая больше не является TypedReference
. Итак, возникает вопрос:
Есть ли способ обмануть компилятор в сохранении экземпляра TypedReference
в течение неопределенного промежутка времени?
Любой способ добиться желаемого результата будет считаться хорошим, даже если это связано с уродливым, небезопасным, медленным кодом. Класс, реализующий следующий интерфейс, был бы идеальным:
interface IRefStorage<T> : IDisposable
{
void Store(ref T value);
//IDisposable.Dispose should release the reference
}
Пожалуйста, не обсуждайте вопрос как общее обсуждение, потому что его цель состоит в том, чтобы увидеть, есть ли способ хранения ссылок на типы, которые могут быть сбиты с толку, такими же злыми, как это может быть.
Последнее замечание, я знаю о возможностях связывания полей через FieldInfo
, но мне показалось, что последний метод не очень сильно поддерживает типы, происходящие из Delegate
.
Возможное решение (результат баунти)
Я бы назвал AbdElRaheim ответом, как только он отредактировал свой пост, чтобы включить решение, которое он представил в своем комментарии, но я полагаю, что это было не очень ясно. В любом случае, среди методов, которые он предоставил, тот, который был подведен в следующем классе (который я слегка изменил), казался самым "надежным" (иронично использовать этот термин, поскольку мы говорим об использовании взлома):
unsafe class Horror : IDisposable
{
void* _ptr;
static void Handler()
{
Console.WriteLine("Hello world.");
}
public void Fix(ref Action action)
{
action += Handler;
TypedReference tr = __makeref(action);
var mem = Marshal.AllocHGlobal(sizeof (TypedReference)); //magic
var refPtr = (TypedReference*) mem.ToPointer();
_ptr = refPtr;
*refPtr = tr;
}
public void Dispose()
{
var tr = *(TypedReference*)_ptr;
__refvalue(tr, Action) -= Handler;
Marshal.FreeHGlobal((IntPtr)_ptr);
}
}
Что Fix
делает, начиная с строки, помеченной как "magic" в комментарии:
- Выделяет память в процессе - в неуправляемой части.
- Объявляет
refPtr
как указатель наTypedReference
и устанавливает его значение указателю области памяти, выделенной выше. Это делается вместо прямого использования_ptr
, потому что поле с типомTypedReference*
генерирует исключение. - Неявно переводит
refPtr
вvoid*
и присваивает указатель_ptr
. - Устанавливает
tr
как значение, указанноеrefPtr
и, следовательно,_ptr
.
Он также предложил другое решение, которое он первоначально написал как ответ, но он казался менее надежным, чем тот, который был выше. С другой стороны, было также другое решение, предоставленное Питером Уишартом, но для этого требовалась точная синхронизация, и каждый экземпляр Horror
"потерял" поток. Я воспользуюсь случаем, чтобы повторить, что вышеупомянутый подход находится в нет, предназначенном для использования в реальном мире, это был просто академический вопрос. Я надеюсь, что это будет полезно для тех, кто читает этот вопрос.