在 C# 里引入 C++ 类型,增强 Interop 体验

本文最后更新于:19 小时前

开门见山

各位想过在 C# 里直接使用 HWND、BOOL、COLORREF、HRESULT、HDC 等这些常见的 Win32 类型吗?感兴趣的话就来看看吧。

起因

为什么我会想到这种操作呢?这还得从下面两件事说起

万恶之源—RECT

经常做 Interop 的朋友们一定都写过这个类型吧,它是用来表示对象位置的大小的结构体:

1
2
3
4
5
6
7
8
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}

那大家想过没有,这本来就是一个 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
[GeneratedCode("Microsoft.Windows.CsWin32", "0.3.106+a37a0b4b70")]

这表示该代码是被生成的,不是人为编写的,并且该生成器的名称是 Microsoft.Windows.CsWin32,于是你便去全网通缉它

第一个就是,还是在微软官方的 GitHub 仓库。

进去后,我们得知,这是个用来快速生成 P/Invoke 方法以及相应的类型的库,旨在释放开发者们的双手,不必手动书写各种 Win32 定义了,并且指针、句柄安全,无需我们自己去释放什么的 (如果有的话)。

那就没得说了,于是你的格局在这里一下就被打开了,你觉得有必要为自己也来一套这样的机制。

但你发现生成器生成的代码太多了,有点多此一举。因为,就连我们熟悉的 RECT 类型,本该7行代码就能完成的,居然变成了这样

这你能忍?虽然目的都是为了更好的使用,但还是略显臃肿。

所以我们就可以手写这些类型:

手写 Win32 类型

这里就给出部分常用的 Win32 类型在 C# 中的定义,其他请自行套用类似的方法来构造,但一定确保双方 (C# 和 C++) 的类型都是 Blittable。

BOOL

那我们先来盘一下 BOOL,首先得知道 BOOL 对应 C# 的什么类型?

你以为是 bool?戳了,看看 C++ 中的定义

1
typedef int                 BOOL; // 来自 minwindef.h

所以 BOOL 实际上是 int,而 int 确实是 Blittable 类型,那我们就可以在 C# 中定义了。

怎么定义?写个结构,命名为 BOOL,把 int 放进去

1
2
3
4
5
[DebuggerDisplay("{(Value == 0 ? false : true)}")] // 方便我们进行调试时 Visual Studio 直接显示 BOOL 是 true 还是 false 
public readonly struct BOOL
{
private readonly int Value;
}

完成了,就这么简单,现在你可以把那些声明的导入函数中写的 bool 全部换成 BOOL 了,也就是把

1
2
3
[DllImport("somedll.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Function([MarshalAs(UnmanagedType.Bool)] bool enabled);

改成

1
2
[DllImport("somedll.dll")]
private static extern BOOL Function(BOOL enabled);

为了更方便我们使用,比如我们想用 Win32 的 TRUEFALSE,以及可以让 BOOL 写在 C# 的 if 语句里,我们可以扩展一下该结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[DebuggerDisplay("{(Value == 0 ? false : true)}")]
public readonly struct BOOL
{
private readonly int Value;

// 为了不用每次使用都创建新实例,我们可以创建一个静态字段来表示 TRUE 和 FALSE
public static readonly BOOL TRUE = new(true);
public static readonly BOOL FALSE = new(false);

private BOOL(bool value)
{
Value = value ? 1 : 0;
}

public static implicit operator bool(BOOL b)
{
// 隐式转换运算符重载,BOOL -> bool
return b.Value != 0; // 我们规定在这里返回结果为 true 的 bool 语句
}

public static explicit operator BOOL(bool b)
{
// 显式转换运算符重载,bool -> BOOL
return new(b);
}
}

这样就大功告成了,我来写个示例代码,看看效果

1
2
3
4
5
6
7
8
9
10
11
private BOOL Test()
{
var b = (BOOL)true; // 显式转换

if (b) // 隐式转换
{
return BOOL.TRUE;
}

return BOOL.FALSE;
}

COLORREF

这就是 Win32 中表示颜色的结构,底层也是 int。C# 中可以使用 ColorTranslator.ToWin32() 将 Color 转换为 int,即 COLORREF。

1
2
typedef unsigned long       DWORD;
typedef DWORD COLORREF;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public readonly struct COLORREF
{
private readonly int Value;

private COLORREF(Color color)
{
Value = ColorTranslator.ToWin32(color);
}

public static implicit operator COLORREF(Color c)
{
return new(c);
}
}

什么?为什么不用 ulong?因为 COLORREF (0x00BBGGRR) 不会超出 int.MaxValue

RECT

虽然文章开头提到了,但这里还是给出完整实现

1
2
3
4
5
6
7
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT
1
2
3
4
5
6
7
8
9
10
11
12
13
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;

public static implicit operator Rectangle(RECT r)
{
return Rectangle.FromLTRB(r.Left, r.Top, r.Right, r.Bottom);
}
}

HWND

这个类型也挺常用的,就是窗口句柄嘛,在 C# 里我们通常用 IntPtr 表示。

这里就不多讲解了,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public readonly struct HWND
{
private readonly IntPtr Value;

private HWND(IntPtr value)
{
Value = value;
}

public static implicit operator bool(HWND hWnd)
{
return hWnd.Value != IntPtr.Zero;
}

public static implicit operator HWND(IntPtr ptr)
{
return new(ptr);
}

public static explicit operator IntPtr(HWND hWnd)
{
return hWnd.Value;
}
}

HDC

这个结构也对应 C# 中 IntPtr,和 HWND 除了名字以外一模一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public readonly struct HDC
{
private readonly IntPtr Value;

private HDC(IntPtr value)
{
Value = value;
}

public static implicit operator bool(HDC hDC)
{
return hDC.Value != IntPtr.Zero;
}

public static implicit operator HDC(IntPtr ptr)
{
return new(ptr);
}

public static explicit operator IntPtr(HDC hDC)
{
return hDC.Value;
}
}

疑难解答

  • 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 就要加)。

在 C# 里引入 C++ 类型,增强 Interop 体验
https://wanghaonie.github.io/posts/27d8f4ba0ca2/
作者
WangHaonie
发布于
2025-07-28 09:54:58
更新于
2025-07-31 15:13:01
许可协议