0Pricing
C# Academy · 课时

P/Invoke 基础

使用 DllImport 声明和调用原生函数,了解基元类型、字符串和结构体的封送处理。

P/Invoke 基础 是 CoddyKit 上的免费 C# Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

什么是 P/Invoke

平台调用服务(P/Invoke)允许 C# 调用本机共享库(Windows 上的 .dll、Linux 上的 .so、macOS 上的 .dylib)。这是从 .NET 调用 Win32 API 或任何 C ABI 库的标准机制。

您的第一个 P/Invoke 调用

使用 [DllImport] 声明本机函数,并提供库名称。CLR 负责查找和加载库,以及封送参数。

using System.Runtime.InteropServices;

// Calling MessageBoxW from user32.dll (Windows)
internal static partial class NativeMethods
{
    [DllImport("user32.dll",
        EntryPoint = "MessageBoxW",
        CharSet = CharSet.Unicode,
        SetLastError = true)]
    internal static extern int MessageBox(
        IntPtr hwnd,
        string text,
        string caption,
        uint type);
}

// Call it:
NativeMethods.MessageBox(IntPtr.Zero, "Hello!", "P/Invoke", 0);

// Windows-only — wrap in RuntimeInformation.IsOSPlatform check
// for cross-platform code

调用自定义本机库

P/Invoke 适用于任何由 C 导出的函数,而不仅仅是操作系统 API。构建本机库,使用 C 链接导出函数,然后从 C# 调用它们。

// mymath.h / mymath.c:
// extern "C" double AddNumbers(double a, double b) { return a + b; }
// Compile: gcc -shared -o libmymath.so mymath.c

// C# declaration:
[DllImport("mymath",              // libmymath.so / mymath.dll
    EntryPoint = "AddNumbers",
    CallingConvention = CallingConvention.Cdecl)]
private static extern double AddNumbers(double a, double b);

// Call:
double result = AddNumbers(3.14, 2.72); // 5.86

// .NET resolves the library via:
// 1. Absolute path if provided
// 2. App directory
// 3. OS library path (PATH / LD_LIBRARY_PATH / DYLD_LIBRARY_PATH)

封送基本类型

CLR 会自动在托管表示形式和本机表示形式之间封送大多数基元类型。了解这些映射可以避免难以察觉的错误。

// C Type       → C# Type
// int (32-bit)  → int  or  System.Int32
// long (64-bit) → long or  System.Int64
// float         → float
// double        → double
// bool          → [MarshalAs(UnmanagedType.Bool)] bool
// char*  (ANSI) → string  (CharSet.Ansi)
// wchar_t*      → string  (CharSet.Unicode)
// void*         → IntPtr
// size_t        → UIntPtr

// Example with explicit marshalling:
[DllImport("libc", EntryPoint = "strlen", CharSet = CharSet.Ansi)]
private static extern UIntPtr StrLen(
    [MarshalAs(UnmanagedType.LPStr)] string s);

int len = (int)StrLen("hello"); // 5

将结构体传递给本机代码

使用 [StructLayout(LayoutKind.Sequential)],确保结构体在内存中的布局与本机代码的要求完全一致。

// C struct:
// struct Point { int x; int y; };
// void DrawPoint(struct Point p);

[StructLayout(LayoutKind.Sequential)]
public struct Point
{
    public int X;
    public int Y;
}

[DllImport("graphics", EntryPoint = "DrawPoint",
    CallingConvention = CallingConvention.Cdecl)]
private static extern void DrawPoint(Point p);

// Pass by value:
DrawPoint(new Point { X = 10, Y = 20 });

// Pass by pointer (ref or out):
[DllImport("graphics", EntryPoint = "GetCenter",
    CallingConvention = CallingConvention.Cdecl)]
private static extern void GetCenter(out Point center);
GetCenter(out var pt);

错误处理:GetLastWin32Error

Windows API 函数通过 GetLastError() 发出错误信号。在 [DllImport] 中将 SetLastError = true,并在调用后调用 Marshal.GetLastWin32Error()。

[DllImport("kernel32.dll",
    EntryPoint = "CreateFileW",
    CharSet = CharSet.Unicode,
    SetLastError = true)]
private static extern IntPtr CreateFile(
    string fileName,
    uint desiredAccess,
    uint shareMode,
    IntPtr securityAttributes,
    uint creationDisposition,
    uint flagsAndAttributes,
    IntPtr templateFile);

const uint GENERIC_READ = 0x80000000;
const uint OPEN_EXISTING  = 3;

var handle = CreateFile("test.txt", GENERIC_READ, 0,
    IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

if (handle == (IntPtr)(-1))
{
    int error = Marshal.GetLastWin32Error();
    throw new System.ComponentModel.Win32Exception(error);
}

封送字符串与缓冲区

字符串封送需要特别注意编码和所有权。使用 StringBuilder 作为输出缓冲区,并使用 MarshalAs 特性指定编码。

// Read into a buffer:
[DllImport("kernel32.dll",
    EntryPoint = "GetComputerNameW",
    CharSet = CharSet.Unicode,
    SetLastError = true)]
private static extern bool GetComputerName(
    System.Text.StringBuilder lpBuffer,
    ref uint nSize);

uint size = 256;
var buffer = new System.Text.StringBuilder((int)size);
if (GetComputerName(buffer, ref size))
    Console.WriteLine(buffer.ToString());

// Return a string owned by native code (don't free it):
[DllImport("mylib", CharSet = CharSet.Ansi)]
[return: MarshalAs(UnmanagedType.LPStr)]
private static extern string GetVersion();

函数指针与回调

将托管委托作为 C 函数指针传递。CLR 会创建一个转换桩,但您必须使委托保持存活(持有一个引用),否则 GC 会将其回收,导致本机代码崩溃。

// C callback signature: typedef int (*Comparer)(const void*, const void*);
// void qsort(void* base, size_t nitems, size_t size, Comparer compar);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int CompareCallback(IntPtr a, IntPtr b);

[DllImport("libc", CallingConvention = CallingConvention.Cdecl)]
private static extern void QSort(
    int[] data, UIntPtr count, UIntPtr size, CompareCallback compare);

// Keep the delegate ALIVE for the duration of the call:
private static readonly CompareCallback _compare =
    (a, b) => Marshal.ReadInt32(a).CompareTo(Marshal.ReadInt32(b));

var data = new[] { 5, 2, 8, 1, 3 };
QSort(data, (UIntPtr)data.Length, (UIntPtr)sizeof(int), _compare);

NativeLibrary API

NativeLibrary 类提供显式的库加载、函数指针解析和跨平台路径自定义,是替代隐式库名解析的现代方案。

using System.Runtime.InteropServices;

// Explicit load:
var handle = NativeLibrary.Load("/usr/lib/libssl.so.3");

// Resolve a function pointer:
var addPtr = NativeLibrary.GetExport(handle, "AddNumbers");
var addFn = Marshal.GetDelegateForFunctionPointer<Func<double,double,double>>(addPtr);
double result = addFn(1.0, 2.0);

// Unload:
NativeLibrary.Free(handle);

// Custom resolver (called when DllImport can't find a library):
NativeLibrary.SetDllImportResolver(typeof(MyNative).Assembly,
    (libName, assembly, searchPath) =>
    {
        if (libName == "mymath")
            return NativeLibrary.Load("/opt/mymath/libmymath.so");
        return IntPtr.Zero;
    });

实战:调用 OpenSSL

一个真实示例:使用 P/Invoke 从 C# 调用 OpenSSL 的 SHA-256 摘要函数。

internal static class OpenSslInterop
{
    [DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr EVP_MD_CTX_new();

    [DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
    private static extern void EVP_MD_CTX_free(IntPtr ctx);

    [DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr EVP_sha256();

    [DllImport("libssl", CallingConvention = CallingConvention.Cdecl)]
    private static extern int EVP_DigestInit_ex(IntPtr ctx, IntPtr type, IntPtr engine);

    // In practice, use System.Security.Cryptography.SHA256 instead:
    // var hash = SHA256.HashData(data);
    // P/Invoke to OpenSSL is only needed when you require
    // non-managed cryptographic operations or specific OpenSSL features
}

快速检查

将委托作为本机回调传递时,为什么必须使其保持存活?

回顾:P/Invoke 基础

要点:

  • P/Invoke:通过 [DllImport] 声明调用原生 C-ABI 函数
  • CLR 会自动封送基本类型;对于字符串和自定义类型,请使用 [MarshalAs] 添加注解
  • [StructLayout(LayoutKind.Sequential)]:确保结构体的内存布局符合原生代码的预期
  • 设置 SetLastError = true,并调用 Marshal.GetLastWin32Error() 来处理 Win32 错误
  • 将委托作为原生回调传递时,请保持委托引用处于有效状态
  • NativeLibrary:显式加载、解析,并使用自定义解析器控制跨平台路径

常见问题解答

「P/Invoke 基础」课时是免费的吗?

是的 — 「P/Invoke 基础」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「P/Invoke 基础」这节课中我会学到什么?

使用 DllImport 声明和调用原生函数,了解基元类型、字符串和结构体的封送处理。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「P/Invoke 基础」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. P/Invoke 基础
  2. LibraryImport 与源代码生成的 P/Invoke
  3. 不安全代码、指针与固定缓冲区
  4. COM 互操作与运行时可调用包装器
← 返回 C# Academy