預設封送處理行為

Interop 封送處理會針對規則運作,以決定與方法參數相關聯的數據在 Managed 與 Unmanaged 記憶體之間傳遞時的行為。 這些內建規則可控制數據型別轉換之類的封送處理活動、被呼叫者是否可以變更傳遞給它的數據,並將這些變更傳回給呼叫端,以及在這種情況下,封送器會提供效能優化。

本節會識別 Interop 封送處理服務的預設行為特性。 它提供封送陣列、布爾型別、char 類型、委派、類別、物件、字串和結構的詳細資訊。

注意

不支援泛型類型的封送處理。 如需詳細資訊,請參閱使用泛型型別互通

使用 Interop 封送處理器進行記憶體管理

Interop 封送器一律會嘗試釋放 Unmanaged 程式代碼所配置的記憶體。 這個行為使用 COM 記憶體管理規則進行編譯,但與管理原生 C++ 的規則不同。

如果您使用平台叫用 (會自動釋放指標的記憶體) 時,預期原生 C++ 行為 (不釋放記憶體),則會發生混淆。 例如,從 C++ DLL 呼叫下列 Unmanage 方法不會自動釋放任何記憶體。

Unmanaged 簽章

BSTR MethodOne (BSTR b) {  
     return b;  
}  

不過,如果您將方法定義為平台叫用原型、將每個 BSTR 類型取代為 String 類型,並呼叫 MethodOne,則 Common Language Runtime 會嘗試釋放 b 兩次。 您可以使用類型,而不是 String 類型來變更封送處理行為IntPtr

運行時間一律會在 Windows 上使用 CoTaskMemFree 方法,在其他 平臺上使用免費 方法釋放記憶體。 如果您使用的記憶體未配置於 Windows 上的 CoTaskMemAlloc 方法,或 在其他平臺上使用 malloc 方法,您必須使用 IntPtr ,並使用適當的方法手動釋放記憶體。 同樣地,您可以在絕不應該釋放記憶體的情況下避免自動釋放記憶體;例如,從 Kernel32.dll 使用 GetCommandLine 函式,該函式會傳回核心記憶體的指標。 如需手動釋放記憶體的詳細資訊,請參閱緩衝區範例

類別的預設封送處理

類別只能由 COM Interop 封送處理,並且一律會封送處理為介面。 在某些情況下,用來封送處理類別的介面就是所謂的類別介面。 如需以您選擇的介面來覆寫類別介面的相關資訊,請參閱類別介面簡介

將類別傳遞給 COM

當 Managed 類別傳遞至 COM 時,Interop 封送器會自動將類別包裝為 COM Proxy,並將 Proxy 所產生的類別介面傳遞至 COM 方法呼叫。 Proxy 接著會將類別介面上的所有呼叫重新委派給 Managed 物件。 Proxy 也會公開類別未明確實作的其他介面。 Proxy 會代表類別自動實作 IUnknownIDispatch 這類介面。

將類別傳遞給 .NET 程式碼

一般而言,Coclass 在 COM 中不會做為方法引數, 通常會以預設介面代替 Coclass 傳遞。

當介面傳遞至 Managed 程式代碼時,Interop 封送器會負責使用適當的包裝函式包裝介面,並將包裝函式傳遞至 Managed 方法。 您可能很難決定要使用哪個包裝函式。 不論物件實作多少個介面,COM 物件的每個執行個體都只會有一個包裝函式。 例如,實作五個不同介面的單一 COM 物件只會有一個包裝函式。 同一個包裝函式會公開所有五個介面。 如果建立 COM 物件的兩個執行個體,則會建立包裝函式的兩個執行個體。

若要讓包裝函式在整個存留期內維持相同的類型,Interop 封送器必須在物件第一次通過封送器傳遞介面時識別正確的包裝函式。 封送器會藉由查看物件實作的其中一個介面來識別物件。

例如,封送器會判斷類別包裝函式應該用來包裝傳入 Managed 程式代碼的介面。 當介面第一次通過封送器時,封送器會檢查介面是否來自已知的物件。 這項檢查會發生於下列兩種情況:

  • 另一個已傳遞至其他位置之 COM 的 Managed 物件正在實作介面。 封送器可以輕易識別Managed物件所公開的介面,而且能夠比對提供實作的Managed物件。 接著 Managed 物件會傳遞給方法,並且不需要任何包裝函式。

  • 已包裝的物件正在實作介面。 為了判斷這是否為這種情況,封送器會查詢 物件的IUnknown 介面,並將傳回的介面與已包裝的其他物件的介面進行比較。 如果該介面與其他包裝函式的介面相同,則物件會有相同的識別,因此會將現有的包裝函式傳遞給方法。

如果介面不是來自已知的物件,封送器會執行下列動作:

  1. 封送器會查詢 IProvideClassInfo2 介面的物件。 如果提供,封送器會使用從 IProvideClassInfo2.GetGUID 傳回的 CLSID 來識別提供介面的 coclass。 使用 CLSID 時,如果先前已註冊元件,封送器可以從登錄中找到包裝函式。

  2. 封送器會查詢 IProvideClassInfo 介面的介面。 如果提供,封送器會使用IProvideClassInfo.GetClassinfo 傳回的 ITypeInfo 來判斷公開介面之類別的 CLSID。 封送器可以使用 CLSID 來尋找包裝函式的元數據。

  3. 如果封送處理器仍然無法識別類別,它會使用稱為 System.__ComObject 的泛型包裝函式類別包裝介面。

委派的預設封送處理

受控委派會根據呼叫機制,封送處理為 COM 介面或函式指標:

  • 針對平台調用,委派預設會封送為 Unmanaged 函式指標。

  • 針對 COM Interop,委派預設會封送為類型為 _DelegateCOM 介面。 _Delegate 介面是在 Mscorlib.tlb 型別程式庫中定義,並且包含 Delegate.DynamicInvoke 方法,可讓您呼叫委派所參考的方法。

下表顯示Managed委派數據類型的封送處理選項。 MarshalAsAttribute 屬性提供幾種 UnmanagedType 列舉值來封送處理委派。

列舉類型 Unmanaged 格式的描述
UnmanagedType.FunctionPtr Unmanaged 函式指標。
UnmanagedType.Interface _Delegate 類型的介面,如 Mscorlib.tlb 中所定義。

請考慮以下的範例程式碼,其中 DelegateTestInterface 的方法是匯出至 COM 類型程式庫。 請注意,只有標記為 ref (或 ByRef) 關鍵字的委派才會傳遞為 In/Out 參數。

using System;  
using System.Runtime.InteropServices;  
  
public interface DelegateTest {  
void m1(Delegate d);  
void m2([MarshalAs(UnmanagedType.Interface)] Delegate d);
void m3([MarshalAs(UnmanagedType.Interface)] ref Delegate d);
void m4([MarshalAs(UnmanagedType.FunctionPtr)] Delegate d);
void m5([MarshalAs(UnmanagedType.FunctionPtr)] ref Delegate d);
}  

類型程式庫表示

importlib("mscorlib.tlb");  
interface DelegateTest : IDispatch {  
[id(…)] HRESULT m1([in] _Delegate* d);  
[id(…)] HRESULT m2([in] _Delegate* d);  
[id(…)] HRESULT m3([in, out] _Delegate** d);  
[id()] HRESULT m4([in] int d);  
[id()] HRESULT m5([in, out] int *d);  
   };  

函式指標和其他任何 Unmanaged 函式指標一樣,都可以解除參考。

在此範例中,當兩個委派封送處理為 UnmanagedType.FunctionPtr時,結果會是 int 和的 int指標。 由於委派類型正在封送處理中, int 這裡代表 void (void*) 的指標,也就是記憶體中委派的位址。 換句話說,此結果是 32 位元 Windows 系統所特有,因為這裡的 int 代表函式指標的大小。

注意

Unmanaged 程式碼所持有之 Managed 委派的函式指標參考,無法防止 Common Language Runtime 在 Managed 物件上執行記憶體回收。

例如,下列程式碼不正確,因為傳遞給 SetChangeHandler 方法的 cb 物件參考無法使 cb 在過了 Test 方法的存留期後仍保持運作。 一旦 cb 物件的記憶體被回收,傳遞給 SetChangeHandler 的函式指標便不再有效。

public class ExternalAPI {  
   [DllImport("External.dll")]  
   public static extern void SetChangeHandler(  
      [MarshalAs(UnmanagedType.FunctionPtr)]ChangeDelegate d);  
}  
public delegate bool ChangeDelegate([MarshalAs(UnmanagedType.LPWStr) string S);  
public class CallBackClass {  
   public bool OnChange(string S){ return true;}  
}  
internal class DelegateTest {  
   public static void Test() {  
      CallBackClass cb = new CallBackClass();  
      // Caution: The following reference on the cb object does not keep the
      // object from being garbage collected after the Main method
      // executes.  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
   }  
}  

為了彌補未預期的記憶體回收,呼叫端必須確保只要 Unmanaged 函式指標正在使用中,cb 物件就會保持運作。 您可以選擇性地讓 Unmanaged 程式碼通知 Managed 程式碼已不再需要函式指標,如下列範例所示。

internal class DelegateTest {  
   CallBackClass cb;  
   // Called before ever using the callback function.  
   public static void SetChangeHandler() {  
      cb = new CallBackClass();  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));  
   }  
   // Called after using the callback function for the last time.  
   public static void RemoveChangeHandler() {  
      // The cb object can be collected now. The unmanaged code is
      // finished with the callback function.  
      cb = null;  
   }  
}  

實值型別的預設封送處理

大多數實值型別,例如整數和浮點數,都是 blittable 且不需要封送處理。 其他 非 Blittable 類型在 Managed 和 Unmanaged 記憶體中有不同的表示法,而且確實需要封送處理。 但其他類型需要跨互通界限進行明確格式化。

本節提供下列格式化實值型別的相關資訊:

除了描述格式化類型之外,本主題也會 識別具有異常封送處理行為的系統實值型 別。

格式化類型是複雜類型,其中包含在記憶體中明確控制其成員配置的資訊。 這項成員配置資訊會透過 StructLayoutAttribute 屬性來提供。 配置可以是下列其中一個 LayoutKind 列舉值:

  • LayoutKind.Auto

    表示 Common Language Runtime 可以為了更高的效率隨意重新排列類型的成員。 不過,當實值類型傳遞至 Unmanaged 程式碼時,成員的配置是可以預測的。 嘗試封送處理這類結構會自動造成例外狀況。

  • LayoutKind.Sequential

    表示類型的成員可以透過它們出現在 Managed 類型定義中的相同順序來配置於 Unmanaged 記憶體中。

  • LayoutKind.Explicit

    表示根據每個欄位提供的 FieldOffsetAttribute 來配置成員。

在平台叫用中使用的實值類型

在下列範例中,PointRect 類型使用 StructLayoutAttribute 提供成員配置資訊。

Imports System.Runtime.InteropServices  
<StructLayout(LayoutKind.Sequential)> Public Structure Point  
   Public x As Integer  
   Public y As Integer  
End Structure  
<StructLayout(LayoutKind.Explicit)> Public Structure Rect  
   <FieldOffset(0)> Public left As Integer  
   <FieldOffset(4)> Public top As Integer  
   <FieldOffset(8)> Public right As Integer  
   <FieldOffset(12)> Public bottom As Integer  
End Structure  
using System.Runtime.InteropServices;  
[StructLayout(LayoutKind.Sequential)]  
public struct Point {  
   public int x;  
   public int y;  
}
  
[StructLayout(LayoutKind.Explicit)]  
public struct Rect {  
   [FieldOffset(0)] public int left;  
   [FieldOffset(4)] public int top;  
   [FieldOffset(8)] public int right;  
   [FieldOffset(12)] public int bottom;  
}  

當封送處理至 Unmanaged 程式代碼時,這些格式化類型會封送為 C 樣式結構。 如此可讓您輕鬆地呼叫具有結構引數的 Unmanaged 應用程式開發介面。 例如,POINTRECT 結構可以傳遞至 Microsoft Windows API PtInRect 函式,如下所示:

BOOL PtInRect(const RECT *lprc, POINT pt);  

您可以使用以下的平台叫用定義來傳遞結構:

Friend Class NativeMethods
    Friend Declare Auto Function PtInRect Lib "User32.dll" (
        ByRef r As Rect, p As Point) As Boolean
End Class
internal static class NativeMethods
{
   [DllImport("User32.dll")]
   internal static extern bool PtInRect(ref Rect r, Point p);
}

Rect 實值類型必須以傳址方式傳遞,因為 Unmanaged 應用程式開發介面必須將 RECT 的指標傳遞至函式。 Point 實值類型必須以傳值方式傳遞,因為 Unmanaged 應用程式開發介面必須在堆疊上傳遞 POINT。 這種微妙的差異是非常重要的。 參考會當做指標傳遞至 Unmanaged 程式碼, 而值會在堆疊上傳遞至 Unmanaged 程式碼。

注意

當格式化型別封送處理為 結構時,只能存取類型內的欄位。 如果類型具有方法、屬性或事件,您無法從 Unmanaged 程式碼存取這些項目。

類別也可以封送至 Unmanaged 程式代碼做為 C 樣式結構,前提是它們具有固定成員配置。 StructLayoutAttribute 屬性也會提供類別的成員配置資訊。 具有固定版面配置和具有固定版面配置之類別的實值型別主要差異在於它們封送處理至 Unmanaged 程式代碼的方式。 實值類型是以傳值方式 (在堆疊上) 傳遞,因此呼叫端不會看到被呼叫端對類型的成員所做的任何變更。 參考類型是以傳址方式 (在堆疊上傳遞類型的參考) 傳遞,因此呼叫端會看到被呼叫端對類型的 Blittable 類型成員所做的所有變更。

注意

如果參考類型具有非 Blittable 類型的成員,則需要轉換兩次:第一次是在將引數傳遞至 Unmanaged 端時,第二次是從呼叫傳回時。 由於這會增加額外負荷,因此如果呼叫端想要看到被呼叫端所做的變更,就必須將 In/Out 參數明確套用至引數。

在下列範例中,SystemTime 類別具有循序成員配置,可以傳遞至 Windows API GetSystemTime 函式。

<StructLayout(LayoutKind.Sequential)> Public Class SystemTime  
   Public wYear As System.UInt16  
   Public wMonth As System.UInt16  
   Public wDayOfWeek As System.UInt16  
   Public wDay As System.UInt16  
   Public wHour As System.UInt16  
   Public wMinute As System.UInt16  
   Public wSecond As System.UInt16  
   Public wMilliseconds As System.UInt16  
End Class  
[StructLayout(LayoutKind.Sequential)]  
   public class SystemTime {  
   public ushort wYear;
   public ushort wMonth;  
   public ushort wDayOfWeek;
   public ushort wDay;
   public ushort wHour;
   public ushort wMinute;
   public ushort wSecond;
   public ushort wMilliseconds;
}  

GetSystemTime 函式的定義如下:

void GetSystemTime(SYSTEMTIME* SystemTime);  

GetSystemTime 的對等平台叫用定義如下:

Friend Class NativeMethods
    Friend Declare Auto Sub GetSystemTime Lib "Kernel32.dll" (
        ByVal sysTime As SystemTime)
End Class
internal static class NativeMethods
{
   [DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
   internal static extern void GetSystemTime(SystemTime st);
}

請注意,SystemTime 引數未當做參考引數輸入,因為 SystemTime 是類別,不是實值類型。 不同於實值類型,類別永遠會以傳址方式傳遞。

下列程式碼範例顯示具有 SetXY 方法的不同 Point 類別。 因為類型具有循序配置,所以可以傳遞至 Unmanaged 程式代碼,並以 結構的形式封送處理。 不過,即使物件是以傳址方式傳遞,還是無法從 Unmanaged 程式碼中呼叫 SetXY 成員。

<StructLayout(LayoutKind.Sequential)> Public Class Point  
   Private x, y As Integer  
   Public Sub SetXY(x As Integer, y As Integer)  
      Me.x = x  
      Me.y = y  
   End Sub  
End Class  
[StructLayout(LayoutKind.Sequential)]  
public class Point {  
   int x, y;  
   public void SetXY(int x, int y){
      this.x = x;  
      this.y = y;  
   }  
}  

在 COM Interop 中使用的實值類型

格式化類型也可以傳遞至 COM Interop 方法呼叫。 事實上,當匯出至類型程式庫時,實值類型會自動轉換成結構。 如下列範例所示,Point 實值類型會變成名為 Point 的類型定義 (typedef)。 Point typedef 會取代在類型程式庫中的所有 Point 實值類型的參考。

型別程式庫呈現

typedef struct tagPoint {  
   int x;  
   int y;  
} Point;  
interface _Graphics {  
   …  
   HRESULT SetPoint ([in] Point p)  
   HRESULT SetPointRef ([in,out] Point *p)  
   HRESULT GetPoint ([out,retval] Point *p)  
}  

透過 COM 介面封送處理時,會使用用來封送處理值和平台調用呼叫參考的相同規則。 例如,將 Point 實值類型的執行個體從 .NET Framework 傳遞至 COM 時,會以傳值方式傳遞 Point。 如果以傳址方式傳遞 Point 實值類型,則會在堆疊上傳遞 Point 的指標。 Interop 封送器在任一方向都不支援較高層級的間接取值 (Point **)。

注意

由於匯出的型別程式庫無法表示明確的配置,因此不可在 COM Interop 中使用將 LayoutKind 列舉值設定為 [明確] 的結構。

系統實值類型

System 命名空間具有數個實值類型,代表執行階段基本類型的 Boxed 格式。 例如,實值型別 System.Int32 結構代表 ELEMENT_TYPE_I4 的 Boxed 格式。 除了將這些類型封送處理為結構,如同其他格式化類型一樣,您可以以與它們方塊的基本類型相同的方式封送處理它們。 因此,System.Int32 會封送處理為 ELEMENT_TYPE_I4,而不是做為包含long類型之單一成員的結構。 下表包含 System 命名空間中的實值型別清單,這些類型是基本類型的 Boxed 表示。

系統實值類型 項目類型
System.Boolean ELEMENT_TYPE_BOOLEAN
System.SByte ELEMENT_TYPE_I1
System.Byte ELEMENT_TYPE_UI1
System.Char ELEMENT_TYPE_CHAR
System.Int16 ELEMENT_TYPE_I2
System.UInt16 ELEMENT_TYPE_U2
System.Int32 ELEMENT_TYPE_I4
System.UInt32 ELEMENT_TYPE_U4
System.Int64 ELEMENT_TYPE_I8
System.UInt64 ELEMENT_TYPE_U8
System.Single ELEMENT_TYPE_R4
System.Double ELEMENT_TYPE_R8
System.String ELEMENT_TYPE_STRING
System.IntPtr ELEMENT_TYPE_I
System.UIntPtr ELEMENT_TYPE_U

System 命名空間中的其他一些實值型別會以不同方式處理。 因為 Unmanaged 程式代碼已經針對這些類型建立了完善的格式,因此封送處理器有封送處理它們的特殊規則。 下表列出系統命名空間中的特殊實值型別,以及其封送處理到的 Unmanaged 類型。

系統實值類型 IDL 類型
System.DateTime DATE
System.Decimal DECIMAL
System.Guid GUID
System.Drawing.Color OLE_COLOR

下列程式碼示範 Stdole2 型別程式庫中 Unmanaged 類型 DATEGUIDDECIMALOLE_COLOR 的定義。

類型程式庫表示

typedef double DATE;  
typedef DWORD OLE_COLOR;  
  
typedef struct tagDEC {  
    USHORT    wReserved;  
    BYTE      scale;  
    BYTE      sign;  
    ULONG     Hi32;  
    ULONGLONG Lo64;  
} DECIMAL;  
  
typedef struct tagGUID {  
    DWORD Data1;  
    WORD  Data2;  
    WORD  Data3;  
    BYTE  Data4[ 8 ];  
} GUID;  

下列程式碼顯示 Managed IValueTypes 介面中的對應定義。

Public Interface IValueTypes  
   Sub M1(d As System.DateTime)  
   Sub M2(d As System.Guid)  
   Sub M3(d As System.Decimal)  
   Sub M4(d As System.Drawing.Color)  
End Interface  
public interface IValueTypes {  
   void M1(System.DateTime d);  
   void M2(System.Guid d);  
   void M3(System.Decimal d);  
   void M4(System.Drawing.Color d);  
}  

類型程式庫表示

[…]  
interface IValueTypes : IDispatch {  
   HRESULT M1([in] DATE d);  
   HRESULT M2([in] GUID d);  
   HRESULT M3([in] DECIMAL d);  
   HRESULT M4([in] OLE_COLOR d);  
};  

另請參閱