C# 使用 DllImport 调用非托管代码指南
在 C# 开发中,经常需要调用 Windows API 或其他由 C/C++ 编写的非托管库。这需要通过 System.Runtime.InteropServices 命名空间中的 DllImport 特性来实现。本文将详细解析数据类型映射、常用属性配置以及正确的签名声明方法。
核心步骤:声明与调用 P/Invoke 接口
要在 C# 中调用非托管函数,首先必须引入 System.Runtime.InteropServices 命名空间。接着,需定义一个静态外部方法,并使用 [DllImport] 特性修饰。该方法必须是 public static extern。
第1步:确定目标函数的类型映射
C#(.NET Framework / .NET Core)是非托管内存管理的托管环境,因此在调用之前需要明确非托管类型到托管类型的对应关系。错误的类型映射会导致运行时异常或数据损坏。
- 返回值: 例如 WinAPI 的返回值
DWORD通常映射为 C# 的int。 - 输入字符串 (In String): 只读字符指针(如
LPCTSTR),通常映射为string。 - 输出/缓冲字符串 (Out Buffer): 需要被修改的字符数组缓冲区(如
LPTSTR),通常映射为StringBuilder。注意:StringBuilder必须在实例化时分配足够的容量(Capacity)。 - 其他数值类型: 根据具体位数选择
uint,ushort,long等。
第2步:编写带 DllImport 特性的方法签名
以获取短路径名的 Windows API `GetShortPathName` 为例。该函数需要接收长路径并返回短路径。
关键点说明:
- DLL 来源: 如果不指定完整路径(如直接写 "kernel32.dll"),系统会按以下顺序查找 DLL:
① 当前运行程序(.exe/.dll)所在目录;
②C:\Windows\System32目录;
③ 环境变量PATH中列出的目录。 - Entry Point 别名: 如果实际函数名与 C# 内部定义的名称不同,需在特性中使用
EntryPoint指定原名。 - 编组特性 MarshalAs: 用于处理复杂的内存布局。对于
LPTStr这种可变长度字符串,虽然string和StringBuilder有默认行为,但在使用指针或特殊结构时通常需要显式标注[MarshalAs(UnmanagedType.LPTStr)]。
using System;
using System.Text;
using System.Runtime.InteropServices;
public class TestPInvoke
{
// Kernel32.dll 位于系统目录,可省略绝对路径
[DllImport("kernel32.dll", CharSet = CharSet.Auto, EntryPoint = "GetShortPathName")]
public static extern int GetShortPathName(
string lpszLongPath, // 输入长路径
StringBuilder lpszShortPath, // 输出缓冲,需提前初始化大小
int cchBuffer // 缓冲区的总大小
);
public static void Main()
{
// 1. 准备长路径
string longPath = @"C:\Users\User\AppData\Local\Microsoft\OneDrive";
// 2. 创建 StringBuilder,容量必须大于等于长路径长度
int capacity = longPath.Length + 10;
StringBuilder sbShort = new StringBuilder(capacity);
// 3. 调用 API
int result = GetShortPathName(longPath, sbShort, sbShort.Capacity);
if (result > 0)
{
Console.WriteLine($"Short Path: {sbShort.ToString()}");
}
else
{
Console.WriteLine("Failed to get short path.");
}
}
}
DllImport 可选属性详解
掌握以下可选属性可以让调用的调用更加精确和安全:
| 属性 | 作用说明 | 常见值 |
|---|---|---|
EntryPoint |
指定 DLL 中实际的导出函数名。若 C# 方法名与 DLL 函数名一致,可忽略此属性。 | 字符串,如 "GetProcAddress" |
CharSet |
决定传递字符串时使用的编码方式(Ansi 还是 Unicode)。 | Auto (推荐), Ansi, Unicode |
ExactSpelling |
默认为 false。如果设为 true,表示函数入口点地址不随 CharSet 变化而自动添加后缀(如 'A' 或 'W')。 |
布尔值 true / false |
CallingConvention |
指定函数调用的堆栈清理约定。大多数 Windows API 使用标准约定。 | WinApi (默认自动匹配平台), Cdecl |
PreserveSig |
指示方法的签名是否保持原样。通常不需要修改,除非涉及特殊的 HRESULT 转换逻辑。 | 布尔值 true / false |
SetLastError |
标记该 API 是否在失败时设置 LastError。设为 true 后,可在代码中通过 Marshal.GetLastWin32Error() 获取错误码。 |
布尔值 false / true |
常见问题排查
找不到 DLL 怎么办?
如果发生 DllNotFoundException,请检查以下几点:
- 确认指定的 DLL 确实存在于上述的三个搜索路径中。
- 如果是第三方自定义 DLL,建议提供包含 DLL 完整绝对路径的路径字符串给
DllImport。 - 架构问题:确保 C# 项目的目标平台(x86 或 x64)与被加载的非托管 DLL 架构一致。混合加载(如 x64 进程加载 x86 DLL)会引发
TypeLoadException。
内存访问违规(Access Violation)
这通常是由于非托管代码尝试写入未分配的内存或越界导致的。在使用 StringBuilder 接收返回值时,务必保证初始 Capacity 足够大;在处理结构体指针时,使用 GCHandle 或 StackAlloc 确保内存有效性。
以上就是使用 DllImport 调用非托管代码的详细内容,更多关于 C# 与底层系统交互的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













