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

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

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

Примечание.

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

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

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

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

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

BSTR MethodOne (BSTR b) {  
     return b;  
}  

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

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

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

Классы можно маршалировать только com-взаимодействием и всегда маршалировать как интерфейсы. Иногда интерфейс, используемый для маршалинга класса, называют интерфейсом класса. Подробнее о переопределении интерфейса класса другим выбранным интерфейсом см. в статье Introducing the class interface (Введение в интерфейс класса).

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

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

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

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

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

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

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

  • Интерфейс реализуется другим управляемым объектом, переданным в 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), передаются как параметры 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, результатом является 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 могут передаваться в функцию PtInRect Microsoft Windows API следующим образом:

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. Основное различие между типами значений с фиксированным макетом и классами с фиксированным макетом заключается в том, как они маршалируются в неуправляемый код. Типы значений передаются по значению (в стеке), поэтому любые изменения, внесенные в члены этого типа вызываемым объектом, не видны вызывающему объекту. Ссылочные типы передаются по ссылке (ссылка на тип передается в стеке). Поэтому все изменения, внесенные в члены непреобразуемого типа вызываемым объектом, видны вызывающему объекту.

Примечание.

Если ссылочный тип содержит члены преобразуемого типа, преобразование должно выполняться дважды: первый раз — при передаче аргумента неуправляемой стороне, а второй раз — при возврате из вызова. В связи с появлением дополнительных издержек параметры In/Out необходимо применять к аргументу в явном виде, если вызывающий объект должен учитывать изменения, внесенные вызываемым объектом.

В примере ниже класс SystemTime имеет последовательное размещение членов и может быть передан в функцию GetSystemTime API Windows.

<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 в любом другом месте библиотеки типов заменяются на определение типа 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 передается в стеке. Маршализатор взаимодействия не поддерживает более высокие уровни косвенного обращения (Точка **) в любом направлении.

Примечание.

Структуры, для которых значение перечисления 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

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

Системный тип значения Тип IDL
System.DateTime DATE
System.Decimal DECIMAL
System.Guid 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);  
};  

См. также