互操作签名最危险的错误往往没有编译提示:C# 和 C 都存在名为 long、bool、char 的类型,但名称相同不代表大小、编码或 ABI 表示相同。一个字段宽度错了,后续参数就可能全部错位。
本文建立从原生头文件到 C# 声明的核对方法。目标不是背一张万能映射表,而是根据平台、编译器和头文件选择位宽明确的托管类型。
1. 优先从定宽类型开始
现代 C 接口如果使用 <stdint.h>,映射最直接:
| C 类型 | 位宽 | C# 类型 |
|---|---|---|
int8_t / uint8_t | 8 | sbyte / byte |
int16_t / uint16_t | 16 | short / ushort |
int32_t / uint32_t | 32 | int / uint |
int64_t / uint64_t | 64 | long / ulong |
float | 通常 32 | float |
double | 通常 64 | double |
对于设备寄存器、协议字段和文件格式,定宽整数同时表达了范围和二进制布局。若厂商公开接口仍使用 short、int、long,应查目标平台 ABI 和编译器文档,不能只按名称映射。
2. C/C++ long 不是 C# long
C# long 固定为 64 位。C/C++ long 至少 32 位,但常见 64 位 ABI 并不统一:64 位 Windows 仍是 32 位,而许多 64 位 Unix 系统是 64 位。
因此跨平台 C 接口应优先导出 int32_t、int64_t 等定宽类型。无法修改旧 SDK 时,Windows 的原生 long 通常映射为 C# int;目标是 Linux/macOS 时必须按该库实际构建 ABI 重新确认,不能共享一份未经验证的声明。.NET 6+ 为这种场景提供了 System.Runtime.InteropServices.CLong/CULong,按目标平台自动以正确宽度承载 C long/unsigned long。
3. 指针大小跟随进程架构
以下类型表达的是地址或与地址同宽的整数:
| 原生含义 | C# 表达 |
|---|---|
void*、不透明句柄 | nint / IntPtr,资源句柄优先用 SafeHandle |
const void* | nint 或 void*,同时在封装层保持只读语义 |
size_t | nuint / UIntPtr |
ptrdiff_t、intptr_t | nint |
uintptr_t | nuint |
nint 与 nuint 随进程位数变化。它们不是“任意大整数”,也不表示内存所有权;一个返回指针究竟是借用、转移还是句柄,仍要看 API 契约。
4. bool 必须先认清原生定义
常见布尔表示至少有三种:
- C99
_Bool或 C++bool通常占 1 字节; - Win32
BOOL是 4 字节有符号整数,零为假、非零为真; - COM
VARIANT_BOOL是 2 字节,VARIANT_TRUE为-1。
.NET 运行时封送启用时,C# bool 默认按 4 字节 Win32 BOOL 处理。它不能直接代表原生 C++ bool。一种显式声明是:
| |
对于新设计的 C ABI,更稳妥的做法是导出 uint8_t 或 int32_t,C# 也使用 byte 或 int,再在封装层转换为布尔值。这样签名不依赖默认封送规则:
| |
5. char 首先是整数,编码另行约定
在 .NET 支持的常见目标平台上,C 的 char 占 1 个 8 位字节,但它是有符号还是无符号由实现决定;C# char 则是 16 位 UTF-16 代码单元。以下映射更可靠:
- 原生单字节数值或原始字节:
byte/sbyte; - UTF-8 文本:
byte*、字节缓冲区,或显式 UTF-8 字符串封送; - Windows UTF-16
wchar_t*:显式 UTF-16 字符串封送; - Unix
wchar_t*:不能假设是 UTF-16,应按平台定义处理。
不要用 C# char 表达任意原生 char,也不要把“ANSI”理解成一种固定的跨平台编码。
6. 枚举的语义和底层宽度都要固定
C# 枚举默认以 int 为底层类型,但 C/C++ 枚举的 ABI 宽度可能受编译器、选项和枚举值范围影响。设备 SDK 若把枚举放入公开结构或函数参数,最好由 C 边界固定为 int32_t:
| |
| |
解析返回值时还要允许未知数字。原生库升级后可能新增状态,托管代码不应因为 Enum.IsDefined 为假就丢失原始值。
7. 句柄不是普通整数
厂商有时把句柄声明为 void*,有时是 uint32_t,还有时是结构体指针。三者不能凭名称互换:
- 指针型句柄用
nint或SafeHandle; - 明确定宽的数字 ID 用对应整数;
- Windows
HANDLE是指针大小,不应写成固定 32 位整数; - 只有文档明确指定的无效值才可作为失败判断,例如空指针或
-1。
后续文章会用 SafeHandle 把“何时关闭、由谁关闭、调用期间能否关闭”纳入类型系统。
8. 用大小断言把猜测变成测试
对于公开结构,最好让原生侧和托管侧各自输出或断言大小、对齐和偏移。纯托管的基础检查可以写成:
| |
这只能证明托管一侧,不能替代用同一套头文件和编译选项得到的原生 sizeof、alignof 与 offsetof。
总结
互操作类型映射的原则是按 ABI 含义和位宽选择类型,而不是按名称翻译。定宽整数、显式布尔表示、指针大小整数和清晰的句柄契约,可以消除大量“在一台机器上恰好能跑”的问题。
下一篇进入组合类型:结构体的字段顺序、填充、对齐、联合体和 blittable 边界。