0Pricing
C# Academy · 课时

LibraryImport 与源代码生成的 P/Invoke

使用 [LibraryImport](C# 11+)实现兼容 AOT 的源代码生成封送处理,其性能优于 DllImport。

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

为什么使用 LibraryImport

[LibraryImport] 在 .NET 7(C# 11)中引入,作为由源代码生成且兼容 AOT 的 [DllImport] 替代方案。传统的 DllImport 依赖通过反射在运行时进行封送,这对 Native AOT 来说存在问题。LibraryImport 会在编译时生成全部封送代码。

声明 LibraryImport 方法

请使用 [LibraryImport] 标记一个 static partial 方法。源代码生成器会补全其实现。同时,包含该方法的类也必须标记为 partial。

using System.Runtime.InteropServices;

internal static partial class NativeMethods
{
    // Source generator creates the P/Invoke body at compile time
    [LibraryImport("mylib", EntryPoint = "add_integers")]
    internal static partial int AddIntegers(int a, int b);

    // String marshaling must be explicit in LibraryImport
    [LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
    internal static partial int ProcessString(string text);
}

LibraryImport 中的字符串封送

与 DllImport 不同,LibraryImport 要求 you 显式指定字符串封送方式。您可以使用 StringMarshalling 枚举或 MarshalAs 属性,使封送成本清晰可见并且便于控制。

// Option 1: StringMarshalling enum (all strings in method)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16)]
static partial bool CreateDirectory(string lpPathName, nint lpSecurityAttributes);

// Option 2: Per-parameter MarshalAs
[LibraryImport("libc")]
static partial int Open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string path,
    int flags);

// Option 3: MarshalUsing for custom marshalers
[LibraryImport("mylib")]
static partial void Process(
    [MarshalUsing(typeof(Utf8StringMarshaller))] string name);

结构体封送

对于结构体,请添加 [NativeMarshalling],以定义托管类型如何映射到其原生表示形式。源代码生成器会使用封送器类型生成安全且尽量减少分配的代码。

[NativeMarshalling(typeof(PointMarshaller))]
public struct Point
{
    public int X;
    public int Y;
}

[CustomMarshaller(typeof(Point), MarshalMode.Default, typeof(PointMarshaller))]
public static class PointMarshaller
{
    public static Point ConvertToManaged(NativePoint native)
        => new Point { X = native.X, Y = native.Y };
    public static NativePoint ConvertToUnmanaged(Point managed)
        => new NativePoint { X = managed.X, Y = managed.Y };

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

比较 DllImport 与 LibraryImport

DllImport 在运行时解释执行,启动速度较慢、依赖反射,并且与 AOT 裁剪不兼容。LibraryImport 会在构建时生成经过优化的 C# 代码:运行时无需反射、可安全裁剪,并且在基准测试中速度明显更快。

// Old — DllImport (still works, but avoid for new AOT code)
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool MoveFile(string src, string dst);

// New — LibraryImport (AOT-safe, source-generated)
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool MoveFile(string src, string dst);
// The compiler generates the actual P/Invoke wrapper body

SetLastError 与错误处理

在 [LibraryImport] 中设置 SetLastError = true,以捕获操作系统错误代码。调用结束后,请使用 Marshal.GetLastPInvokeError()(首选)或 Marshal.GetLastWin32Error() 获取该代码。

[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
                SetLastError = true)]
static partial bool CreateDirectory(string path, nint secAttr);

bool ok = CreateDirectory(@"C:\Temp\NewDir", 0);
if (!ok)
{
    int err = Marshal.GetLastPInvokeError();
    // err is ERROR_ALREADY_EXISTS (183) if folder exists
    throw new Win32Exception(err);
}

Span<T> 与内存封送

源代码生成的 P/Invoke 的优势之一,是对 Span<T> 提供一等支持。传递 ReadOnlySpan<byte> 可以避免使用数组的 DllImport 所需的固定和内存分配。

[LibraryImport("mylib")]
static partial int ProcessBuffer(
    ReadOnlySpan<byte> data,
    int length);

// Usage — no fixed or GCHandle needed
byte[] buffer = Encoding.UTF8.GetBytes("hello");
int result = ProcessBuffer(buffer, buffer.Length);

// For output buffers use Span<byte>
[LibraryImport("mylib")]
static partial int FillBuffer(Span<byte> output, int maxLen);

启用源代码生成

当项目面向 .NET 7 或更高版本,并引用 System.Runtime.InteropServices 命名空间时,源代码生成会自动启用。对于较旧的目标框架,您需要使用 Microsoft.Interop.SourceGeneration 分析器包。

<!-- In your .csproj — no extra package needed on .NET 7+ -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <!-- Enable Roslyn analyzers and source generators -->
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <!-- AllowUnsafeBlocks may be needed for some marshalers -->
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

检查生成的代码

将 <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 添加到 csproj,使生成的文件写入 obj/。这样您就能准确查看源代码生成器生成的内容,这是很好的学习练习。

// The generator emits something like this for a LibraryImport method:
// (simplified view of generated stub)

internal static partial int AddIntegers(int a, int b)
{
    // Direct call — no reflection, no boxing, fully inlineable
    return __PInvoke(a, b);

    [System.Runtime.InteropServices.DllImportAttribute(
        "mylib",
        EntryPoint = "add_integers",
        ExactSpelling = true)]
    static extern int __PInvoke(int a, int b);
}

实际应用:包装 C 库

一种常见模式是定义一个静态包装类,集中放置所有 LibraryImport 声明,然后在其上提供高级安全 API。请将 partial 声明保持为内部或私有成员,只向外部公开安全 wrappers。

internal static partial class LibSodiumNative
{
    private const string LibName = "libsodium";

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_keybytes")]
    internal static partial nuint KeyBytes();

    [LibraryImport(LibName, EntryPoint = "crypto_secretbox_easy")]
    internal static partial int SecretBoxEasy(
        Span<byte> ciphertext,
        ReadOnlySpan<byte> message,
        ulong mlen,
        ReadOnlySpan<byte> nonce,
        ReadOnlySpan<byte> key);
}

// Public safe wrapper hides the native signature
public static byte[] Encrypt(byte[] message, byte[] key, byte[] nonce)
{
    var cipher = new byte[message.Length + 16];
    LibSodiumNative.SecretBoxEasy(cipher, message, (ulong)message.Length, nonce, key);
    return cipher;
}

快速检查

与 [DllImport] 相比,[LibraryImport] 的主要优势是什么?

回顾:LibraryImport 与源代码生成的 P/Invoke

要点:

  • [LibraryImport] 替代 [DllImport],用于支持 AOT 的 P/Invoke
  • 封送在编译时生成,无需运行时反射
  • 需要 static partial 方法和 partial 类
  • 必须通过 StringMarshalling 或 MarshalAs 显式指定字符串封送方式
  • 提供一等 Span<T> 支持,无需承担固定带来的开销
  • 使用 EmitCompilerGeneratedFiles 检查生成的代码

常见问题解答

「LibraryImport 与源代码生成的 P/Invoke」课时是免费的吗?

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

「LibraryImport 与源代码生成的 P/Invoke」这节课中我会学到什么?

使用 [LibraryImport](C# 11+)实现兼容 AOT 的源代码生成封送处理,其性能优于 DllImport。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

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

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

「LibraryImport 与源代码生成的 P/Invoke」课时需要多长时间?

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

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

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

此课程中的所有课时

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