在 C# 里引入 C++ 类型,增强 Interop 体验
本文最后更新于:19 小时前
开门见山
各位想过在 C# 里直接使用 HWND、BOOL、COLORREF、HRESULT、HDC 等这些常见的 Win32 类型吗?感兴趣的话就来看看吧。
起因
为什么我会想到这种操作呢?这还得从下面两件事说起
万恶之源—RECT
经常做 Interop 的朋友们一定都写过这个类型吧,它是用来表示对象位置的大小的结构体:
1 |
|
那大家想过没有,这本来就是一个 Win32 类型,我们在 C# 中定义,居然也能正常运行?
这得得益于 Win32 类型基本上都是值类型,且所有的字段都是 Blittable 的,也就是在内存中的排列是有序 (Sequential) 的,且和 C# 类型 (int、uint、long、ulong 等基本值类型) 相同,两者都是 Blittable 类型,C# 端只管将各字段按顺序读取或写入内存就行了。
格局打开,既然 RECT 这个类型我们都能在 C# 定义并正常运作,那为何不尝试定义一下其他更常见的类型呢?
CsWin32—强大的源生成器
如果你时常在 source.dot.net 上浏览 WinForms 的源码,你一定会看到一些 Win32 类型:
上图就出现了诸如 LRESULT、HWND、WPARAM、LPARAM 等 Win32 类型,如果你好奇它们是怎么被定义的,当你点击进去,你会发现:
它被声明为了 partial,这意味着我们不能在这里看到它的完整实现了。另一半代码的话,反正我是没在 source.dot.net 和 GitHub 上找到,但还有一个更直接的方法。
于是聪明的你发现它位于 System.Private.Windows.Core 程序集中的 Windows.Win32.Foundation 命名空间下,接着你便直接用 Everything 通缉电脑中相关文件
连家都被偷了,还愁找不到完整代码吗?接下来我要做什么大家应该都清楚吧。
直接挑一个最大的 (通常说明版本更新),拖入到某反编译软件中,并展开到 Windows.Win32.Foundation 命名空间:
各种 Win32 类型尽收眼底,那我们还是打开 HWND 的定义看看
这不就是完整实现吗?
并且你还发现这个 struct 被加上了一个很吸引眼球的特性:
1 |
|
这表示该代码是被生成的,不是人为编写的,并且该生成器的名称是 Microsoft.Windows.CsWin32
,于是你便去全网通缉它
第一个就是,还是在微软官方的 GitHub 仓库。
点进去后,我们得知,这是个用来快速生成 P/Invoke 方法以及相应的类型的库,旨在释放开发者们的双手,不必手动书写各种 Win32 定义了,并且指针、句柄安全,无需我们自己去释放什么的 (如果有的话)。
那就没得说了,于是你的格局在这里一下就被打开了,你觉得有必要为自己也来一套这样的机制。
但你发现生成器生成的代码太多了,有点多此一举。因为,就连我们熟悉的 RECT 类型,本该7行代码就能完成的,居然变成了这样
这你能忍?虽然目的都是为了更好的使用,但还是略显臃肿。
所以我们就可以手写这些类型:
手写 Win32 类型
这里就给出部分常用的 Win32 类型在 C# 中的定义,其他请自行套用类似的方法来构造,但一定确保双方 (C# 和 C++) 的类型都是 Blittable。
BOOL
那我们先来盘一下 BOOL,首先得知道 BOOL 对应 C# 的什么类型?
你以为是 bool?戳了,看看 C++ 中的定义
1 |
|
所以 BOOL 实际上是 int,而 int 确实是 Blittable 类型,那我们就可以在 C# 中定义了。
怎么定义?写个结构,命名为 BOOL,把 int 放进去
1 |
|
完成了,就这么简单,现在你可以把那些声明的导入函数中写的 bool 全部换成 BOOL 了,也就是把
1 |
|
改成
1 |
|
为了更方便我们使用,比如我们想用 Win32 的 TRUE
和 FALSE
,以及可以让 BOOL 写在 C# 的 if 语句里,我们可以扩展一下该结构体
1 |
|
这样就大功告成了,我来写个示例代码,看看效果
1 |
|
COLORREF
这就是 Win32 中表示颜色的结构,底层也是 int。C# 中可以使用 ColorTranslator.ToWin32() 将 Color 转换为 int,即 COLORREF。
1 |
|
1 |
|
什么?为什么不用 ulong?因为 COLORREF (0x00BBGGRR) 不会超出 int.MaxValue
RECT
虽然文章开头提到了,但这里还是给出完整实现
1 |
|
1 |
|
HWND
这个类型也挺常用的,就是窗口句柄嘛,在 C# 里我们通常用 IntPtr 表示。
这里就不多讲解了,直接上代码
1 |
|
HDC
这个结构也对应 C# 中 IntPtr,和 HWND 除了名字以外一模一样
1 |
|
疑难解答
- BOOL 在 Win32 被定义为 int,但在 C# 里你却用 BOOL 来封装 int,这河里?
这恒河里。并且这个问题问的有点奇怪,因为是在不同的层面上提出的。要理解的话,那我们就假设根本就没有 BOOL 这个东西,只有 int。这样在 C# 里我们直接写 int 就行,没有问题吧?现在我用一个结构把 int 封装起来,也就是嵌套了 (本来传 int 就行,现在传的是被 BOOL 封装的 int),看样子有问题,但实际上没问题。因为 CLR 在封送时会展开 (Flatten,平展,参考) 所有字段到最基本的类型,也就是会扒了 BOOL 的皮,只拿出里面的 int。还是没理解?那你自己做个小实验,先定义一个 RECT,再定义一个 MyRect 结构,里面只放一个字段public RECT rc;
,再把 MyRect 作为某 WinAPI 函数的返回值或参数的类型,运行看看得到的 MyRect 里的 RECT 里有没有值,你就恍然大悟了。我帮你试过了,看看:
- 你在定义了 BOOL 时使用了静态字段来表示 TRUE 和 FALSE,这些字段不会被 CLR 封送?
不会,封送时只针对实例字段。 - 为什么有些结构体加了 LayoutKind.Sequential,有些没有?
1. 如果你的 struct 只有一个字段,那肯定不需要的,因为一个字段不存在顺不顺序的问题
2. 在 C# struct 中,各实例字段默认本来就是按顺序排列的,理论上来说我们都可以不用加。但为了防止 CLR 某些优化导致顺序的可能发生改变,最好显式标注 LayoutKind.Sequential。这个特性告诉 CLR 该结构的所有实例字段必须按顺序排列,不要把它优化了。 - 为什么没有定义 LPCWSTR 等 Win32 常见字符串类型?
因为双方 (string 和 LPCWSTR) 都不是 Blittable 类型 (参考),这意味这你得自行处理各种指针以及转换,还要注意生命周期的问题等,所以不建议人工定义。如果非得要用这个类型的话,可以让 CsWin32 生成,或者用传统的[MarshalAs(UnmanageType.LPWStr)]
特性也足够了 (其实这个也可以不用加,当定义了 CharSet.Unicode 后,C# 默认当宽字符处理,除非是其他类型的字符串,比如 BSTR 就要加)。