.NET Interop IntPtr与ref

可能是一个菜鸟问题但是互操作不是我的优点之一。

除了限制重载次数之外,还有什么理由我应该声明我的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); 

by ref重载最终更容易使用,但我想知道是否存在我不知道的缺点。

编辑:

到目前为止,有很多很棒的信息。

@P爸爸:你有一个基于抽象(或任何)类的结构类的例子吗? 我将签名改为:

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

没有InOut和Marshal作为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,Version = 1.0.9.1212,Culture = neutral,PublicKeyToken = 111aac7a42f7965e”加载类型“CC.Utilities.WindowsApi.CHARFORMAT2”,因为格式无效。

NativeStruct类:

 public class NativeStruct { } 

我尝试过abstract ,添加StructLayout属性等,我得到了相同的exception。

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

编辑:

我没有按照常见问题解答,我问了一个可以讨论但没有得到积极回答的问题。 除此之外,这个post中还有很多有见地的信息。 所以我会把它留给读者投票给答案。 第一个到10个以上的投票将是答案。 如果在两天(太平洋标准时间12/17)没有答案满足这一点,我将添加我自己的答案,总结线程中的所有美味知识:-)

再次编辑:

我撒谎,接受P爸爸的答案,因为他是男人,并且得到了很大的帮助(他也有一只可爱的小猴子:-P)

如果结构是可编组的而没有自定义处理,我更喜欢后一种方法,在这种方法中,你将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 

顺便说一下,在你的例子中将指针作为IntPtr传递,你使用了错误的AllocSendMessage不是COM函数,因此您不应该使用COM分配器。 使用Marshal.AllocHGlobalMarshal.FreeHGlobal 。 他们名字不好; 如果您已经完成了Windows API编程,这些名称才有意义,甚至可能不是。 AllocHGlobal在kernel32.dll中调用GlobalAlloc ,它返回一个HGLOBAL 。 这曾经与16天后由LocalAlloc返回的HLOCAL不同,但在32位Windows中它们是相同的。

我想,使用术语HGLOBAL来指代(本机)用户空间内存块只是一种卡住了,设计Marshal类的人不应该花时间去思考对于大多数人来说多么不直观.NET开发人员。 另一方面,大多数.NET开发人员不需要分配非托管内存,所以….


编辑

你提到你在使用类而不是结构时得到一个TypeLoadException,并要求提供一个样本。 我使用CHARFORMAT2了快速测试,因为它看起来就像你正在尝试使用的那样。

首先是ABC 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别名为DWORDLCIDCOLORREF ,并将System.UInt16别名为WORD 。 我尝试尽可能地将我的P / Invoke定义保持为SDK规范。 CFMCFE是包含这些字段的标志值的enums 。 为简洁起见,我将其定义排除在外,但如果需要可以添加它们。

我已将SendMessage声明为:

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

HWNDSystem.IntPtr的别名, MSGSystem.UInt32WPARAMSystem.UIntPtr

lParam上的[In, Out]属性是必需的,否则,它似乎没有被两个方向封送(在调用本机代码之前和之后)。

我叫它:

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

EMSCF再次被忽略了(相对)简洁。

我检查成功:

 Console.WriteLine(cf.szFaceName); 

我得到:

 微软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无法移动它们 – 它们在托管内存中的位置地址被传递给本机函数。

非blittable类型必须编组。 CLR分配非托管内存并在托管对象及其非托管表示之间复制数据,从而在格式之间进行必要的转换。

由于string成员, CHARFORMAT2结构是非blittable。 CLR不能只传递一个指向.NET string对象的指针,在该对象中需要一个固定长度的字符数组。 因此必须对CHARFORMAT2结构进行封送处理。

如图所示,为了正确编组,必须使用要编组的类型声明互操作函数。 换句话说,鉴于上述定义,CLR必须根据NativeStruct的静态类型进行某种确定。 我猜想它正确地检测到对象需要被编组,但只是“编组”一个零字节对象,即NativeStruct本身的大小。

因此,为了使您的代码适用于CHARFORMAT2 (以及您可能使用的任何其他非blittable类型),您将不得不返回将SendMessage声明为获取CHARFORMAT2对象。 对不起,我把你误入歧途。


上一次编辑的Captcha:

whippet

是的,鞭子好!


科里

这是偏离主题的,但我注意到你在应用程序中看起来像你正在制作的潜在问题。

富文本框控件使用标准的GDI文本测量和文本绘制function。 为什么这是个问题? 因为尽管声称TrueType字体在屏幕上看起来与纸上字体相同,但GDI并不能准确地放置字符。 问题在于四舍五入。

GDI使用全整数例程来测量文本和放置字符。 每个字符的宽度(以及每条线的高度,就此而言)四舍五入到最接近的整数像素,没有纠错。

您可以在测试应用中轻松看到错误。 将字体设置为Courier New,12点。 这种固定宽度的字体应该将字符精确地放在每英寸10个字符或每个字符0.1英寸的位置。 这应该意味着,如果您的起始线宽为5.5英寸,您应该能够在换行之前在第一行上放置55个字符。

  ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123 

但是如果你尝试,你会发现只有54个字符后才会发生换行。 更重要的是第54 角色和第53 角色的一部分突出了标尺栏上显示的明显边缘。

假设您的设置为标准96 DPI(普通字体)。 如果您使用120 DPI(大字体),您将看不到此问题,尽管在这种情况下您可能无法正确调整控件的大小。 您也不太可能在打印页面上看到这一点。

这里发生了什么? 问题是0.1英寸(一个字符的宽度)是9.6像素(再次,使用96 DPI)。 GDI不使用浮点数对空格字符进行空格,因此它最多可将此数字舍入为10像素。 所以55个字符需要55 * 10 = 550像素/ 96 DPI = 5.7291666 …英寸,而我们所期望的是5.5英寸。

虽然在文字处理程序的正常使用情况下这可能不太明显,但有可能出现在屏幕上不同位置而不是页面上发生自动换行的情况,或者一旦打印出来就会出现相同的情况。他们在屏幕上做了。 如果这是您正在处理的商业应用程序,这可能会成为您的问题。

不幸的是,解决这个问题并不容易。 这意味着你将不得不放弃丰富的文本框控件,这意味着为你自己实现它所做的一切都是一个巨大的麻烦,这是非常多的。 这也意味着您必须实现的文本绘图代码变得相当复杂。 我有代码可以做到这一点,但是在这里发布它太复杂了。 但是,您可能会发现此示例或此 示例很有用。

祝好运!


1抽象基类

我有一些有趣的案例,其中参数类似于ref Guid parent ,相应的文档说:

“指向父级的GUID的指针。传递空指针以使用 [插入一些系统定义的项目] 。”

如果null (或IntPtr.Zero for IntPtr参数)确实是一个无效参数,那么你可以使用ref参数 – 可能更好,因为它更清楚你需要通过什么。

如果null是有效参数,则可以传递ClassType而不是ref StructType 。 引用类型( class )的对象作为指针传递,它们允许null

不,您不能重载SendMessage并使wparam参数成为int。 这将使您的程序在64位版本的操作系统上失败。 它必须是一个指针,IntPtr,blittable引用或out或ref值类型。 超出out / ref类型是否正常。


编辑:正如OP指出的那样,这实际上不是问题。 64位函数调用约定通过寄存器传递前4个参数,而不是堆栈。 因此,对于wparam和lparam参数,没有堆栈错位的危险。

我没有看到任何缺点。

对于简单类型和简单结构,By-ref通常就足够了。

如果结构具有可变大小或者您想要进行自定义处理,则应该优先使用IntPtr。

使用ref比手动操作指针更简单且更不容易出错,因此我认为没有充分理由不使用它…使用ref另一个好处是你不必担心释放非托管分配的内存