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


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

Как классы System.String , так и System.Text.StringBuilder имеют аналогичное поведение маршаллинга.

Строки маршалируются как тип COM-стиля BSTR или как строка, завершающаяся значением NULL (массив символов, заканчивающийся нулевым символом). Символы внутри строки могут быть маршалированы как Юникод (по умолчанию для систем Windows) или ANSI.

Строки, используемые в интерфейсах

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

Тип перечисления Описание неуправляемого формата
UnmanagedType.BStr (по умолчанию) COM-стиль BSTR с префиксной длиной и символами Юникод.
UnmanagedType.LPStr Указатель на массив символов ANSI, завершающийся значением NULL.
UnmanagedType.LPWStr Указатель на массив символов Юникода, завершающийся значением NULL.

Эта таблица относится к String. Для StringBuilder разрешены только варианты UnmanagedType.LPStr и UnmanagedType.LPWStr.

В следующем примере показаны строки, объявленные в интерфейсе IStringWorker.

public interface IStringWorker
{
    void PassString1(string s);
    void PassString2([MarshalAs(UnmanagedType.BStr)] string s);
    void PassString3([MarshalAs(UnmanagedType.LPStr)] string s);
    void PassString4([MarshalAs(UnmanagedType.LPWStr)] string s);
    void PassStringRef1(ref string s);
    void PassStringRef2([MarshalAs(UnmanagedType.BStr)] ref string s);
    void PassStringRef3([MarshalAs(UnmanagedType.LPStr)] ref string s);
    void PassStringRef4([MarshalAs(UnmanagedType.LPWStr)] ref string s);
}
Public Interface IStringWorker
    Sub PassString1(s As String)
    Sub PassString2(<MarshalAs(UnmanagedType.BStr)> s As String)
    Sub PassString3(<MarshalAs(UnmanagedType.LPStr)> s As String)
    Sub PassString4(<MarshalAs(UnmanagedType.LPWStr)> s As String)
    Sub PassStringRef1(ByRef s As String)
    Sub PassStringRef2(<MarshalAs(UnmanagedType.BStr)> ByRef s As String)
    Sub PassStringRef3(<MarshalAs(UnmanagedType.LPStr)> ByRef s As String)
    Sub PassStringRef4(<MarshalAs(UnmanagedType.LPWStr)> ByRef s As String)
End Interface

В следующем примере показан соответствующий интерфейс, описанный в библиотеке типов.

interface IStringWorker : IDispatch
{
    HRESULT PassString1([in] BSTR s);
    HRESULT PassString2([in] BSTR s);
    HRESULT PassString3([in] LPStr s);
    HRESULT PassString4([in] LPWStr s);
    HRESULT PassStringRef1([in, out] BSTR *s);
    HRESULT PassStringRef2([in, out] BSTR *s);
    HRESULT PassStringRef3([in, out] LPStr *s);
    HRESULT PassStringRef4([in, out] LPWStr *s);
};

Строки, используемые в Platform Invoke (вызове платформы)

Если CharSet имеет значение Unicode или строковый аргумент явно помечен как [MarshalAs(UnmanagedType.LPWSTR)] и строка передается по значению (не как ref или out), строка закрепляется и используется непосредственно в нативном коде. В противном случае вызов платформы копирует строковые аргументы, преобразуя формат .NET Framework (Юникод) в неуправляемый формат платформы. Строки неизменяемы и не копируются из неуправляемой памяти в управляемую память при возврате вызова.

Нативный код отвечает только за освобождение памяти, если строка передается по ссылке и ей назначается новое значение. В противном случае среда выполнения .NET владеет памятью и отпустит ее после вызова.

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

Тип перечисления Описание неуправляемого формата
UnmanagedType.AnsiBStr Стиль COM BSTR с префиксной длиной и символами ANSI.
UnmanagedType.BStr COM-стиль BSTR с префиксной длиной и символами Юникод.
UnmanagedType.LPStr (по умолчанию) Указатель на массив символов ANSI, завершающийся значением NULL.
UnmanagedType.LPTStr Указатель на массив символов, зависимых от платформы, завершаемых значением NULL.
UnmanagedType.LPUTF8Str Указатель на массив символов, завершаемых значением NULL, в кодировке UTF-8.
UnmanagedType.LPWStr Указатель на массив символов Юникода, завершающийся значением NULL.
UnmanagedType.TBStr Стиль COM BSTR с префиксной длиной и зависимыми от платформы символами.
VBByRefStr Значение, которое позволяет Visual Basic изменять строку в неуправляемом коде и отражать результаты в управляемом коде. Это значение поддерживается только для вызова платформы. Это значение по умолчанию в Visual Basic для строк ByVal.

Эта таблица относится к String. Для StringBuilder, допустимы только варианты LPStr, LPTStrи LPWStr.

В следующем определении типа показано правильное использование MarshalAsAttribute для вызовов платформы platform invoke.

class StringLibAPI
{
    [DllImport("StringLib.dll")]
    public static extern void PassLPStr([MarshalAs(UnmanagedType.LPStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPWStr([MarshalAs(UnmanagedType.LPWStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPTStr([MarshalAs(UnmanagedType.LPTStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassLPUTF8Str([MarshalAs(UnmanagedType.LPUTF8Str)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassBStr([MarshalAs(UnmanagedType.BStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassAnsiBStr([MarshalAs(UnmanagedType.AnsiBStr)] string s);
    [DllImport("StringLib.dll")]
    public static extern void PassTBStr([MarshalAs(UnmanagedType.TBStr)] string s);
}
Class StringLibAPI
    Public Declare Auto Sub PassLPStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPStr)> s As String)
    Public Declare Auto Sub PassLPWStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPWStr)> s As String)
    Public Declare Auto Sub PassLPTStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPTStr)> s As String)
    Public Declare Auto Sub PassLPUTF8Str Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.LPUTF8Str)> s As String)
    Public Declare Auto Sub PassBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.BStr)> s As String)
    Public Declare Auto Sub PassAnsiBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.AnsiBStr)> s As String)
    Public Declare Auto Sub PassTBStr Lib "StringLib.dll" (
        <MarshalAs(UnmanagedType.TBStr)> s As String)
End Class

Строки, используемые в структурах

Строки являются допустимыми элементами структур; StringBuilder однако буферы недопустимы в структурах. В следующей таблице показаны варианты передачи для типа данных String, когда тип передаётся как поле. Атрибут MarshalAsAttribute предоставляет несколько значений перечисления UnmanagedType для маршалирования строк в поле.

Тип перечисления Описание неуправляемого формата
UnmanagedType.BStr COM-стиль BSTR с префиксной длиной и символами Юникод.
UnmanagedType.LPStr (по умолчанию) Указатель на массив символов ANSI, завершающийся значением NULL.
UnmanagedType.LPTStr Указатель на массив символов, зависимых от платформы, завершаемых значением NULL.
UnmanagedType.LPUTF8Str Указатель на массив символов, завершаемых значением NULL, в кодировке UTF-8.
UnmanagedType.LPWStr Указатель на массив символов Юникода, завершающийся значением NULL.
UnmanagedType.ByValTStr Массив символов фиксированной длины; Тип массива определяется набором символов содержащей структуры.

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

Аргумент CharSet , применяемый StructLayoutAttribute к содержащей структуре, определяет формат символов строк в структурах. В следующих примерах структур содержатся строковые ссылки и встроенные строки, а также ANSI, Юникод и зависимые от платформы символы. Представление этих структур в библиотеке типов отображается в следующем коде C++:

struct StringInfoA
{
    char *  f1;
    char    f2[256];
};

struct StringInfoW
{
    WCHAR * f1;
    WCHAR   f2[256];
    BSTR    f3;
};

struct StringInfoT
{
    TCHAR * f1;
    TCHAR   f2[256];
};

В следующем примере показано, как использовать MarshalAsAttribute для определения одной и той же структуры в разных форматах.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct StringInfoA
{
    [MarshalAs(UnmanagedType.LPStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct StringInfoW
{
    [MarshalAs(UnmanagedType.LPWStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
    [MarshalAs(UnmanagedType.BStr)] public string f3;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct StringInfoT
{
    [MarshalAs(UnmanagedType.LPTStr)] public string f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string f2;
}
<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Ansi)> _
Structure StringInfoA
    <MarshalAs(UnmanagedType.LPStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
End Structure

<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Unicode)> _
Structure StringInfoW
    <MarshalAs(UnmanagedType.LPWStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
<MarshalAs(UnmanagedType.BStr)> Public f3 As String
End Structure

<StructLayout(LayoutKind.Sequential, CharSet := CharSet.Auto)> _
Structure StringInfoT
    <MarshalAs(UnmanagedType.LPTStr)> Public f1 As String
    <MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> _
    Public f2 As String
End Structure

Буферы строк фиксированной длины

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

Решение заключается в передаче byte[] или char[](в зависимости от ожидаемой кодировки) в качестве аргумента вместо аргумента String. Массив, помеченный [Out], может быть разыменован и изменен вызывающей стороной, если он не превышает объём выделенного массива.

Например, функция API Windows GetWindowText (определенная в winuser.h) требует, чтобы вызывающий объект передал буфер символов фиксированной длины, в который функция записывает текст окна. Аргумент lpString указывает на выделенный вызывающим буфер размером nMaxCount. Ожидается, что вызывающая сторона выделяет буфер и указывает аргумент nMaxCount, равный размеру выделенного буфера. В следующем примере показано GetWindowText объявление функции, определённое в winuser.h.

int GetWindowText(
    HWND hWnd,        // Handle to window or control.
    LPTStr lpString,  // Text buffer.
    int nMaxCount     // Maximum number of characters to copy.
);

char[] может быть разыменован и изменён вызываемой функцией. Рекомендуемый подход — использовать ArrayPool<T> для аренды char[], что позволяет избежать повторяющихся выделений кучи. В следующем примере кода показан этот шаблон.

using System;
using System.Buffers;
using System.Runtime.InteropServices;

internal static class NativeMethods
{
    [DllImport("User32.dll", CharSet = CharSet.Unicode)]
    public static extern int GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount);
}

public class Window
{
    internal IntPtr h;        // Internal handle to Window.
    public string GetText()
    {
        char[] buffer = ArrayPool<char>.Shared.Rent(256 + 1);
        try
        {
            int length = NativeMethods.GetWindowText(h, buffer, buffer.Length);
            return new string(buffer, 0, length);
        }
        finally
        {
            ArrayPool<char>.Shared.Return(buffer);
        }
    }
}
Imports System
Imports System.Buffers
Imports System.Runtime.InteropServices

Friend Class NativeMethods
    Public Declare Auto Function GetWindowText Lib "User32.dll" _
        (hWnd As IntPtr, <Out> lpString() As Char, nMaxCount As Integer) As Integer
End Class

Public Class Window
    Friend h As IntPtr ' Friend handle to Window.
    Public Function GetText() As String
        Dim buffer() As Char = ArrayPool(Of Char).Shared.Rent(256 + 1)
        Try
            Dim length As Integer = NativeMethods.GetWindowText(h, buffer, buffer.Length)
            Return New String(buffer, 0, length)
        Finally
            ArrayPool(Of Char).Shared.Return(buffer)
        End Try
    End Function
End Class

Вы также можете рассмотреть возможность передачи StringBuilder вместо String. Буфер, созданный при маршаллинге StringBuilder, может быть разыменован и изменен вызываемой стороной, если только он не превышает объем StringBuilder. Его также можно инициализировать до фиксированной длины. Например, если инициализировать буфер StringBuilder вместимостью N, маршаллизатор предоставляет буфер размером (N+1) символов. +1 учитывает тот факт, что неуправляемая строка имеет нулевой терминатор, а в StringBuilder он отсутствует.

Предостережение

Избегайте StringBuilder параметров при решении проблем с производительностью. Маршаллирование StringBuilderвсегда создает копию нативного буфера. Типичный вызов для извлечения строки из нативного кода может привести к четырём выделениям:

  1. Управляемый StringBuilder буфер.
  2. Собственный буфер, выделенный во время маршаллинга.
  3. Если [Out], содержимое собственного буфера копируется в недавно выделенный управляемый массив.
  4. Выделено stringToString()по .

Повторное использование одного и того же StringBuilder для вызовов сохраняет только одно выделение памяти. Использование буфера символов, арендованного из ArrayPool<char>, гораздо более эффективно — это уменьшает последующие вызовы только до выделения для ToString().

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

Дополнительные сведения см. в разделе "Строковые параметры " и CA1838: избегайте StringBuilder параметров для P/Invokes.

См. также