一、核心概念解析
1. 什么是 Win32 API?
Win32 API(Windows 32-bit Application Programming Interface)是微软为 Windows 操作系统提供的底层编程接口,包含了操作系统的核心功能(如窗口管理、文件操作、进程控制、内存管理、系统信息获取等),本质上是一组用 C/C++ 编写的原生函数。
2. C# 为什么能调用 Win32 API?
C# 运行在 .NET 运行时(CLR)中,属于托管代码;而 Win32 API 是非托管代码(直接运行在操作系统层面)。.NET 提供了 P/Invoke(Platform Invocation Services,平台调用服务) 机制,这是 CLR 提供的核心功能,允许托管代码调用非托管的函数(如 Win32 API)。
3. P/Invoke 核心要素
要在 C# 中调用 Win32 API,必须满足以下条件:
- 函数签名匹配:C# 中声明的函数必须和 Win32 API 的原生签名(返回值、参数类型、调用约定)一致;
- DLL 导入 :指定 Win32 API 所在的系统 DLL(如
kernel32.dll、user32.dll、advapi32.dll等); - 数据类型映射 :C/C++ 的原生类型(如
DWORD、HANDLE、LPCSTR)需要映射到 C# 对应的类型(如uint、IntPtr、string)。
二、Win32 API 调用的语法规则
1. 基础声明格式
在 C# 中,通过 DllImport 特性(位于 System.Runtime.InteropServices 命名空间)声明 Win32 API 函数,核心语法如下:
cs
using System.Runtime.InteropServices; // 必须引入此命名空间
class Win32Api
{
// DllImport 特性指定 API 所在的 DLL
[DllImport("DLL名称",
CharSet = CharSet.ANSI/Unicode, // 字符集(匹配 API 要求)
SetLastError = true/false, // 是否捕获系统错误码
CallingConvention = CallingConvention.StdCall)] // 调用约定(Win32 几乎都是 StdCall)
// 方法声明:必须是 static extern,返回值+方法名+参数列表(类型要匹配)
public static extern 返回值类型 方法名(参数类型1 参数1, 参数类型2 参数2, ...);
}
2. 关键参数说明
|-------------------|------------------------------------------------------------------------|
| 特性参数 | 作用 |
| DllName | Win32 API 所在的系统 DLL 名称(如 kernel32.dll、user32.dll) |
| CharSet | 字符编码:CharSet.Ansi(ANSI)、CharSet.Unicode(UTF-16)、CharSet.Auto(自动) |
| SetLastError | 设为 true 时,可通过 Marshal.GetLastWin32Error() 获取系统错误码 |
| CallingConvention | 调用约定:Win32 API 默认为 StdCall(C# 默认为 Winapi,等价于 StdCall) |
3. 常见类型映射(Win32 → C#)
Win32 API 的原生类型和 C# 类型必须严格映射,否则会导致调用失败甚至程序崩溃:
|-------------------------------------------|--------------|-------------------------------------------|
| | Win32 类型 | | |----------|---| | C# 等效类型 | 说明 |
| DWORD | uint | 32 位无符号整数 |
| HANDLE | IntPtr | 句柄(指针类型,用 IntPtr 兼容 32/64 位) |
| LPCSTR | string | ANSI 字符串(常量指针) |
| LPWSTR | string | Unicode 字符串(可变指针) |
| BOOL | bool/int | Win32 的 BOOL 是 int(0 / 非 0),C# 可用 bool 兼容 |
| |--------------|---| | int/long | | | int/long | 直接映射 |
| VOID | void | 无返回值 |
三、控制台实战案例(多个场景)
环境准备
- 开发工具:Visual Studio(任意版本)或 VS Code + .NET SDK
- 创建项目:控制台应用(.NET Framework/.NET Core/.NET 5+ 均可,示例用 .NET 8)
- 核心命名空间:
System.Runtime.InteropServices(必须)
案例 1:获取系统目录(简单无参数 / 返回值)
需求 :调用 kernel32.dll 中的 GetSystemDirectory 函数,获取 Windows 系统目录(如 C:\Windows\System32)。
步骤 1:查看 Win32 API 原生签名
cs
// Win32 原生声明(C/C++)
UINT GetSystemDirectoryA(
LPSTR lpBuffer, // 接收目录的缓冲区
UINT uSize // 缓冲区大小
);
- 返回值:实际复制到缓冲区的字符数(不含终止符);
- 字符集:
A后缀表示 ANSI,W后缀表示 Unicode(推荐用 Unicode)。
步骤 2:C# 声明并调用
cs
using System;
using System.Runtime.InteropServices; // 核心命名空间
namespace Win32ApiDemo
{
class Program
{
// 1. 声明 Win32 API(使用 Unicode 版本 GetSystemDirectoryW)
[DllImport("kernel32.dll", // API 所在 DLL
CharSet = CharSet.Unicode, // 匹配 W 后缀的 Unicode 版本
SetLastError = true)] // 启用错误码捕获
// static extern 是固定写法,返回值 uint 对应 Win32 的 UINT
private static extern uint GetSystemDirectoryW(
char[] lpBuffer, // 字符数组作为缓冲区(替代 C 的 char*)
uint uSize // 缓冲区大小
);
static void Main(string[] args)
{
try
{
// 2. 准备缓冲区(系统目录最长不超过 260 字符,预留冗余)
char[] buffer = new char[256];
// 3. 调用 Win32 API
uint result = GetSystemDirectoryW(buffer, (uint)buffer.Length);
// 4. 处理结果
if (result == 0)
{
// 调用失败,获取错误码
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine($"调用失败,错误码:{errorCode}");
}
else
{
// 将字符数组转为字符串(去掉空字符)
string systemDir = new string(buffer).TrimEnd('\0');
Console.WriteLine($"Windows 系统目录:{systemDir}");
}
}
catch (Exception ex)
{
Console.WriteLine($"异常:{ex.Message}");
}
Console.ReadKey();
}
}
}
运行结果
cs
Windows 系统目录:C:\Windows\System32
案例 2:弹出系统消息框(调用 user32.dll)
需求 :调用 user32.dll 中的 MessageBox 函数,弹出 Windows 原生消息框(控制台程序也能调用 GUI 相关 API)。
cs
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 1. 声明 MessageBoxW(Unicode 版本)
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBoxW(
IntPtr hWnd, // 父窗口句柄(控制台无窗口,传 IntPtr.Zero)
string lpText, // 消息内容
string lpCaption, // 标题
uint uType // 消息框类型(按钮+图标)
);
// 定义消息框类型常量(对应 Win32 的宏)
private const uint MB_OK = 0x00000000; // 仅 OK 按钮
private const uint MB_ICONINFORMATION = 0x00000040; // 信息图标
private const uint MB_OKCANCEL = 0x00000001; // OK + 取消按钮
static void Main(string[] args)
{
try
{
// 2. 调用 MessageBoxW
int ret = MessageBoxW(
IntPtr.Zero, // 无父窗口
"这是 C# 调用 Win32 API 弹出的消息框!", // 消息内容
"Win32 API 演示", // 标题
MB_OK | MB_ICONINFORMATION // 组合类型:OK 按钮 + 信息图标
);
// 3. 处理返回值(用户点击的按钮)
switch (ret)
{
case 1:
Console.WriteLine("用户点击了【确定】按钮");
break;
case 2:
Console.WriteLine("用户点击了【取消】按钮");
break;
default:
Console.WriteLine($"返回值:{ret}(调用失败)");
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"异常:{ex.Message}");
}
Console.ReadKey();
}
}
}
关键说明
MessageBoxW的返回值:1 = 确定,2 = 取消,3 = 终止,4 = 重试,5 = 忽略等;- 消息框类型可以通过
|组合(如MB_OK | MB_ICONWARNING表示 OK 按钮 + 警告图标); - 控制台程序调用 GUI API 时,
hWnd传IntPtr.Zero即可。
案例 3:获取进程 ID(调用 GetCurrentProcessId)
需求 :调用 kernel32.dll 的 GetCurrentProcessId 获取当前控制台程序的进程 ID。
cs
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 声明 GetCurrentProcessId(无参数,返回 DWORD)
[DllImport("kernel32.dll", SetLastError = false)] // 此函数不会失败,无需捕获错误码
private static extern uint GetCurrentProcessId();
static void Main(string[] args)
{
// 调用 API
uint pid = GetCurrentProcessId();
Console.WriteLine($"当前控制台程序的进程 ID:{pid}");
// 验证:可以在任务管理器中查看控制台程序的 PID 是否一致
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
}
运行结果
cs
当前控制台程序的进程 ID:12345
按任意键退出...
案例 4:读写 INI 文件
需求 :调用 kernel32.dll 的 WritePrivateProfileString 和 GetPrivateProfileString 读写 INI 配置文件(Win32 原生 INI 操作)。
cs
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 1. 声明写 INI 的 API
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WritePrivateProfileStringW(
string lpAppName, // 节名(INI 的 [Section])
string lpKeyName, // 键名
string lpString, // 键值
string lpFileName // INI 文件路径
);
// 2. 声明读 INI 的 API
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetPrivateProfileStringW(
string lpAppName, // 节名
string lpKeyName, // 键名
string lpDefault, // 默认值(读取失败时返回)
char[] lpReturnedString, // 接收值的缓冲区
uint nSize, // 缓冲区大小
string lpFileName // INI 文件路径
);
static void Main(string[] args)
{
string iniPath = $"{Environment.CurrentDirectory}\\demo.ini";
try
{
// 步骤 1:写入 INI 文件
bool writeSuccess = WritePrivateProfileStringW(
"UserInfo", // 节名
"UserName", // 键名
"张三", // 键值
iniPath // 文件路径
);
if (writeSuccess)
{
Console.WriteLine($"成功写入 INI 文件:{iniPath}");
}
else
{
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine($"写入失败,错误码:{errorCode}");
}
// 步骤 2:读取 INI 文件
char[] buffer = new char[1024];
uint readLen = GetPrivateProfileStringW(
"UserInfo", // 节名
"UserName", // 键名
"默认值", // 默认值
buffer, // 缓冲区
(uint)buffer.Length, // 缓冲区大小
iniPath // 文件路径
);
if (readLen > 0)
{
string value = new string(buffer).TrimEnd('\0');
Console.WriteLine($"读取到的值:{value}");
}
else
{
Console.WriteLine("读取失败或键不存在");
}
}
catch (Exception ex)
{
Console.WriteLine($"异常:{ex.Message}");
}
Console.ReadKey();
}
}
}
运行结果
cs
成功写入 INI 文件:D:\Win32ApiDemo\bin\Debug\net8.0\demo.ini
读取到的值:张三
生成的 INI 文件内容
cs
[UserInfo]
UserName=张三
四、常见问题与避坑指南
1. 调用失败的常见原因
- 类型不匹配 :如 Win32 的
DWORD用了 C# 的int(虽然有时能运行,但 64 位系统会出问题); - 字符集错误 :调用
A后缀的 API 却用CharSet.Unicode,或反之; - 调用约定错误 :Win32 API 几乎都是
StdCall,若设为Cdecl会导致栈溢出; - 缓冲区大小不足:如获取系统目录时缓冲区太小,返回值为 0;
- 权限问题:部分 Win32 API 需要管理员权限(如修改系统设置),需右键以管理员运行程序。
2. 如何调试 Win32 API 调用?
- 启用
SetLastError = true,调用后通过Marshal.GetLastWin32Error()获取错误码,对照 Windows 错误码表 排查; - 检查 API 名称是否正确(如是否漏写
W/A后缀); - 用
IntPtr替代所有句柄类型(避免 32/64 位兼容性问题); - 在 try-catch 中捕获
EntryPointNotFoundException(API 名称错误)、AccessViolationException(内存访问错误)等异常。