欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C# 如何创建一个超大(8MB)联合体

程序员文章站 2022-07-12 23:15:50
...

在项目上遇到了一个问题,我维护的网络视频播放客户端老是因为C++解码库的异常导致崩溃,所以决定把解码过程直接隔离到子进程里。这样:

  • 我需要频繁的把主进程接收的视频帧数据,转发到子进程里解码渲染;
  • 我需要偶尔与解码进程通信,如录像回放中命令解码器调整播放速度,或者接收解码器的帧缓存警告以调整视频流的转发速度。

因此,我需要一种在主子进程间传递消息的结构,它应该:

  1. 能快速序列化/反序列化;
  2. 能快速判断消息类型;
  3. 能承载大数据量;
  4. 能方便的把数据共享给C++解码库,并能快速释放。

对于目标4,因为托管堆上的内存由GC管理,且C++不能直接访问,所以考虑直接在非托管内存中申请内存,并自己释放内存。

对于目标1和2,考虑用C# 精确控制内存布局的结构体 来实现,通过精确控制内存布局,可以实现和 C++联合体 相似的数据结构。

对于目标3,C#要创建超大联合体,需要用到指针,因此需要在项目属性-生成里勾选允许不安全代码选项。

    /// <summary>
    /// 表示一个8M大小的信令
    /// </summary>
    [StructLayout(LayoutKind.Explicit)]
    internal unsafe struct M8Token
    {
        [FieldOffset(0)]
        public int tokenType;

        [FieldOffset(4)]
        public int dataLength;

        [FieldOffset(0)]
        public fixed byte data[1 << 23];
    }

上面的代码中,StructLayout用于定义结构体的内存布局方式;LayoutKind.Explicit表示精确布局,即由FieldOffset指定结构体里每个字段在内存中对齐位置,单位为字节。

    该结构体内存的0-3字节组成了tokenType,4-7字节组成了dataLength;从第8字节起,就是其携带的数据。

    有意思的是data字段,这是一个byte指针,指向一个8MB大小的byte数组。FieldOffset对齐到0字节表示data指针指向结构体内存块的0号字节,因此M8Token结构体的大小就是8MB。

    序列化时,只需通过data指针读取M8Token对象所有字节值;反序列化时,只需通过data指针按序填充byte数组就行了。

        private static M8Token _token;

        private static unsafe void Main(string[] args)
        {
            int sz = Marshal.SizeOf(typeof(M8Token));
            _token = new M8Token();
            // M8Token tmpToken = new M8Token(); // Error:*!!
            M8Token* token1 = (M8Token*)Marshal.AllocHGlobal(sz);
            token1->tokenType = 55;
            token1->dataLength = 10;

            byte* pbData1 = token1->data;
            for (int i = 0; i < 10; i++)
            {
                pbData1[i + 8] = (byte)i;
                // 或者
                //*(pbData1 + i + 8) = (byte)i;
            }

            M8Token* token2 = (M8Token*)Marshal.AllocHGlobal(sz);
            int dataLen = token1->dataLength + 8;
            byte* pbData2 = token2->data;
            for (int i = 0; i < dataLen; i++)
            {
                *(pbData2 + i) = *(pbData1 + i);
            }

            Console.WriteLine($"token type:{token2->tokenType}");
            Console.WriteLine($"data len:{token2->dataLength}");

            IntPtr dataPtr = (IntPtr)(pbData2 + 8);
            SendSliceToCpp(dataPtr, token2->dataLength);

            Marshal.FreeHGlobal((IntPtr)token1);
            Marshal.FreeHGlobal((IntPtr)token2);
            Console.ReadLine();
        }

        private static void SendSliceToCpp(IntPtr dataPtr, int dataLen)
        {
            unsafe
            {
                byte* pbDataArr = (byte*)dataPtr;
                Console.Write("data:");
                for (int i = 0; i < dataLen; i++)
                {
                    Console.Write("," + pbDataArr[i]);
                }
            }
            // 调用C++函数
        }

结果:

token type:55
data len:10
data:,0,1,2,3,4,5,6,7,8,9

    需要注意的是,M8Token大小为1<<23字节,也就是8MB,远大于.NET默认堆栈大小,直接在方法体里new一个局部变量会导致*异常。但是可以给一个类的字段new一个M8Token对象,这时M8Token对象在托管堆上分配内存所以不会导致*。

    通过Marshal.AllocHGlobal(int size)申请的非托管内存块并返回该内存块的指针。实际上这个指针没有类型,我们按照M8Token的结构填充数据,C++代码可以直接通过这个指针访问内存中的数据。

    当用完后,通过Marshal.FreeHGlobal(Intptr)立即释放非托管内存,防止内存泄漏。

 

相关标签: C# C# 指针