Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

CLR中处理Union语义的限制与回避方法

Posted on 2004-07-08 11:09  Flier Lu  阅读(2676)  评论(1编辑  收藏  举报
http://www.blogcn.com/user8/flier_lu/index.html?id=1670460&run=.0FB98AB

CLR中通过预定义属性(Attribute)为值类型结构的定义提供了很大的灵活性,基本上可以很灵活地处理绝大部分原有Win32 API和COM接口的定义。
     
     对没有显式指定的类和结构,C#及其它编译器有权利任意更改字段定义顺序,以优化占用空间和访问时间。例如一个结构
 
以下为引用:

 struct A
 {
   short a;
   int b;
   byte c;
   byte d;
 }
 

    
     在缺省情况下使用的是LayoutKind.Auto模式,编译器可以任意重排字段顺序,如
 
以下为引用:

 struct A
 {
   int b;
   short a;
   byte c;
   byte d;
 }
 

 
     以保障字节对齐,在不增加填补(padding)空间的同时提高访问效率。
     但这样一来就无法与C++/COM等定义的结构兼容,因此需要显式指定排列方式为LayoutKind.Sequential或LayoutKind.Explicit。前者要求编译器保障内存中字段顺序与定义的相同,按照StructLayoutAttribute.Pack进行对齐;后者则通过FieldOffsetAttribute预定义属性显式定义每个字段的位置,可以用于模拟Union语义。
     例如前面例子可以强制要求结构保持字段顺序同时按1字节对齐
 
以下为引用:

 [StructLayout(LayoutKind.Sequential, Pack=1)]
 struct A
 {
   short a;
   int b;
   byte c;
   byte d;
 }
 

   
     而通过 LayoutKind.Explicit 模式则可以模拟 Union 语义
 
以下为引用:

 union MyUnion
 {
     int number;
     double d;
 }
 

    
 
以下为引用:

 [ StructLayout( LayoutKind.Explicit )]
 public struct MyUnion 
 {
    [ FieldOffset( 0 )]
    public int i;
    [ FieldOffset( 0 )]
    public double d;
 }
 

    
     但这里的 Union 语义受到一定的限制:在被 Union 语义使用的字段或结构中,必须很小心地使用引用类型或 Marshal 形式的字段,因为 CLR 不允许值类型和引用类型在内存布局上重叠。准确的说是 LayoutKind.Explicit 这种内存布局中不能有引用类型字段与值类型字段重叠,而引用类型字段与引用类型字段,或者值类型字段与值类型字段完全重叠都是允许的。
     
     实际上 C# 中提供了很多 syntax sweet 辅助用户处理常见数据结构,例如对数组和字符串的处理就提供了很全面的支持,如
 
以下为引用:

 typedef struct _MYSTRSTRUCT
 {
    wchar_t* buffer;
    UINT size; 
 } MYSTRSTRUCT;

 struct UnmanagedInformation {
   int num;
   char* string;
   int array[32];
 };
 


    
     可以通过 PInvoke 内建支持很容易转换为 Managed 结构定义
 
以下为引用:

 [ StructLayout( LayoutKind.Sequential, CharSet=CharSet.Unicode )]
 public struct MyStrStruct 
 {  
    public String buffer;
    public int size;
 }

 [StructLayout( LayoutKind.Sequential, CharSet=CharSet.Ansi )]
 struct ManagedInformation {
   public int num;
   public string str;
   [MarshalAs (UnmanagedType.ByValArray, SizeConst=32)]
   public int[] array[];
 }
 


    
     这种特殊的语法现象,实际上是使用引用类型的语法来操作值类型的语义。例如上面通过 [MarshalAs (UnmanagedType.ByValArray, SizeConst=32)] 定义的数组,在 CLR 中语法上是作为一个引用类型存在的。
 
以下为引用:

 .field public  marshal( fixed array [32]) int32[] 'array'
 

    
     使用的时候同样需要先 info.array = new int[32]; 然后才能使用;但同时它在语义上定义了 ManagedInformation 结构中的一个连续内存区域。
 
以下为引用:

 void test2()
 {
   ManagedInformation info = new ManagedInformation();

   info.num = 3;
   info.str = "Test";
   info.array = new int[32];
   for(int i=0; i<info.array.Length; i++)
     info.array[i] = i+1;

   IntPtr pInfo = Marshal.AllocHGlobal(Marshal.SizeOf(info));
   Marshal.StructureToPtr(info, pInfo, false);
   Marshal.FreeHGlobal(pInfo);
 }
 


    
     从上面的例子可以看到 info.str 和 info.array 的使用完全和引用类型一致,但在执行完 Marshal.StructureToPtr 后,在调试器的数据窗口直接查看 pInfo 指向的内存区域,就会发现 info.str 保存了一个指向 "Test" 字符串的 Unmanaged 指针,而 info.array 是一个固定长度的连续地址数组。
     
     在平时这种 syntax sweet 可以大大简化用户的操作,但是在使用 Union 语义的时候就不成了,因为这种用引用类型对值类型的包装,是无法与其他值类型字段在内存上重叠的。做这样限定的原因,可能是因为对 GC 来说需要确定地知道某个字段是否为引用类型,CLR不能给 GC 一个具有二义性的语义。而 Marshal 的字段实际上也是一个引用字段,故而受到同样的限制。因为当一个引用类型字段与值类型字段重叠的时候就会出现二义性的问题;而值类型字段互相重叠(GC看来此内存还是保存一个值类型),或者引用类型字段互相重叠不存在二义性的问题。
     
     也就是说类似 DEBUG_EVENT 的结构,是无法在C#中直接通过正常语法定义的。
 
以下为引用:

 typedef struct _EXCEPTION_RECORD {  
   DWORD ExceptionCode;  
   DWORD ExceptionFlags;  
   struct _EXCEPTION_RECORD* ExceptionRecord;  
   PVOID ExceptionAddress;  
   DWORD NumberParameters;  
   ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

 typedef struct _EXCEPTION_DEBUG_INFO {  
   EXCEPTION_RECORD ExceptionRecord;  
   DWORD dwFirstChance;
 } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

 typedef struct _DEBUG_EVENT {  
   DWORD dwDebugEventCode;  
   DWORD dwProcessId;  
   DWORD dwThreadId;  
   union {    
     EXCEPTION_DEBUG_INFO Exception;    
     CREATE_THREAD_DEBUG_INFO CreateThread;    
     CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;    
     EXIT_THREAD_DEBUG_INFO ExitThread;    
     EXIT_PROCESS_DEBUG_INFO ExitProcess;    
     LOAD_DLL_DEBUG_INFO LoadDll;    
     UNLOAD_DLL_DEBUG_INFO UnloadDll;    
     OUTPUT_DEBUG_STRING_INFO DebugString;    
     RIP_INFO RipInfo;  
   } u;
 } DEBUG_EVENT, *LPDEBUG_EVENT;
 


    
     如果定义成类似上面 Union 的形式,如
 
以下为引用:

 [StructLayout(LayoutKind.Sequential)]
   public struct EXCEPTION_RECORD 
 {
   public const int EXCEPTION_MAXIMUM_PARAMETERS = 15;

   public int ExceptionCode;
   public uint ExceptionFlags;
   public IntPtr ExceptionRecord;
   public IntPtr ExceptionAddress;
   public uint NumberParameters;

   [MarshalAs(UnmanagedType.ByValArray, SizeConst=EXCEPTION_MAXIMUM_PARAMETERS)]
   public uint[] ExceptionInformation;
 }

 [StructLayout(LayoutKind.Sequential)]
   public struct EXCEPTION_DEBUG_INFO 
 {
   public EXCEPTION_RECORD ExceptionRecord;
   public uint dwFirstChance;
 } 

 [StructLayout(LayoutKind.Explicit)]
 public struct DEBUG_EVENT
 {
   [FieldOffset(0)] public uint dwDebugEventCode;
   [FieldOffset(4)] public uint dwProcessId;
   [FieldOffset(8)] public uint dwThreadId;
   
   [FieldOffset(12)] public EXCEPTION_DEBUG_INFO Exception;      
   [FieldOffset(12)] public CREATE_THREAD_DEBUG_INFO CreateThread;
   [FieldOffset(12)] public CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
   [FieldOffset(12)] public EXIT_THREAD_DEBUG_INFO ExitThread;
   [FieldOffset(12)] public EXIT_PROCESS_DEBUG_INFO ExitProcess;
   [FieldOffset(12)] public LOAD_DLL_DEBUG_INFO LoadDll;
   [FieldOffset(12)] public UNLOAD_DLL_DEBUG_INFO UnloadDll;
   [FieldOffset(12)] public OUTPUT_DEBUG_STRING_INFO DebugString;
   [FieldOffset(12)] public RIP_INFO RipInfo;
 }
 


    
     则在动态构造 DEBUG_EVENT 时会引发异常,在静态构造时程序进入函数前就异常。异常信息如下:
 
以下为引用:

 An unhandled exception of type 'System.TypeLoadException' occurred in Debugger.exe

 Additional information: 未能从程序集 Debugger, Version=1.0.1572.568, Culture=neutral, PublicKeyToken=null 中加载类型 DEBUG_EVENT,因为它在 16 偏移位置处包含一个对象字段,该字段已由一个非对象字段不正确地对齐或重叠。
 


    
     最可恨的是如果静态构造时,程序根本就在构造函数堆栈时歇菜了,不进入函数体也不抛出异常。害得我晚上调试时以为是自己程序的问题,把代码改的乱七八糟,最后只能靠屏蔽代码发现这个问题 :( 
     例如下面的简单代码会导致函数无法进入但也没有异常抛出,就看到 CPU 到了 100%,几秒钟后就一切正常,但函数并没有被调用。
 
以下为引用:

 class Test
 {
   void DoSomething()
   {
     Win32.DEBUG_EVENT evtHeader;
   }
 }
 

    
     直接查看 IL 代码也没有什么异常用法,问题可能出在 JIT 载入类型的时候 :( 不知道这算不算 .NET Framework 1.1 的一个 bug。IL 代码如下,很简单,问题应该出在载入类型构造堆栈的时候
 
以下为引用:

 .method public hidebysig newslot virtual final 
         instance void  DebugEvent(unsigned int32 pDebugEvent,
                                   int32 fOutOfBand) cil managed
 {
   // Code size       2 (0x2)
   .maxstack  0
   .locals init ([0] valuetype Debugger.Utils.Win32/DEBUG_EVENT evtHeader)
   IL_0000:  nop
   IL_0001:  ret
 } // end of method UnmanagedEventListener::DebugEvent
 

    
     MSDN上举例时使用的方式是为 Union 语义的每种情况定义一个单独的结构,但这样代码太过繁琐。我现在采用的是通过设计的方式回避这个问题,根据DEBUG_EVENT.dwDebugEventCode动态判断其后字段的内容,再手工转换。
     
     我首先定义一个DEBUG_EVENT_HEADER类型,然后在代码中先转换这一部分内容
 
以下为引用:

 public class Win32 
 {
   [StructLayout(LayoutKind.Sequential)]
     public struct DEBUG_EVENT_HEADER
   {
     public uint dwDebugEventCode;
     public uint dwProcessId;
     public uint dwThreadId;
   }
 }

 // ...
     
 public class UnmanagedEventListener
 {
   public void DebugEvent(uint pDebugEvent, int fOutOfBand)
   {
     IntPtr pEvent = new IntPtr(pDebugEvent);
     
     Win32.DEBUG_EVENT_HEADER evtHeader = (Win32.DEBUG_EVENT_HEADER)Marshal.PtrToStructure(
       new IntPtr(pDebugEvent), typeof(Win32.DEBUG_EVENT_HEADER));
      
    // ...
   }
 }

 // ...
 



     这里的 pDebugEvent 参数是一个指针,我使用 new IntPtr(pDebugEvent) 将之转换为 Managed 指针,传递给 Marshal.PtrToStructure 函数,从一个 Unmanaged 内存指针读入一个结构到 Managed 值类型对象。然后就可以根据事件类型进行处理,如
 
以下为引用:

 public class UnmanagedEventListener
 {
   public void DebugEvent(uint pDebugEvent, int fOutOfBand)
   {
     // ...
     
     IntPtr pEventData = new IntPtr(pDebugEvent + Marshal.SizeOf(evtHeader));
     
     switch(evtHeader.dwDebugEventCode)
     {
       case Win32.CREATE_PROCESS_DEBUG_EVENT:
       {
         ProcessEvent(ctxt, (Win32.CREATE_PROCESS_DEBUG_INFO)
           Marshal.PtrToStructure(pEventData, typeof(Win32.CREATE_PROCESS_DEBUG_INFO)));          
         break;
       }
       case Win32.EXIT_PROCESS_DEBUG_EVENT:
       {
         ProcessEvent(ctxt, (Win32.EXIT_PROCESS_DEBUG_INFO)
         Marshal.PtrToStructure(pEventData, typeof(Win32.EXIT_PROCESS_DEBUG_INFO)));
         break;
       }     
       // ... 
     } 
     // ...
   } 
   // ...
 }
 

    
     这样一来就可以通过指针操作模拟 Union 语义,但远不如直接用属性定义看起来舒服,呵呵
     
     一个稍微好一点的解决方法是自定义一些属性,标记 DEBUG_EVENT 结构里那些 Union 中的结构,然后通过 Reflection 完成读取和转换工作,代码比较繁琐,这里就不一一列举了。
     
     但是实际上在传统C++语言中这种用法是大量存在,例如在处理复杂网络协议和文件结构的程序里面,多层 Union 加上数组的结构是无法避免的。而 CLR 在处理结构的时候,实际上可以通过进一步将之细分为运行时内存布局和存储时内存布局来解决这个问题,以免用户通过较为 dirty 的方式模拟这个语义。但可惜到最新的 .NET Framework 2.0 都还是存在这个限制。
     
     
     关于结构类型定义的方法下面这篇文章里面有非常详细的讲解

     Everything you (n)ever wanted to know about Marshalling (and were afraid to ask!)
     
     此外有个 Wiki 项目 PInvoke.NET 也还不错,可以直接查询到很多 Win32 API 的定义。不过最终解决方法还是写个可以编译 .h/.idl 文件到 CLR 格式定义的编译器出来,不知道谁搞定做这个造福全人类的东东啊,呵呵

 btw: 感谢MSN上的朋友 笨笨 (-_-b)和水木网友 Nineteen 提示我找出真正问题所在 :P