【问题标题】:.NET Interop IntPtr vs. ref.NET 互操作 IntPtr 与 ref
【发布时间】:2010-12-27 00:34:19
【问题描述】:

可能是一个菜鸟问题,但互操作性还不是我的强项之一。

除了限制重载的数量之外,还有什么理由我应该像这样声明我的 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 Daddy:你有一个基于抽象(或任何)类的结构类的例子吗?我把我的签名改成:

[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, Version=1.0.9.1212, Culture=neutral, PublicKeyToken=111aac7a42f7965e”加载类型“CC.Utilities.WindowsApi.CHARFORMAT2”。

NativeStruct 类:

public class NativeStruct
{
}

我试过abstract,添加StructLayout 属性等,我得到了同样的异常。

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

编辑:

我没有遵循常见问题解答,我提出了一个可以讨论但没有得到肯定回答的问题。除此之外,该线程中还有很多有见地的信息。所以我会把它留给读者投票给答案。第一个投票超过 10 票将是答案。如果两天内(太平洋标准时间 12 月 17 日)没有答案,我将添加我自己的答案,总结线程中的所有美味知识:-)

再次编辑:

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

【问题讨论】:

  • NativeStruct 类也需要StructLayout 属性。我将发布我的工作示例代码。

标签: c# .net winapi interop intptr


【解决方案1】:

我没有看到任何缺点。

对于简单类型和简单结构来说,按引用通常就足够了。

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

【讨论】:

    【解决方案2】:

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

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

    如果null(或IntPtr.Zero 用于IntPtr 参数)确实是一个无效参数,那么您可以使用ref 参数 - 可能会更好,因为它更清楚您需要传递什么.

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        如果结构在没有自定义处理的情况下是可编组的,我非常喜欢后一种方法,在这种方法中,您将 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 在 kernel32.dll 中调用 GlobalAlloc,返回一个 HGLOBAL。这曾经与 HLOCAL 不同,在 16 位时代由 LocalAlloc 返回,但在 32 位 Windows 中它们是相同的。

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

        </pedantry>


        编辑

        您提到在使用类而不是结构时遇到 TypeLoadException,并要求提供示例。我使用CHARFORMAT2 进行了快速测试,因为它看起来就是您想要使用的。

        首先是ABC1:

        [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。

        lParam 上的[In, Out] 属性是此工作所必需的,否则,它似乎不会双向编组(在调用本机代码之前和之后)。

        我称之为:

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

        EM 和 SCF 是 enums,为了(相对)简洁,我再次省略了。

        我检查成功:

        Console.WriteLine(cf.szFaceName);
        

        我得到:

        微软无衬线字体

        像魅力一样工作!


        嗯,我想,这取决于你睡了多少觉,以及你想同时做多少事情。

        如果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 对象。抱歉,我在这件事上让你误入歧途。


        上一次编辑的验证码:

        鞭子

        是的,鞭子很好!


        科里,

        这是题外话,但我注意到您正在制作的应用可能存在问题。

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

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

        在您的测试应用中可以很容易地看到该错误。将字体设置为 Courier New 12 磅。这种固定宽度的字体应该每英寸恰好间隔 10 个字符,或每字符 0.1 英寸。这应该意味着,如果您的起始行宽为 5.5 英寸,那么在换行之前您应该能够在第一行放置 55 个字符。

        ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123

        但如果您尝试一下,您会发现仅在 54 个字符后发生换行。更重要的是,第 54th 个字符和第 53rd 个字符的一部分超出了标尺栏上显示的明显边距。

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

        这里发生了什么?问题是 0.1 英寸(一个字符的宽度)是 9.6 像素(同样,使用 96 DPI)。 GDI 不使用浮点数分隔字符,因此它会将其四舍五入为 10 像素。所以 55 个字符占用 55 * 10 = 550 像素 / 96 DPI = 5.7291666...英寸,而我们的预期是 5.5 英寸。

        虽然这在文字处理器程序的正常用例中可能不太明显,但也有可能出现自动换行出现在屏幕上和页面上的不同位置的情况,或者出现排列不一致的情况一旦像他们在屏幕上一样打印出来。如果这是您正在开发的商业应用程序,这可能会给您带来问题。

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

        祝你好运!


        1抽象基类

        【讨论】:

        • +1 用于结构 -> 类提醒。如果我这样做了,我可以为我的结构创建一个空的构造函数,比如 CHARFORMAT,它具有不同版本的 cbSize 属性。可以让我不必每次都使用 Marshal.SizeOf() 。
        • 它还可以让您免于对SendMessage 进行多次重载。您可以从单个抽象类派生所有本机类型定义并声明 SendMessage 以获取该类。
        • 感谢您迄今为止提供的所有重要信息。对 HGLOBAL / HLOCAL 的区别一无所知。我的例子是我在互联网上找到的,但我一直在走ref 路线。无论如何,我对您使用抽象基类的解决方案非常感兴趣,但遇到了一些问题(请参阅我的编辑)。我会继续努力弄明白的。
        • +2(如果可能,哈哈)你摇滚 P 爸爸。我的基类上缺少的StructLayout 属性导致了TypeLoadException,但问题是我将[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32] 用于szFaceName,导致消息失败。更改为UnmanagedType.ByValTStr(MUCH 更加用户友好)解决了消息失败问题。再次向您表示敬意,并感谢您提供的所有建议!
        • 我犯了一个错误。昨晚我累了,想做太多事情。查看编辑后的帖子。
        【解决方案5】:

        不,您不能重载 SendMessage 并将 wparam 参数设为 int。这将使您的程序在 64 位版本的操作系统上失败。它必须是一个指针,可以是 IntPtr、blittable 引用或 out 或 ref 值类型。否则重载 out/ref 类型也没问题。


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

        【讨论】:

        • 您有任何将 wParam 指定为指针的示例吗?当然,我刚刚开始深入研究这一点,但我见过的大多数都不是 wParam 的指针,例如msdn.microsoft.com/en-us/library/bb788026(VS.85).aspx。或者也许它们应该是指针,但描述并没有像 lParam 那样指定它。
        • 是否用作指针无所谓,该函数期望在x64中传递8个字节。您将通过 4,错误对齐其他参数。
        • 我之所以质疑是因为如果我使用 Reflector 查看 System.Windows.Forms.UnsafeNativeMethods 我会看到很多 SendMessage 重载。他们中的大多数人为 wParam 采用 Int32。当然,也有一些采用 IntPtr,但在使用中(从我的随意浏览中),首选 Int32 wParam 重载。所以显然有理由使用指针,但我想知道这是什么原因。显然,如果消息说要使用一个点,那么......但如果它说“wParam - 此参数未使用;它必须为零。”或“wParam - 以下之一(uint 定义的 const)”
        • 我的评论很长。无论如何,当我阅读您的 cmets 时,听起来 .NET RichTextBox(其中包括我正在使用 Reflector 查看的控件)不应该在 64 位操作系统中运行,我知道情况并非如此。跨度>
        • 哇,我也看到了。源代码文件具有针对 CA1901 (PInvokeDeclarationsShouldBePortable) 的 FxCop 警告抑制。我不知道这怎么可能起作用。也许 P/Invoke 编组器知道 SendMessage,似乎很遥远。我需要对此进行研究。
        猜你喜欢
        • 2011-08-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多