您的位置:

PInvoke——一个强大而又不太为人所知的编程技术

随着软件开发的不断进步,我们今天可以做到的事情昨天或者是前几年是不可能的,但是很多时候这些进步并不为我们所知,而PInvoke就是其中一个值得我们深入了解的技术。

一、PInvoke库

PInvoke(Platform Invoke) 是一个强有力的 .NET Framework 功能,它提供了一种在 .NET 应用程序中调用非托管 DLL 函数的方法。PInvoke 的一项主要功能就是能够让 .NET Framework 应用程序直接使用基于 C 或 C++ 的非托管代码,而这些代码通常是通过相应 DLL 导出的。

在实际编程中,我们会遇到很多需要调用非托管代码的情况。比如,我们需要调用操作系统的 API 功能,调用硬件驱动程序的函数等等。这些非托管代码往往是以 C 或 C++ 等语言编写的,它们不受 .NET Framework 的管理,与 .NET 应用程序的运行环境也是非常不同的。因此,我们在使用这些非托管代码时,需要借助 PInvoke 技术,实现 .NET 应用程序与这些非托管代码之间的互操作。


[DllImport("User32.dll")]
static extern int MessageBox(int h, string m, string c, int type);

以上是一个C# 调用MessageBox的示例,包含DllImport Attribute,它用于指定需要哪个动态链接库(DLL)和其内部函数,在应用程序中使用 unmanaged API 。使用 DllImport 可以达到以下目的:

  • 将参数从一个数据类型转化到另一个数据类型
  • 指定 DLL 文件和函数的入口点,因为它们是 unmanaged 的
  • 返回调用相关 API 函数的结果。

二、PInvoke gdi32

GDI+是图形设备界面(GDI)内置图形渲染引擎的后续版本,最近的版本是 DirectX。

下面的 P/Invoke 函数用于打开并显示图像文件:


[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hDc);
 
public static void DrawImg()
{
    OpenFileDialog openFileDialog = new OpenFileDialog();
    openFileDialog.Filter = "Image Files(*.bmp;*.jpg;*.jpeg;*.png;)|*.bmp;*.jpg;*.jpeg;*.png;";//过滤器
    if (openFileDialog.ShowDialog() == DialogResult.OK)
    {
        using (Bitmap bitmap = new Bitmap(openFileDialog.FileName))
        {
            IntPtr hBitmap = bitmap.GetHbitmap();
            try
            {
                using(Graphics g = Graphics.FromHwnd(IntPtr.Zero))
                {
                    IntPtr hDC = g.GetHdc();
                    try
                    {
                        using( Graphics imageGrpahics = Graphics.FromImage(bitmap))
                        {
                            IntPtr hdcImage = imageGrpahics.GetHdc();
                            try
                            {
                                BitBlt(hDC, 0, 0, bitmap.Width, bitmap.Height, hdcImage, 0, 0, SrcCopy);
                            }
                            finally
                            {
                                imageGrpahics.ReleaseHdc(hdcImage);
                            }
                        }
                    }
                    finally
                    {
                        g.ReleaseHdc(hDC);
                    }
                }
            }
            finally
            {
                DeleteObject(hBitmap);
            }
        }
    }
}

以上代码可以在 openfiledialog 中选取图片文件,并将其显示在程序界面中。

三、PInvoke DLL

P/Invoke 在非托管代码中调用托管(.Net)代码,通过 C++/CLI 支持实现混合编程,通过导出 C++ 静态类导出成非托管 DLL 供其他工程(包括非 .Net 工程)引用,从而实现托管代码的动态链接。

下面是一个使用 P/Invoke 的示例:

在 Visual Studio 中,创建一个新的 C++ CLR 项目,我们可以编写以下代码导出一个以 System.String 为参数,返回 System.Int32 的函数 Multiply:


#include "pch.h"
#include "tchar.h" 
 
using namespace System;
 
namespace ExampleCppDll
{
    public ref class CppWrapper
    {
    public:
        static int Multiply(String^ operand)
        {
            try
            {
                int i_Result = 1;
                for each(Char ch in operand)
                {
                    i_Result *= Convert::ToInt32(ch.ToString());
                }
                return i_Result;
            }
            catch(...)
            {
                return -1;
            }
        }
     };
}
 
extern "C"
{
    __declspec(dllexport) 
    int __stdcall Multiply(LPCTSTR operand)
    {
        return ExampleCppDll::CppWrapper::Multiply(gcnew String(operand));
    }
}

然后编译生成 DLL ,在C# 中,我们可以使用 P/Invoke 调用刚刚写的 Multiply 函数:


[DllImport("ExampleCppDll.dll", CharSet = CharSet.Unicode)]
public static extern int Multiply(string strNumbers); 

四、PInvoke vb6

通过使用 PInvoke 在 VB6 中调用 unmanaged 代码。

下面是一个使用 PInvoke 的示例:


Private Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" ( _
    ByRef lpVersionInformation As OSVERSIONINFO _
) As Long
 
Private Type OSVERSIONINFO
    dwOSVersionInfoSize As Long
    dwMajorVersion As Long
    dwMinorVersion As Long
    dwBuildNumber As Long
    dwPlatformId As Long
    szCSDVersion As String * 128       ' Size of string buffer in Bytes
End Type
 
Private Sub Form_Load()
    Dim os_version As OSVERSIONINFO
    os_version.dwOSVersionInfoSize = LenB(os_version)
    Call GetVersionEx(os_version)
    Debug.Print "Windows version: " & os_version.dwMajorVersion & "." & os_version.dwMinorVersion
End Sub 

以上代码可以在 VB6 中调用 GetVersionEx 函数,获取 Windows 版本信息,并输出到调试窗口中。

五、PInvoke文档

P/Invoke 的参考文档支持许多方面:

  • MDSN 使用 MSDN Library 搜索或 MSDN 中的 P/Invoke 索引
  • Pinvoke.net 它是开发人员社区,提供 P/Invoke 不断更新的文档库
  • PInvoke Interop Assistant 可以帮助您将 C、C 和 C++ 头文件转换为 C# PInvoke 或 VB declarative DLL 函数声明。

六、PInvoke技术

你可以通过PInvoke 技术将合适的编程语言组合实现所需的操作,同时明白一些坑点。

以下是一些 PInvoke 技术示例:

  • 使用 PInvoke 和 Callback 委托将 Windows 程序代码转换为 C# 中的代码
  • 在嵌入式 Windows CE 中使用 PInvoke 和 .Net Compact Framework / Windows Mobile
  • 在 WPF 中使用 PInvoke、SetWindowLong 和 HwndSource 实现 WindowTransparency
  • PCreate ProcessAsUser with SeIncreaseQuotaPrivilege

七、PInvokeStackImbalance

PInvokeStackimbalance 是对于 P/Invoke 传递参数而产生的错误类型的名字,主要由于目标函数参数数目或类型不匹配而导致。针对该错误,可以通过调整P/Invoke 声明中的参数数量或类型来解决。

以下是解决栈平衡错误的 P/Invoke 声明示例:


[DllImport("User32.dll")]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);

在调用 MoveWindow 时,需要设置第 5 个参数为 bool 类型而不是 int 类型,以下是正确的声明:


[DllImport("User32.dll")]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, bool bRepaint);

八、PInvoke openfiledialog

PInvoke OpenFileDialog

以下是 P/Invoke OpenFileDialog 的示例代码:


[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr GetForegroundWindow();
 
[STAThread]
static void Main()
{
    var opDlg= new OpenFileDialog();
    opDlg.ShowDialog(GetForegroundWindow);
}

以上代码通过 P/Invoke OpenFileDialog 在提示框架之间提供弹出式窗口。

结论

通过 PInvoke,在非托管代码中调用托管代码,我们可以更简单的处理 unmanaged 代码。无论我们想在 .Net 中调用现有的 C/C++ 动态链接库或使 C# 代码跨越语言边界工作,在经验和技术上,P/Invoke 都是不可或缺的一项功能。