共用方式為


默認封送處理行為

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

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

備註

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

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

互操作封送器總是會嘗試釋放由非受控代碼配置的記憶體。 此行為符合 COM 記憶體管理規則,但與管理原生C++的規則不同。

使用平臺調用時預期原生 C++ 行為(無記憶體釋放)可能會引起混淆,因為平臺調用會自動釋放指標所指的記憶體。 例如,從C++ DLL 呼叫下列 Unmanaged 方法不會自動釋放任何記憶體。

未管理的簽章

BSTR MethodOne (BSTR b) {
     return b;
}

然而,如果你將方法定義為平台呼叫原型,將每個 BSTR 類型替換為一個 String 型別,並呼叫 MethodOne,該通用語言執行時會嘗試釋放 b 兩次。 你可以透過使用 IntPtr 類型而非 String 類型來改變編組行為。

執行時在 Windows 上總是使用 CoTaskMemFree 方法,而在其他平台則使用 free 方法來釋放記憶體。 如果你使用的記憶體在 Windows 上沒有用CoTaskMemAlloc方法或在其他平台上沒有用malloc方法進行分配,則你必須是用IntPtr並用正確的方法手動釋放記憶體。 同樣地,在某些不應釋放記憶體的情況下,例如使用來自 Kernel32.dll 的GetCommandLine函式時,該函式回傳指向核心記憶體的指標,也可以避免自動釋放記憶體。 如需手動釋放記憶體的詳細資訊,請參閱 緩衝區範例

類別的預設封送處理

類別只能由 COM Interop 封送處理,而且一律會封送為介面。 在某些情況下,用來封送類別的介面稱為類別介面。 如需使用您選擇的介面覆蓋類別介面的相關資訊,請參閱 類別介面簡介

將類別傳遞至 COM

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

將類別傳遞至 .NET 程序代碼

Coclass 通常不會當做 COM 中的方法自變數使用。 相反地,預設介面通常會傳遞來取代coclass。

當介面傳遞至受控代碼時,Interop 封送處理器會負責使用適當的包裝來包裝介面,並將其傳遞至受控方法。 判斷要使用的包裝函式可能很困難。 不論物件實作多少介面,COM 物件的每個實例都有單一唯一包裝函式。 例如,實作五個不同介面的單一 COM 物件只有一個包裝函式。 相同的包裝函式會公開所有五個介面。 如果建立 COM 物件的兩個實例,則會建立包裝函式的兩個實例。

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

例如,封送器會判斷應該使用類別包裝器來包裝傳入受控代碼的介面。 當介面第一次通過封送器時,封送器會檢查介面是否來自已知的物件。 此檢查會在兩種情況下發生:

  • 介面正由另一個受管理的物件實作,該物件已經傳遞到其他地方的 COM。 封送器可以輕鬆識別受管理物件所公開的介面,並能夠將其與提供實作的受管理物件進行匹配。 已管理的物件然後會被傳遞至這個方法,而且不需要包裝函式。

  • 已經被包裝的物件正在執行介面。 為了判斷是否如此,Marshaller 會查詢物件的介面, IUnknown 並將回傳的介面與其他已包裝過的物件介面進行比較。 如果介面與另一個包裝函式相同,物件會具有相同的身分識別,而且現有的包裝函式會傳遞至 方法。

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

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

  2. Marshaller 會查詢 IProvideClassInfo 介面。 若提供,marshaller 會使用 ITypeInfoIProvideClassInfo.GetClassinfo 回傳的 CLSID 來判斷該介面所暴露類別的 CLSID。 封送器可以使用 CLSID 來尋找包裝函式的元數據。

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

委派的預設封送處理

根據呼叫機制,管理的委派將被封送處理為 COM 介面或函數指標:

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

  • 對於 COM 互通,預設情況下,代理會被編組為類型為 _Delegate 的 COM 介面。 介面 _Delegate 定義於 Mscorlib.tlb 型式庫中,包含 Delegate.DynamicInvoke 方法,讓你能呼叫代理所參考的方法。

下表顯示受控委派資料類型的封送處理選項。 屬性 MarshalAsAttribute 提供數個 UnmanagedType 列舉值以轉換委派。

列舉型別 Unmanaged 格式的描述
UnmanagedType.FunctionPtr 未管理的函式指標。
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);
   };

函式指標可以被解參考,就像任何其他非受控函式指標可以被解參考一樣。

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

備註

非受控程式碼所持有對受管委派的函式指標的參考,不會防止公用語言執行平台對受管物件執行垃圾收集。

例如,下列程式碼不正確,因為傳遞到cb方法的對象SetChangeHandler參考,不會讓cbTest方法存留期結束後繼續存活。 一旦 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 記憶體中有不同的表示法,而且確實需要封送處理。 其他類型仍然需要跨互作界限的明確格式設定。

本節提供下列格式化實值類型的相關信息:

除了描述格式化類型之外,本主題還會識別出具有特殊封送行為的 System 值型別。

格式化的類型是複雜類型,其中包含明確控制其記憶體中成員配置的資訊。 成員配置資訊是使用 StructLayoutAttribute 屬性來提供。 版面配置可以是以下LayoutKind列舉值之一:

  • LayoutKind.Auto

    表示 Common Language Runtime 可以自由重新排序類型的成員,以提高效率。 不過,當值類型傳遞至非受控程式碼時,成員的配置是可預測的。 嘗試封送處理這類結構時,會自動造成例外狀況。

  • 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;
}

當封送至非受控程式碼時,這些格式化類型會封送為 C 樣式的結構。 這可讓您輕鬆呼叫具有結構自變數的 Unmanaged API。 例如, 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值類型必須以傳址方式傳遞,因為非受控 API 期望傳遞的是指向RECT的指標給函數。 實 Point 值類型會以傳值方式傳遞,因為非受控 API 預期 POINT 會在堆疊上傳遞。 這種微妙的差異非常重要。 參考會以指標的形式傳遞至 Unmanaged 程式代碼。 值會傳遞至堆疊上的非受控程式碼。

備註

當格式化的類型被封送為結構時,只能存取類別中的欄位。 如果類型具有方法、屬性或事件,則無法從 Unmanaged 程式代碼存取它們。

類別也可以封送至非受控程式碼,作為 C 風格結構,只要它們具有固定的成員配置。 類別的成員配置資訊是由 StructLayoutAttribute 屬性提供的。 具有固定版面配置的實值型別與類別之間的主要差異在於它們封送至非受控程式碼的方式。 實值型別會以值傳遞(在堆疊上),因此由呼叫端看不到被呼叫者對型別成員所做的任何變更。 參考型別會以參考方式傳遞(型別的參考是在堆棧上傳遞);因此,呼叫者可以看到被叫者對可直接複製型別成員所做的所有變更。

備註

如果引用類型具有非平移型別的成員,則需要進行兩次轉換:第一次是當參數傳遞到非受控端時,第二次是在從呼叫返回時進行傳回。 由於這個額外負荷,如果呼叫者想要看到被呼叫者所做的變更,必須將 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 是類別,而不是實值類型。 不同於實值型別,類別一律會以傳址方式傳遞。

下列程式代碼範例顯示具有稱為 Point之方法的不同SetXY類別。 因為類型具有循序配置,所以可以傳遞至非受控程式碼,並以結構的形式封送處理。 不過,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 互通方法的呼叫中。 事實上,導出至類型庫時,實值型別會自動轉換成結構。 如下列範例所示,Point 實值型別會成為名稱為 Point 的類型定義(typedef)。 在型別庫中,其他地方對Point實值型別的所有參考,都替換為Point型別定義(typedef)。

類型庫表示法

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 **)。

備註

LayoutKind 枚舉值設為 的 Explicit 結構體無法用於 COM 互操作,因為匯出的型別庫無法表達明確的佈局。

系統實值類型

命名空間 System 有數個實值型別,代表運行時間基本類型之 Boxed 格式。 例如,值類型System.Int32結構代表ELEMENT_TYPE_I4的 boxed 格式。 相較於將這些類型像其他格式化類型那樣封送處理成結構,您應該以其對應的基本類型的方式進行封送處理。 System.Int32 因此會被封送為ELEMENT_TYPE_I4,而不是作為包含單個long類型成員的結構。 下表列出命名空間中 System 原始型態的框框表示值型別。

系統實值類型 項目類型
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 其他一些值類型則有不同的處理方式。 因為非受控代碼已為這些類型建立完善格式,因此封送處理器針對它們有特殊規則。 下表列出命名空間中的 System 特殊值類型,以及它們被編組到的非管理型別。

系統實值類型 IDL 類型
System.DateTime 日期
System.Decimal 十進制
System.Guid GUID (英文)
System.Drawing.Color OLE_COLOR

以下程式碼展示了 Stdole2 型態函式庫中未受管理型別 DATE、GUIDDECIALs 及 DECIMAOLE_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);
};

另請參閱