相对优雅得C#调用Win32API

今天有人找我解决一些软件相关的问题,然后发现会牵扯到一些Win32的API
一般调用Win32API都是用C++方便的,而且微软给的文档也是C++的
我虽然对C++不是很熟,但对C#熟啊

在C#中,我们可以通过P/Invoke去做动态库的互操作
但对于Win32API,如果自己写P/Invoke,里面就有很多不确定的东西
光是那个DllImport特性里面的参数就不是很好填

那C#开发者是不是很难做Win32开发呢,那也不是
在Nuget上有很多别的大佬包装好的Win32库,安装完可以直接调用
但Win32中有超级多的API,这会导致代码提示里面会多出非常多东西,确实是有点干扰了

但好在微软也提供了一个解决方案,CsWin32
这个是基于C#源生成器的Win32API封装器
就是需要什么API,它就去生成那些API的P/Invoke代码
并且生成的质量还挺高的,保留原本的调用风格的同时,尽可能去迎合C#的开发体验

<0x00> 安装CsWin32

因为会用到C#的源生成器,所以最好使用VS2022并且相对新的版本,不然代码提示之类的会出问题
VSCode的源生成器体验也是不错的,这个直接装C# kit插件就可以了

直接在Nuget中搜索CsWin32,包名是Microsoft.Windows.CsWin32
这个包最低支持到.net framework 4.5也即.NET Standard 2.0
对于这个版本,还需要安装System.Memory
如果是.NET Standard 2.1及以上,也即.net 5.0及以上的版本,那么就不需要安装
(如果是新项目的话应该会用新版本的框架吧)

<0x01> 如何使用

添加需要的API

首先是要告诉源生成器要生成哪些API的封装
在项目根目录新建叫NativeMethods.txt的文件

在里面可以添加你需要添加的Win32函数或者结构的名字

PssCaptureSnapshot
PssQuerySnapshot

PSS_PROCESS_INFORMATION
PSS_HANDLE_INFORMATION
PSS_VA_CLONE_INFORMATION

这里的示例是Windows进程快照相关的API,使用的API也不多,仅做演示作用

简单解释下使用的API

PssCaptureSnapshot

捕获目标进程的快照

DWORD PssCaptureSnapshot(
  [in]           HANDLE            ProcessHandle,
  [in]           PSS_CAPTURE_FLAGS CaptureFlags,
  [in, optional] DWORD             ThreadContextFlags,
  [out]          HPSS              *SnapshotHandle
);

ProcessHandle目标进程的句柄
CaptureFlags指定要捕获的标志
ThreadContextFlags如果 CaptureFlags 指定线程上下文,则要捕获的 CONTEXT 记录标志
SnapshotHandle返回此函数捕获的快照的句柄

函数返回的是winerror.h中定义的错误代码,无错误是ERROR_SUCCESS

PssQuerySnapshot

查询捕获的快照的信息

DWORD PssQuerySnapshot(
  [in]  HPSS                        SnapshotHandle,
  [in]  PSS_QUERY_INFORMATION_CLASS InformationClass,
  [out] void                        *Buffer,
  [in]  DWORD                       BufferLength
);

SnapshotHandle要查询的快照的句柄
InformationClass用于选择要查询的信息
Buffer此函数提供的信息,类型由InformationClass决定
BufferLength缓冲区的大小(以字节为单位)

函数返回的是winerror.h中定义的错误代码,无错误是ERROR_SUCCESS

剩下的是一些结构,东西多就不细讲了,跟本文关系不大,具体就看文档吧

在代码中使用Win32API

这里做一个使用Win32API查询进程PID的示例
(虽然C#的Process类对象本身就可以直接查询)

using System.Diagnostics;
using Windows.Win32;
using Windows.Win32.System.Diagnostics.ProcessSnapshotting;

public class Program
{
    public static void Main()
    {
	    // 使用C#自带的Process类型获取记事本进程
        Process test = Process.GetProcessesByName("notepad")[0];
        // 声明一个查询flags
        PSS_CAPTURE_FLAGS flags = PSS_CAPTURE_FLAGS.PSS_CAPTURE_THREADS;
        // 使用Win32API来捕获进程快照
        PInvoke.PssCaptureSnapshot(
            test.SafeHandle,
            flags,
            0,
            out HPSS snapshotHandle);
        // 声明查询进程基本信息的变量
        PSS_PROCESS_INFORMATION info;
        // 涉及到指针操作,所以要用unsafe块包装
        unsafe
        {
	        // 使用Win32API来查询进程快照信息
            PInvoke.PssQuerySnapshot(
                snapshotHandle,
                PSS_QUERY_INFORMATION_CLASS.PSS_QUERY_PROCESS_INFORMATION,
                &info,
                (uint)sizeof(PSS_PROCESS_INFORMATION));
        }
        Console.WriteLine(info.ProcessId);
    }
}

运行结果

确实是获取到了记事本的PID

而且观察代码,不难发现,CsWin32会非常智能地使用C#自带的类型
比如说Process类型里面的SafeHandle属性,这个返回的是SafeProcessHandle
这个虽然它的命名空间是Microsoft.Win32.SafeHandles,但确实是C#本身就有的
对于C#不带的类型,只有使用的API需要这些类型传参,CsWin32才会去生成对应的代码

<0x02> 一些不知道类型的枚举

如果你需要使用某个枚举,但不知道是什么类型,CsWin32可以自动指出具体类型
比如前面提到那两个API的返回值是winerror.h中定义的错误代码,但我们不知道这是什么类
这时候可以在NativeMethods.txt里面直接加上ERROR_SUCCESS
CsWin32会抛出警告:应该使用正确的声明

最后也指出正确的声明是WIN32_ERROR,文件里改好就行
这里对上面的代码稍作修改作为示例

using System.Diagnostics;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ProcessSnapshotting;

public class Program
{
    public static void Main()
    {
        Process test = Process.GetProcessesByName("notepad")[0];
        PSS_CAPTURE_FLAGS flags = PSS_CAPTURE_FLAGS.PSS_CAPTURE_THREADS;
        PInvoke.PssCaptureSnapshot(
            test.SafeHandle,
            flags,
            0,
            out HPSS snapshotHandle);
        PSS_PROCESS_INFORMATION info;
        // 添加错误码的声明
        uint errorCode;
        unsafe
        {
	        // 获取错误码返回值
            errorCode = PInvoke.PssQuerySnapshot(
                snapshotHandle,
                PSS_QUERY_INFORMATION_CLASS.PSS_QUERY_PROCESS_INFORMATION,
                &info,
                (uint)sizeof(PSS_PROCESS_INFORMATION));
        }
        Console.WriteLine(info.ProcessId);
        // 跟WIN32_ERROR.ERROR_SUCCESS做比较,返回True
        Console.WriteLine((WIN32_ERROR)errorCode == WIN32_ERROR.ERROR_SUCCESS);
    }
}

当然头铁不改正确的声明也没关系,也是这样使用

对于其他不知道怎么声明的枚举也可以这样让CsWin32去找