Поделиться через


Поведение маршаллинга по умолчанию

Маршалирование взаимодействия работает с правилами, которые определяют поведение данных, связанных с параметрами метода, по мере того как они передаются между управляемой и неуправляемой памятью. Эти встроенные правила управляют такими действиями маршаллинга, как преобразования типа данных, независимо от того, может ли вызывающий объект изменять данные и возвращать эти изменения вызывающему объекту, и при каких обстоятельствах маршаллизатор обеспечивает оптимизацию производительности.

В этом разделе определяются характеристики поведения по умолчанию службы маршалинга взаимодействия. В нём представлены подробные сведения о массивах, маршалировании, типах Boolean, символьных типах, делегатах, классах, объектах, строках и структурах.

Замечание

Маршаллирование универсальных типов не поддерживается. Дополнительные сведения см. в разделе "Взаимодействие с использованием универсальных типов".

Управление памятью с маршализатором взаимодействия

Маршализатор взаимодействия всегда пытается освободить память, выделенную неуправляемым кодом. Это поведение соответствует правилам управления памятью COM, но отличается от правил, регулирующих собственный C++.

Путаница может возникнуть, если вы ожидаете типичное поведение C++ (без освобождения памяти), при использовании механизма Platform Invoke, который автоматически освобождает память для указателей. Например, вызов следующего неуправляемого метода из библиотеки DLL C++ не освобождает память автоматически.

Неуправляемая сигнатура

BSTR MethodOne (BSTR b) {  
     return b;  
}  

Однако если вы определите метод как прототип платформенного вызова, замените каждый тип BSTR на тип String и вызовите MethodOne, то общая языкосвязанная среда выполнения (CLR) попытается освободить b дважды. Поведение маршаллинга можно изменить, используя IntPtr типы вместо типов String.

Среда выполнения всегда использует метод CoTaskMemFree в Windows и бесплатный метод на других платформах для освобождения памяти. Если память, которую вы используете, не была выделена с использованием метода CoTaskMemAlloc в Windows или метода malloc на других платформах, необходимо использовать IntPtr и освободить ее вручную с помощью соответствующего метода. Аналогичным образом можно избежать автоматического освобождения памяти в ситуациях, когда память никогда не должна быть освобождена, например при использовании функции GetCommandLine из Kernel32.dll, которая возвращает указатель на память ядра. Дополнительные сведения об освобождении памяти вручную см. в примере буферов.

Маршалирование по умолчанию для классов

Классы можно передавать только через COM-взаимодействие и всегда передаются как интерфейсы. В некоторых случаях интерфейс, который используется для преобразования класса, называется интерфейсом класса. Сведения о переопределении интерфейса класса с помощью выбранного интерфейса см. в разделе "Введение в интерфейс класса".

Передача классов в COM

Когда управляемый класс передается в COM, маршализатор взаимодействия автоматически упаковывает класс с помощью COM-прокси и передает интерфейс класса, созданный прокси-сервером, вызову метода COM. Затем прокси-сервер делегирует все вызовы интерфейса класса обратно в управляемый объект. Прокси-сервер также предоставляет другие интерфейсы, которые не реализуются явным образом классом. Прокси-сервер автоматически реализует интерфейсы, такие как IUnknown и IDispatch от имени класса.

Передача классов в код .NET

Coclasses обычно не используются в качестве аргументов метода в COM. Вместо этого интерфейс по умолчанию обычно передается вместо coclass.

Когда интерфейс передается в управляемый код, маршаллизатор отвечает за обертывание интерфейса соответствующей оболочкой и передачу этой оболочки в управляемый метод. Выбор оболочки может быть сложной задачей. Каждый экземпляр COM-объекта имеет одну уникальную оболочку, вне зависимости от того, сколько интерфейсов реализует объект. Например, один COM-объект, реализующий пять отдельных интерфейсов, имеет только одну оболочку. Одна и та же оболочка обеспечивает доступ ко всем пяти интерфейсам. Если создаются два экземпляра COM-объекта, создаются два экземпляра оболочки.

Чтобы оболочка поддерживала тот же тип в течение всего времени существования, маршализатор interop должен определить правильную оболочку при первом прохождении интерфейса, предоставляемого объектом, через маршализатор. Маршаллизатор определяет объект, просматривая один из интерфейсов, который реализует объект.

Например, маршаллизатор определяет, что оболочка класса должна использоваться для упаковки интерфейса, переданного в управляемый код. Когда интерфейс впервые передается через маршрутизатор, он проверяет, поступает ли интерфейс от известного объекта. Эта проверка выполняется в двух ситуациях:

  • Интерфейс реализуется другим управляемым объектом, переданным в COM в другом месте. Маршализатор может легко идентифицировать интерфейсы, предоставляемые управляемыми объектами, и сопоставлять интерфейс с управляемым объектом, который обеспечивает её реализацию. Затем управляемый объект передается методу и не требуется оболочка.

  • Объект, который уже был упакован, реализует интерфейс. Чтобы определить, является ли это так, маршаллировщик запрашивает у объекта его интерфейс IUnknown и сравнивает возвращенный интерфейс с интерфейсами других объектов, которые уже обернуты. Если интерфейс совпадает с другим интерфейсом, объекты имеют ту же идентичность, а существующая оболочка передаётся в метод.

Если интерфейс не является из известного источника, маршаллер выполняет следующее:

  1. Маршаллизатор запрашивает объект для интерфейса IProvideClassInfo2 . Если предоставлено, маршализатор использует CLSID, возвращенный из IProvideClassInfo2.GetGUID для идентификации кокласса, предоставляющего интерфейс. С помощью CLSID маршализатор может найти обертку в реестре, если сборка была зарегистрирована ранее.

  2. Маршаллизатор запрашивает интерфейс для интерфейса IProvideClassInfo . Если предоставлено, маршаллизатор использует ITypeInfo, возвращенный из IProvideClassInfo.GetClassInfo, для определения CLSID класса, предоставляющего интерфейс. Маршаллер может использовать CLSID для поиска метаданных для оболочки.

  3. Если маршаллер по-прежнему не может идентифицировать класс, он оборачивает интерфейс универсальным классом оболочки с именем System.__ComObject.

Маршалирование по умолчанию для делегатов

Управляемый делегат управляется как COM-интерфейс или в виде указателя на функцию в зависимости от механизма вызова.

  • Для вызова платформы делегат преобразуется в неуправляемый указатель на функцию по умолчанию.

  • Для взаимодействия COM делегат маршалируется как COM-интерфейс типа _Delegate по умолчанию. Интерфейс _Delegate определен в библиотеке типов Mscorlib.tlb и содержит Delegate.DynamicInvoke метод, который позволяет вызывать метод, на который ссылается делегат.

В следующей таблице показаны параметры маршалинга для типа данных управляемого делегата. Атрибут MarshalAsAttribute предоставляет различные значения перечисления UnmanagedType для маршалирования делегатов.

Тип перечисления Описание неуправляемого формата
UnmanagedType.FunctionPtr Указатель неуправляемой функции.
UnmanagedType.Interface (неуправляемый тип: Интерфейс) Интерфейс типа _Delegate, как определено в mscorlib.tlb.

Рассмотрим следующий пример кода, в котором методы DelegateTestInterface экспортируются в библиотеку типов COM. Обратите внимание, что только делегаты, помеченные ключевым словом ref (или ByRef), передаются как входные/выходные параметры.

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, результатом являются int и указатель на int. Поскольку типы делегатов маршалируются, int здесь представляет указатель на пустоту (void*), которая является адресом делегата в памяти. Другими словами, этот результат относится к 32-разрядным системам Windows, так как int здесь представляет размер указателя функции.

Замечание

Ссылка на указатель функции на управляемый делегат, который удерживается неуправляемым кодом, не мешает среде выполнения общего языка (CLR) выполнять сборку мусора для управляемого объекта.

Например, следующий код является неверным, так как ссылка на cb объект, переданная SetChangeHandler методу, не поддерживает 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));
   }  
}  

Чтобы компенсировать непредвиденную сборку мусора, вызывающий код должен убедиться, что cb объект остается доступным до тех пор, пока используется неуправляемый указатель функции. При необходимости, можно настроить неуправляемый код так, чтобы уведомлять управляемый код о том, когда указатель функции больше не требуется, как показано в следующем примере.

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

Маршаллинг по умолчанию для типов значений

Большинство типов значений, таких как целые числа и числа с плавающей запятой, являются блитабельными и не требуют маршаллинга. Другие неблиттабельные типы имеют разнородные представления в управляемой и неуправляемой памяти и требуют маршаллинга. Тем не менее другие типы требуют явного форматирования по границе взаимодействия.

В этом разделе содержатся сведения о следующих типах форматированных значений:

Помимо описания форматированных типов, в этом разделе определяются системные типы значений, обладающие необычным поведением маршалинга.

Форматированный тип — это сложный тип, содержащий информацию, которая явно контролирует расположение его членов в памяти. Сведения о макете элемента предоставляются с помощью атрибута StructLayoutAttribute . Макет может быть одним из следующих LayoutKind значений перечисления:

  • LayoutKind.Auto

    Указывает, что среда CLR свободна для переупорядочения элементов типа для повышения эффективности. Однако, когда значимый тип передается в неуправляемый код, макет его элементов предсказуем. Попытка маршалировать такую структуру автоматически вызывает исключение.

  • LayoutKind.Sequential

    Указывает, что элементы типа должны быть размещены в неуправляемой памяти в том же порядке, в котором они отображаются в определении управляемого типа.

  • LayoutKind.Explicit

    Указывает, что элементы выкладываются в соответствии с предоставленным полем FieldOffsetAttribute .

Типы значений, используемые в вызове платформы

В следующем примере типы Point и Rect предоставляют сведения о макете элементов с помощью 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. Это обеспечивает простой способ вызова неуправляемого API с аргументами структуры. Например, структуры POINT и RECT можно передать в функцию 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 будет передан в стеке. Это тонкое различие очень важно. Ссылки передаются в неуправляемый код в виде указателей. Значения передаются в неуправляемый код в стеке.

Замечание

Если форматированный тип маршалируется как структура, доступны только поля внутри типа. Если тип содержит методы, свойства или события, они недоступны из неуправляемого кода.

Классы также можно маршалировать в неуправляемый код в виде структур стиля C, если они имеют фиксированное расположение членов. Сведения о макете элемента для класса также предоставляются атрибутом StructLayoutAttribute . Основное различие между типами значений с фиксированным макетом и классами с фиксированным макетом заключается в том, как они маршалируются в неуправляемый код. Типы значений передаются по значению (в стеке) и, следовательно, любые изменения, внесенные в члены типа вызываемого объекта, не отображаются вызывающим элементом. Ссылочные типы передаются по ссылке (ссылка на тип передается в стеке); следовательно, все изменения, внесенные в элементы типа blittable типа вызываемого объекта, отображаются вызывающим элементом.

Замечание

Если у ссылочного типа есть члены неуправляемых типов, преобразование требуется дважды: в первый раз при передаче аргумента на неуправляемую сторону и второй раз при возврате из вызова. Из-за этой добавленной нагрузки параметры 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-взаимодействии

Форматированные типы также можно передавать в вызовы метода взаимодействия COM. Фактически при экспорте в библиотеку типов типы значений автоматически преобразуются в структуры. Как показано в следующем примере, Point тип значения становится определением типа (typedef) с именем Point. Все ссылки на тип значения Point в других частях библиотеки типов заменяются на 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 передается через стек. Маршализатор взаимодействия не поддерживает более высокие уровни индирекции (Точка **) в обоих направлениях.

Замечание

Структуры, в которых LayoutKind значение перечисления установлено на Explicit, не могут использоваться в COM-взаимодействии, так как экспортируемая библиотека типов не может выразить явный макет.

Системные типы значений

Пространство System имен имеет несколько типов значений, представляющих прямоугольную форму примитивных типов среды выполнения. Например, структура типа System.Int32 значения представляет прямоугольную форму ELEMENT_TYPE_I4. Вместо того чтобы маршалировать эти типы как структуры, как это делается с другими форматированными типами, вы должны маршалировать их так же, как и примитивные типы, которые они обрамляют. Поэтому 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

Некоторые другие типы значений в пространстве имен системы обрабатываются иначе. Поскольку в неуправляемом коде уже имеются прочно установленные форматы для этих типов, механизм маршаллинга имеет специальные правила для их обработки. В следующей таблице перечислены специальные типы значений в пространстве имен системы , а также неуправляемый тип, в который они маршаллируются.

Тип системного значения Тип IDL
System.DateTime ДАТА
System.Decimal ДЕСЯТИЧНАЯ СИСТЕМА
System.Guid глобальный уникальный идентификатор
System.Drawing.Color OLE_COLOR

В следующем коде показано определение неуправляемых типов DATE, GUID, DECIMAL и OLE_COLOR в библиотеке типов Stdole2.

Представление библиотеки типов

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;  

В следующем коде показаны соответствующие определения в управляемом 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);  
};  

См. также