C# 调用 Win32 API

一、核心概念解析

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.dlluser32.dlladvapi32.dll 等);
  • 数据类型映射 :C/C++ 的原生类型(如 DWORDHANDLELPCSTR)需要映射到 C# 对应的类型(如 uintIntPtrstring)。

二、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.dlluser32.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 时,hWndIntPtr.Zero 即可。

案例 3:获取进程 ID(调用 GetCurrentProcessId)

需求 :调用 kernel32.dllGetCurrentProcessId 获取当前控制台程序的进程 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.dllWritePrivateProfileStringGetPrivateProfileString 读写 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(内存访问错误)等异常。
相关推荐
黑头人2 小时前
Error: JAVA_HOME is not set and Java could not be found
java·开发语言
唐装鼠2 小时前
Rust 中的 `parse` 方法详解(deepseek)
开发语言·后端·rust
双河子思2 小时前
C# 语言编程经验
开发语言·c#
FuckPatience2 小时前
C# 把halcon中的任意图形HXLD在WPF中绘制出来
开发语言·c#
唐装鼠2 小时前
Rust 自动引用规则完全指南(deepseek)
开发语言·后端·rust
qq_336313932 小时前
java基础-异常
java·开发语言
千里马-horse2 小时前
Napi::Array
开发语言·array·napi
lly2024062 小时前
Julia 的复数和有理数
开发语言
春日见2 小时前
如何提升手眼标定精度?
linux·运维·开发语言·数码相机·matlab