Наложение нескольких полей ссылки CLR друг на друга в явной структуре?

Изменить: Мне хорошо известно, что это очень хорошо работает с типами значений, мой конкретный вопрос заключается в использовании этого для ссылочных типов.

Edit2: Я также знаю, что вы не можете накладывать типы ссылок и типы значений в структуре, это просто для случая перекрытия нескольких полей ссылочного типа друг с другом.

Я занимался структурированием в .NET/С#, и я только узнал, что вы можете это сделать:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1 {

    class Foo { }
    class Bar { }

    [StructLayout(LayoutKind.Explicit)]
    struct Overlaid {
        [FieldOffset(0)] public object AsObject;
        [FieldOffset(0)] public Foo AsFoo;
        [FieldOffset(0)] public Bar AsBar;
    }

    class Program {
        static void Main(string[] args) {
            var overlaid = new Overlaid();
            overlaid.AsObject = new Bar();
            Console.WriteLine(overlaid.AsBar);

            overlaid.AsObject = new Foo();
            Console.WriteLine(overlaid.AsFoo);
            Console.ReadLine();
        }
    }
}

В основном обходимо выполнять динамическое кастинг во время выполнения, используя структуру, которая имеет явный формат поля, а затем обращается к объекту внутри по мере его правильного типа.

Теперь мой вопрос: может ли это привести к утечке памяти каким-либо образом или к любому другому undefined поведению внутри CLR? Или это полностью поддерживаемое соглашение, которое можно использовать без каких-либо проблем?

Я знаю, что это один из более темных уголков CLR, и что этот метод является всего лишь жизнеспособным вариантом в очень немногих конкретных случаях.

Ответ 1

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

Это было бы безопаснее:

struct Overlaid { // could also be a class for reference-type semantics
    private object asObject;
    public object AsObject {get {return asObject;} set {asObject = value;} }
    public Foo AsFoo { get {return asObject as Foo;} set {asObject = value;} }
    public Bar AsBar { get {return asObject as Bar;} set {asObject = value;} }
}

Отсутствие риска рваных ссылок и т.д. и все еще только одно поле. Он не включает в себя какой-либо рискованный код и т.д. В частности, он не рискует чем-то глупо:

    [FieldOffset(0)]
    public object AsObject;
    [FieldOffset(0)]
    public Foo AsFoo;
    [FieldOffset(1)]
    public Bar AsBar; // kaboom!!!!

Другая проблема заключается в том, что вы можете поддерживать только одно поле таким образом, если вы не можете гарантировать режим CPU; смещение 0 легко, но становится сложнее, если вам нужно несколько полей и вам нужно поддерживать x86 и x64.

Ответ 2

Ну, вы нашли дыру цикла, CLR разрешает его, поскольку все перекрывающиеся поля являются объектами. Все, что позволяло бы вам связываться с ссылкой на объект напрямую, будет отклонено с помощью исключения TypeLoadException:

  [StructLayout(LayoutKind.Explicit)]
  struct Overlaid {
    [FieldOffset(0)]
    public object AsObject;
    [FieldOffset(0)]
    public IntPtr AsPointer;
  }

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

Запись этих полей, однако, приводит к исключению ExecutionEngineException. Я думаю, однако, что это эксплойт, если вы можете правильно оценить значение дескриптора отслеживания. Практическое использование достаточно близко к нулю, хотя.

Ответ 3

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

Я предполагаю, что вы можете использовать StructLayout и скомпилировать свой код без флагов /unsafe - это функция CLR. Вам нужен атрибут StructLayout просто потому, что С# не имеет прямых способов объявления типов таким образом.

Посмотрите эту страницу, в которой подробно описывается, как С# structs преобразуется в IL, вы заметите, что существует много памяти поддержка макетов встроена в IL/CLR.

Ответ 4

Так как сборщик мусора является нетипизированным и только различает ссылки на объекты и простые биты, перекрывающиеся ссылки не будут его путать. Однако, хотя одна ссылка на объект может полностью перекрывать другую, это не поддается проверке, а также небезопасно (стандарт ECMA-335, стр. 180, II.10.7. Легко построить программу, которая использует эту невоспроизводимость для ужасающего краха:

using System.Runtime.InteropServices;

class Bar
{
    public virtual void func() { }
}

[StructLayout(LayoutKind.Explicit)]
struct Overlaid
{
    [FieldOffset(0)]
    public object foo;

    [FieldOffset(0)]
    public Bar bar;
}

class Program
{
    static void Main(string[] args)
    {
        var overlaid = new Overlaid();
        overlaid.foo = new object();
        overlaid.bar.func();
    }
}

Здесь вызов func загружает указатель на функцию из одного последнего элемента виртуальной таблицы класса объекта. В соответствии с этой статьей после vtbl есть таблица дескрипторов. Обработка его как указателя функции приводит к исключению System.AccessViolationException.

Ответ 5

Я не знаю никаких проблем с этим. Кроме того, я сомневаюсь, что Microsoft разрешит это использование, если это было опасно неочевидным образом.