Изменение типа массива приводит к падению после сборки мусор
От: Albeoris  
Дата: 22.08.15 14:27
Оценка: 9 (2)
Доброго времени суток!

Как известно, в CLR у каждого массива есть заголовок — два IntPtr, один из которых указывает на тип элемента массива, а другой описывает его размер.
Логично предположить, что если изменить тип элементов массива, скажем с byte на int, а размер массива, соответственно, уменьшить в 4 раза, при условии, что он кратен 4, упаковать его в (object) и распаковать, как (int[]), мы получим массив нового типа без какого-либо копирования элементов. Быстро и удобно.

В большинстве случаев это не требуется, и можно обойтись unsafe/fixed и кастом типа указателей.
Но далеко не все методы .NET умеют работать с указателями. Наглядный пример — методы Read/Write потоков.

Но есть проблема — работает вышеописанный способ лишь до первой сборки мусора. Как только GC пытается удалить осиротевший массив, претерпевший подобное изнасилование, приложение падает.
Вопрос — чего не хватает? Как научить GC работать со старыми массивами, у которых изменился тип?

  Текущая реализация грязного хака
int[] result = _input.DungerousReadStructs<Int32>(entry.Length / 4);



public static T[] DungerousReadStructs<T>(this Stream input, int count) where T : struct
{
    if (count < 1)
        return new T[0];

    Array result = new T[count];
    Int32 entrySize = UnsafeTypeCache<T>.UnsafeSize;
    using (UnsafeTypeCache<byte>.ChangeArrayType(result, entrySize))
        input.EnsureRead((byte[])result, 0, result.Length);

    return (T[])result;
}



public static class TypeCache<T>
{
    public static readonly Type Type = typeof(T);
}

public static class UnsafeTypeCache<T>
{
    public static readonly Int32 UnsafeSize = GetSize();
    public static readonly UIntPtr ArrayTypePointer = GetArrayTypePointer();

    private static Int32 GetSize()
    {
        DynamicMethod dynamicMethod = new DynamicMethod("SizeOf", typeof(Int32), Type.EmptyTypes);
        ILGenerator generator = dynamicMethod.GetILGenerator();

        generator.Emit(OpCodes.Sizeof, TypeCache<T>.Type);
        generator.Emit(OpCodes.Ret);

        return ((Func<int>)dynamicMethod.CreateDelegate(typeof(Func<Int32>)))();
    }

    private static unsafe UIntPtr GetArrayTypePointer()
    {
        T[] result = new T[1];
        using (SafeGCHandle handle = new SafeGCHandle(result, GCHandleType.Pinned))
            return *(((UIntPtr*)handle.AddrOfPinnedObject().ToPointer()) - 2);
    }

    public static IDisposable ChangeArrayType(Array array, Int32 oldElementSize)
    {
        if (array.Length < 1)
            throw new NotSupportedException();

        SafeGCHandle handle = new SafeGCHandle(array, GCHandleType.Pinned);
        try
        {
            unsafe
            {
                UIntPtr* arrayPointer = (UIntPtr*)handle.AddrOfPinnedObject().ToPointer();
                UIntPtr arrayLength = *(arrayPointer - 1);
                UIntPtr arrayType = *(arrayPointer - 2);
                UInt64 arraySize = ((UInt64)arrayLength * (UInt64)oldElementSize);

                if (arraySize % (UInt64)UnsafeSize != 0)
                    throw new InvalidCastException();

                try
                {
                    *(arrayPointer - 1) = new UIntPtr(arraySize / (UInt64)UnsafeSize);
                    *(arrayPointer - 2) = ArrayTypePointer;

                    return new DisposableAction(() =>
                    {
                        *(arrayPointer - 1) = arrayLength;
                        *(arrayPointer - 2) = arrayType;
                        handle.Dispose();
                    });
                }
                catch
                {
                    *(arrayPointer - 1) = arrayLength;
                    *(arrayPointer - 2) = arrayType;
                    throw;
                }
            }
        }
        catch
        {
            handle.SafeDispose();
            throw;
        }
    }
}


Работает. А вот если перед тем, как я дёрну "Free" не вернуть значения типа и длины к оригинальным, сборка мусора крашет приложение.
Вот и возник вопрос — отчего так? Помимо заголовка архива, тип объекта хранится где-то ещё? (н.п. в объявлении локальной переменной?). Можно ли и его изменить?

Также интересует — как в такой случае правильно описывать структуры? Marshaling работать не будет — это понятно. Соответственно, никаких ссылочных типов.
Вопрос: Как прикрутить собственный маршалинг, н.п. для корректного распознавания строк в той или иной кодировке? Вложенных массивов (аналог ByValStr, ByValArray)
Вопрос: Нужно ли структурам задавать аттрбиут StructLayout и будет ли он использоваться (или это часть механизма маршалинга?)
Вопрос: К чему приведёт использование подобного хака в совокупности со stackalloc?
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Отредактировано 22.08.2015 16:37 Albeoris . Предыдущая версия . Еще …
Отредактировано 22.08.2015 16:33 Albeoris . Предыдущая версия .
c# cshapr native pointer hack gc internal array crash
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.