.NET Interop IntPtr против реф - PullRequest
       23

.NET Interop IntPtr против реф

9 голосов
/ 16 декабря 2009

Вероятно, вопрос нуб, но взаимодействие пока не является моей сильной стороной.

Помимо ограничения количества перегрузок, есть ли причина, по которой я должен объявить свои DllImports как:

[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);

И используйте их так:

IntPtr lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(formatrange));
Marshal.StructureToPtr(formatrange, lParam, false);

int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, lParam);

Marshal.FreeCoTaskMem(lParam);

Вместо создания целевой перегрузки:

[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref FORMATRANGE lParam);

И используя это как:

FORMATRANGE lParam = new FORMATRANGE();
int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, ref lParam);

Перегрузка по ref в итоге становится проще в использовании, но мне интересно, есть ли недостаток, о котором я не знаю.

Edit:

Множество отличной информации, ребята.

@ P Папа: У вас есть пример создания класса struct из абстрактного (или любого) класса? Я изменил свою подпись на:

[DllImport("user32.dll", SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] CHARFORMAT2 lParam);

Без In, Out и MarshalAs SendMessage (EM_GETCHARFORMAT в моем тесте) завершится ошибкой. Приведенный выше пример хорошо работает, но если я изменю его на:

[DllImport("user32.dll", SetLastError = true)]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] NativeStruct lParam);

Я получаю исключение System.TypeLoadException, в котором говорится, что формат CHARFORMAT2 недопустим (я постараюсь записать его здесь).

Исключение:

Не удалось загрузить тип 'CC.Utilities.WindowsApi.CHARFORMAT2' из сборки 'CC.Utilities, версия = 1.0.9.1212, Culture = нейтральный, PublicKeyToken = 111aac7a42f7965e', так как формат недопустим.

Класс NativeStruct:

public class NativeStruct
{
}

Я попробовал abstract, добавив атрибут StructLayout и т. Д., И я получил то же исключение.

[StructLayout(LayoutKind.Sequential)]
public class CHARFORMAT2: NativeStruct
{
    ...
}

Edit:

Я не следил за часто задаваемыми вопросами и задал вопрос, который можно обсудить, но на который нет положительного ответа. Кроме того, в этой теме было много полезной информации. Поэтому я оставлю это на усмотрение читателей, чтобы проголосовать за ответ. Первым от 10 до 10 голосов будет ответ. Если в течение двух дней ни один ответ не встретится с этим (12/17 по тихоокеанскому времени), я добавлю свой собственный ответ, который суммирует все вкусные знания в теме: -)

Редактировать еще раз:

Я солгал, принимая ответ P Daddy, потому что он человек и ему очень помогли (у него тоже есть симпатичная маленькая обезьянка :-P)

Ответы [ 5 ]

15 голосов
/ 16 декабря 2009

Если структура маршализуема без пользовательской обработки, я очень предпочитаю последний подход, где вы объявляете функцию p / invoke как принимающую ref (указатель на) ваш тип. Кроме того, вы можете объявить ваши типы как классы вместо структур, а затем вы также можете передать null.

[StructLayout(LayoutKind.Sequential)]
struct NativeType{
    ...
}

[DllImport("...")]
static extern bool NativeFunction(ref NativeType foo);

// can't pass null to NativeFunction
// unless you also include an overload that takes IntPtr

[DllImport("...")]
static extern bool NativeFunction(IntPtr foo);

// but declaring NativeType as a class works, too

[StructLayout(LayoutKind.Sequential)]
class NativeType2{
    ...
}

[DllImport("...")]
static extern bool NativeFunction(NativeType2 foo);

// and now you can pass null

<pedantry>

Кстати, в вашем примере, передавая указатель как IntPtr, вы использовали неправильный Alloc. SendMessage не является функцией COM, поэтому не следует использовать распределитель COM. Используйте Marshal.AllocHGlobal и Marshal.FreeHGlobal. Они плохо названы; имена имеют смысл, только если вы занимались программированием Windows API, а может быть, даже тогда. AllocHGlobal вызывает GlobalAlloc в kernel32.dll, что возвращает HGLOBAL. Этот использовал , чтобы отличаться от HLOCAL, возвращаемого LocalAlloc в 16-битные дни, но в 32-битных Windows они одинаковы.

Использование термина HGLOBAL для обозначения блока (нативной) пользовательской памяти просто застряло, я полагаю, и люди, разрабатывающие класс Marshal, не должны были подумать о как это не интуитивно понятно для большинства разработчиков .NET. С другой стороны, большинству разработчиков .NET не нужно выделять неуправляемую память, поэтому ....

</pedantry>


Редактировать

Вы упоминаете, что получаете исключение TypeLoadException при использовании класса вместо структуры, и запрашиваете образец. Я сделал быстрый тест, используя CHARFORMAT2, так как похоже, что вы пытаетесь его использовать.

Первая азбука 1 :

[StructLayout(LayoutKind.Sequential)]
abstract class NativeStruct{} // simple enough

Требуется атрибут StructLayout, или вы получите исключение TypeLoadException.

Теперь CHARFORMAT2 класс:

[StructLayout(LayoutKind.Sequential, Pack=4, CharSet=CharSet.Auto)]
class CHARFORMAT2 : NativeStruct{
    public DWORD    cbSize = (DWORD)Marshal.SizeOf(typeof(CHARFORMAT2));
    public CFM      dwMask;
    public CFE      dwEffects;
    public int      yHeight;
    public int      yOffset;
    public COLORREF crTextColor;
    public byte     bCharSet;
    public byte     bPitchAndFamily;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
    public string   szFaceName;
    public WORD     wWeight;
    public short    sSpacing;
    public COLORREF crBackColor;
    public LCID     lcid;
    public DWORD    dwReserved;
    public short    sStyle;
    public WORD     wKerning;
    public byte     bUnderlineType;
    public byte     bAnimation;
    public byte     bRevAuthor;
    public byte     bReserved1;
}

Я использовал using операторы для псевдонима System.UInt32 как DWORD, LCID и COLORREF, а псевдоним System.UInt16 как WORD. Я стараюсь, чтобы мои определения P / Invoke соответствовали спецификации SDK, насколько это возможно. CFM и CFE - это enums, которые содержат значения флагов для этих полей. Я оставил их определения для краткости, но могу добавить их при необходимости.

Я объявил SendMessage как:

[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SendMessage(
    HWND hWnd, MSG msg, WPARAM wParam, [In, Out] NativeStruct lParam);

HWND - это псевдоним для System.IntPtr, MSG - System.UInt32, а WPARAM - System.UIntPtr.

[In, Out] атрибут lParam необходим для этой работы, в противном случае он, кажется, не получает маршалинг в обоих направлениях (до и после вызова нативного кода).

Я звоню с:

CHARFORMAT2 cf = new CHARFORMAT2();
SendMessage(rtfControl.Handle, (MSG)EM.GETCHARFORMAT, (WPARAM)SCF.DEFAULT, cf);

EM и SCF - это enum s. Я опять упущен для (относительной) краткости.

Я проверяю успех с:

Console.WriteLine(cf.szFaceName);

и я получаю:

Microsoft Sans Serif

Работает как шарм!


Хм или нет, в зависимости от того, сколько вы спали и сколько вещей вы пытаетесь сделать одновременно, я полагаю.

Этот будет работать, если CHARFORMAT2 будет blittable тип. (Blittable тип - это тип, который имеет такое же представление в управляемой памяти, как и в неуправляемой памяти.) Например, MINMAXINFO тип работает , как описано.

[StructLayout(LayoutKind.Sequential)]
class MINMAXINFO : NativeStruct{
    public Point ptReserved;
    public Point ptMaxSize;
    public Point ptMaxPosition;
    public Point ptMinTrackSize;
    public Point ptMaxTrackSize;
}

Это потому, что типы blittable на самом деле не маршалируются. Они просто закреплены в памяти - это удерживает GC от их перемещения - и адрес их расположения в управляемой памяти передается собственной функции.

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

Структура CHARFORMAT2 не является близаемой из-за члена string. CLR не может просто передать указатель на объект .NET string, где ожидается массив символов фиксированной длины. Таким образом, структура CHARFORMAT2 должна быть упорядочена.

Как представляется, для правильного маршалинга функция взаимодействия должна быть объявлена ​​с маршалируемым типом. Другими словами, учитывая приведенное выше определение, CLR должен делать какое-то определение на основе статического типа NativeStruct. Я бы предположил, что он правильно определяет необходимость маршалинга объекта, но затем только "маршалинг" объекта с нулевым байтом, размером NativeStruct.

.

Таким образом, чтобы ваш код работал для CHARFORMAT2 (и любых других неблизких типов, которые вы можете использовать), вам придется вернуться к объявлению SendMessage как объекта CHARFORMAT2. Извините, что сбил вас с толку.


Капча для предыдущего редактирования:

Уиппет

Да, хорошо!


Cory

Это не по теме, но я заметил потенциальную проблему для вас в приложении, похоже, что вы делаете.

Элемент управления rich textbox использует стандартные функции измерения текста и рисования текста GDI. Почему это проблема? Потому что, несмотря на заявления о том, что шрифт TrueType на экране выглядит так же, как и на бумаге, GDI точно не размещает символы. Проблема округления.

GDI использует целочисленные подпрограммы для измерения текста и размещения символов. Ширина каждого символа (и высота каждой строки, в этом отношении) округляется до ближайшего целого числа пикселей без исправления ошибок.

Ошибка легко увидеть в вашем тестовом приложении. Установите шрифт Courier New на 12 пунктов. Этот шрифт фиксированной ширины должен содержать символы ровно 10 на дюйм, или 0,1 дюйма на символ. Это должно означать, что при ширине стартовой строки 5,5 дюймов вы должны уместить до 55 символов в первой строке до начала переноса.

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123

Но если вы попробуете, вы увидите, что перенос происходит только после 54 символов. Более того, символ 54 th и часть 53 rd выступают за видимое поле, отображаемое на линейке.

Предполагается, что у вас есть настройки со стандартным 96 DPI (обычные шрифты). Если вы используете 120 DPI (большие шрифты), вы не увидите этой проблемы, хотя, по-видимому, в этом случае вы неправильно определяете размер элемента управления. Вы также вряд ли увидите это на распечатанной странице.

Что здесь происходит? Проблема в том, что 0,1 дюйма (ширина одного символа) составляет 9,6 пикселей (опять же, используя 96 DPI). GDI не разделяет символы, используя числа с плавающей точкой, поэтому округляет до 10 пикселей. Таким образом, 55 символов занимают 55 * 10 = 550 пикселей / 96 точек на дюйм = 5,7291666 ... дюймов, тогда как мы ожидали 5,5 дюймов.

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

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

Удачи!


1 Абстрактный базовый класс

3 голосов
/ 16 декабря 2009

У меня были забавные случаи, когда параметр что-то вроде ref Guid parent и соответствующая документация гласит:

"Указатель на GUID с указанием родителя. Передайте нулевой указатель для использования [вставить некоторый системный элемент] ."

Если null (или IntPtr.Zero для IntPtr параметров) действительно является недопустимым параметром, тогда вы можете использовать параметр ref - может быть, даже лучше, так как совершенно ясно, что именно нужно передать .

Если null является допустимым параметром, вы можете передать ClassType вместо ref StructType. Объекты ссылочного типа (class) передаются как указатели, и они допускают null.

2 голосов
/ 16 декабря 2009

Нет, вы не можете перегрузить SendMessage и сделать аргумент wparam целым. Это приведет к сбою вашей программы в 64-битной версии операционной системы. Это должен быть указатель, либо IntPtr, легкая ссылка или тип значения out или ref. В противном случае возможна перегрузка типа out / ref.


РЕДАКТИРОВАТЬ: Как указала OP, это на самом деле не проблема. Соглашение о вызове 64-битной функции передает первые 4 аргумента через регистры, а не через стек. Таким образом, нет опасности неправильного выравнивания стека для аргументов wparam и lparam.

1 голос
/ 16 декабря 2009

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

1 голос
/ 16 декабря 2009

Недостатков не вижу.

By-ref часто достаточно для простого типа и простой структуры.

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

...