C#中不安全的值类型数组转换为字节数组

10

我使用一个扩展方法将浮点数组转换为字节数组:

public static unsafe byte[] ToByteArray(this float[] floatArray, int count)
{
    int arrayLength = floatArray.Length > count ? count : floatArray.Length;
    byte[] byteArray = new byte[4 * arrayLength];
    fixed (float* floatPointer = floatArray)
    {
        fixed (byte* bytePointer = byteArray)
        {
            float* read = floatPointer;
            float* write = (float*)bytePointer;
            for (int i = 0; i < arrayLength; i++)
            {
                *write++ = *read++;
            }
        }
    }
    return byteArray;
}

我理解数组是一个指向与元素类型和数量相关联的内存的指针。另外,对我来说,似乎没有办法在不复制数据的情况下将数组与字节数组进行转换。

我理解得对吗?甚至是否不可能编写 IL 从指针、类型和长度创建数组而不复制数据?

编辑:感谢答案,我学到了一些基础知识,并尝试了新技巧!

最初接受了 Davy Landman 的答案后,我发现他的 StructLayout 黑科技虽然可以将字节数组转换为浮点数数组,但无法实现反向转换。举个例子:

[StructLayout(LayoutKind.Explicit)]
struct UnionArray
{
    [FieldOffset(0)]
    public Byte[] Bytes;

    [FieldOffset(0)]
    public float[] Floats;
}

static void Main(string[] args)
{
    // From bytes to floats - works
    byte[] bytes = { 0, 1, 2, 4, 8, 16, 32, 64 };
    UnionArray arry = new UnionArray { Bytes = bytes };
    for (int i = 0; i < arry.Bytes.Length / 4; i++)
        Console.WriteLine(arry.Floats[i]);

    // From floats to bytes - index out of range
    float[] floats = { 0.1f, 0.2f, 0.3f };
    arry = new UnionArray { Floats = floats };
    for (int i = 0; i < arry.Floats.Length * 4; i++)
        Console.WriteLine(arry.Bytes[i]);
}

似乎CLR将这两个数组都视为具有相同的长度。如果结构体是由浮点数据创建的,则字节数组的长度就太短了。


除了问题之外,如果您使用Math.Min(count, floatArray.Length),您的第一条语句将更清晰。 - Hosam Aly
7个回答

17
你可以使用一种非常丑陋的hack技巧,通过内存操作将数组临时更改为byte[]。
这种方法非常快速和高效,因为它不需要克隆数据并进行迭代。
我在32位和64位操作系统中测试过这个技巧,所以它应该是可移植的。
源代码和示例用法可以在 https://gist.github.com/1050703 上找到,但为了你的方便,我也会在这里粘贴。
public static unsafe class FastArraySerializer
{
    [StructLayout(LayoutKind.Explicit)]
    private struct Union
    {
        [FieldOffset(0)] public byte[] bytes;
        [FieldOffset(0)] public float[] floats;
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private struct ArrayHeader
    {
        public UIntPtr type;
        public UIntPtr length;
    }

    private static readonly UIntPtr BYTE_ARRAY_TYPE;
    private static readonly UIntPtr FLOAT_ARRAY_TYPE;

    static FastArraySerializer()
    {
        fixed (void* pBytes = new byte[1])
        fixed (void* pFloats = new float[1])
        {
            BYTE_ARRAY_TYPE = getHeader(pBytes)->type;
            FLOAT_ARRAY_TYPE = getHeader(pFloats)->type;
        }
    }

    public static void AsByteArray(this float[] floats, Action<byte[]> action)
    {
        if (floats.handleNullOrEmptyArray(action)) 
            return;

        var union = new Union {floats = floats};
        union.floats.toByteArray();
        try
        {
            action(union.bytes);
        }
        finally
        {
            union.bytes.toFloatArray();
        }
    }

    public static void AsFloatArray(this byte[] bytes, Action<float[]> action)
    {
        if (bytes.handleNullOrEmptyArray(action)) 
            return;

        var union = new Union {bytes = bytes};
        union.bytes.toFloatArray();
        try
        {
            action(union.floats);
        }
        finally
        {
            union.floats.toByteArray();
        }
    }

    public static bool handleNullOrEmptyArray<TSrc,TDst>(this TSrc[] array, Action<TDst[]> action)
    {
        if (array == null)
        {
            action(null);
            return true;
        }

        if (array.Length == 0)
        {
            action(new TDst[0]);
            return true;
        }

        return false;
    }

    private static ArrayHeader* getHeader(void* pBytes)
    {
        return (ArrayHeader*)pBytes - 1;
    }

    private static void toFloatArray(this byte[] bytes)
    {
        fixed (void* pArray = bytes)
        {
            var pHeader = getHeader(pArray);

            pHeader->type = FLOAT_ARRAY_TYPE;
            pHeader->length = (UIntPtr)(bytes.Length / sizeof(float));
        }
    }

    private static void toByteArray(this float[] floats)
    {
        fixed(void* pArray = floats)
        {
            var pHeader = getHeader(pArray);

            pHeader->type = BYTE_ARRAY_TYPE;
            pHeader->length = (UIntPtr)(floats.Length * sizeof(float));
        }
    }
}

用法如下:

var floats = new float[] {0, 1, 0, 1};
floats.AsByteArray(bytes =>
{
    foreach (var b in bytes)
    {
        Console.WriteLine(b);
    }
});

3
这里有一篇更新更加便携的“一年后”的答案,链接为:https://dev59.com/fXRB5IYBdhLWcg3wc280#3577253 。@Omer - 或许你也可以更新一下这个回答? - Cristian Diaconescu
谢谢! :) 另外,你应该为那段代码添加一个许可证。CodingHorror 解释了为什么 - Cristian Diaconescu
没错。我在代码片段中添加了一个FreeBSD许可证。SO上的代码已经有了许可证。请查看页脚:“用户贡献根据cc-wiki许可证授权,需要署名”。 - Omer Mor

2

是的,类型信息和数据都在同一内存块中,因此如果您重写浮点数组中的类型信息以欺骗系统将其视为字节数组,则这是不可能的。这将是一个非常丑陋的黑客行为,并且很容易引起问题...

以下是如何在没有使用不安全代码的情况下转换浮点数的方法:

public static byte[] ToByteArray(this float[] floatArray) {
    int len = floatArray.Length * 4;
    byte[] byteArray = new byte[len];
    int pos = 0;
    foreach (float f in floatArray) {
        byte[] data = BitConverter.GetBytes(f);
        Array.Copy(data, 0, byteArray, pos, 4);
        pos += 4;
    }
    return byteArray;
}

1
如果您对建议的黑客感兴趣,请查看下面我的答案中的实现: https://dev59.com/SXRB5IYBdhLWcg3wcm6d#3577227 - Omer Mor
使用Buffer.BlockCopy(floatArray, 0, data, 0, data.Length)可能比使用foreachBitConverter更快。 - Ray

2

我曾经为快速转换数组编写了类似的东西。它基本上是一个丑陋的概念证明,而不是一个漂亮的解决方案。 ;)

public static TDest[] ConvertArray<TSource, TDest>(TSource[] source)
    where TSource : struct
    where TDest : struct {

    if (source == null)
        throw new ArgumentNullException("source");

        var sourceType = typeof(TSource);
        var destType = typeof(TDest);

        if (sourceType == typeof(char) || destType == typeof(char))
            throw new NotSupportedException(
                "Can not convert from/to a char array. Char is special " +
                "in a somewhat unknown way (like enums can't be based on " +
                "char either), and Marshal.SizeOf returns 1 even when the " +
                "values held by a char can be above 255."
            );

        var sourceByteSize = Buffer.ByteLength(source);
        var destTypeSize = Marshal.SizeOf(destType);
        if (sourceByteSize % destTypeSize != 0)
            throw new Exception(
                "The source array is " + sourceByteSize + " bytes, which can " +
                "not be transfered to chunks of " + destTypeSize + ", the size " +
                "of type " + typeof(TDest).Name + ". Change destination type or " +
                "pad the source array with additional values."
            );

        var destCount = sourceByteSize / destTypeSize;
        var destArray = new TDest[destCount];

        Buffer.BlockCopy(source, 0, destArray, 0, sourceByteSize);

        return destArray;
    }
}

2

1
    public byte[] ToByteArray(object o)
    {
        int size = Marshal.SizeOf(o);
        byte[] buffer = new byte[size];
        IntPtr p = Marshal.AllocHGlobal(size);
        try
        {
            Marshal.StructureToPtr(o, p, false);
            Marshal.Copy(p, buffer, 0, size);
        }
        finally
        {
            Marshal.FreeHGlobal(p);
        }
        return buffer;
    }

这可能会帮助您将对象转换为字节数组。


1

0

如果你仍然对这个黑客攻击感兴趣,那么请查看这段修改过的代码 - 它非常好用,而且几乎不需要时间成本,但是由于它是一种黑客攻击方式,可以在没有信任要求和不安全标记的情况下获得对整个进程地址空间的完全访问权限,所以未来可能无法使用。

    [StructLayout(LayoutKind.Explicit)]
    struct ArrayConvert
    {
        public static byte[] GetBytes(float[] floats)
        {
            ArrayConvert ar = new ArrayConvert();
            ar.floats = floats;
            ar.length.val = floats.Length * 4;
            return ar.bytes;
        }
        public static float[] GetFloats(byte[] bytes)
        {
            ArrayConvert ar = new ArrayConvert();
            ar.bytes = bytes;
            ar.length.val = bytes.Length / 4;
            return ar.floats;
        }

        public static byte[] GetTop4BytesFrom(object obj)
        {
            ArrayConvert ar = new ArrayConvert();
            ar.obj = obj;
            return new byte[]
            {
                ar.top4bytes.b0,
                ar.top4bytes.b1,
                ar.top4bytes.b2,
                ar.top4bytes.b3
            };
        }
        public static byte[] GetBytesFrom(object obj, int size)
        {
            ArrayConvert ar = new ArrayConvert();
            ar.obj = obj;
            ar.length.val = size;
            return ar.bytes;
        }

        class ArrayLength
        {
            public int val;
        }
        class Top4Bytes
        {
            public byte b0;
            public byte b1;
            public byte b2;
            public byte b3;
        }

        [FieldOffset(0)]
        private Byte[] bytes;
        [FieldOffset(0)]
        private object obj;
        [FieldOffset(0)]
        private float[] floats;

        [FieldOffset(0)]
        private ArrayLength length;

        [FieldOffset(0)]
        private Top4Bytes top4bytes;
    }

这些黑客正在破坏内部垃圾收集器数据结构。这将导致间歇性崩溃、数据损坏和与C++中的use-after-free相同类别的安全漏洞。像这样黑客攻击内部垃圾收集器数据结构绝对不受.NET运行时支持。http://github.com/HelloKitty/Reinterpret.Net/issues/1有一个长时间的讨论,关于这种黑客攻击将导致的崩溃。 - Jan Kotas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接