C# 高级教程 - 19 互操作与源代码生成器

互操作(Interop)让 .NET 代码可以调用原生 C/C++ 库和 COM 组件,这在图像处理、硬件驱动、游戏引擎集成等场景中不可或缺。源代码生成器(Source Generator)则是 Roslyn 编译器的扩展能力,可以在编译时分析代码并生成额外的源文件。本文将深入探讨这两项技术。

P/Invoke 基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Runtime.InteropServices;

// 声明外部函数
internal static partial class NativeMethods
{
// .NET 5+ 可以使用 LibraryImport(源码生成,无运行时反射)
[LibraryImport("user32.dll", SetLastError = true)]
internal static partial int MessageBoxW(
IntPtr hWnd,
[MarshalAs(UnmanagedType.LPWStr)] string text,
[MarshalAs(UnmanagedType.LPWStr)] string caption,
uint type);
}

// 调用
NativeMethods.MessageBoxW(IntPtr.Zero, "Hello", "Native", 0);

DllImport vs LibraryImport

特性 DllImport LibraryImport(.NET 5+)
生成方式 运行时反射 编译时源码生成
性能 有 P/Invoke 开销 优化后开销更小
安全性 自动处理 需要 partial 方法
可用性 所有 .NET 版本 .NET 5+

数据封送(Marshalling)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 结构体封送
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct Point
{
public int X;
public int Y;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string Name;
}

// 复杂参数封送
internal static partial class NativeCalls
{
// 传递结构体
[LibraryImport("mylib.dll")]
internal static partial int ProcessPoint(ref Point point);

// 传递数组
[LibraryImport("mylib.dll")]
internal static partial void ProcessArray(
[MarshalAs(UnmanagedType.LPArray)] byte[] data,
int length);

// 传递回调函数(函数指针)
[LibraryImport("mylib.dll")]
internal static partial void SetCallback(
delegate* unmanaged[Cdecl]<int, void> callback);

// 字符串编码
[LibraryImport("mylib.dll")]
[return: MarshalAs(UnmanagedType.LPWStr)]
internal static partial string GetString();
}

// 函数指针调用(C# 9+)
unsafe
{
delegate* unmanaged[Cdecl]<int, int, int> add = &NativeAdd;
int result = add(3, 4);
}

static int NativeAdd(int a, int b) => a + b;

Span 在互操作中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 在 .NET 5+ 中,Span 可以直接用于 P/Invoke
internal static partial class NativeMemory
{
[LibraryImport("kernel32.dll", SetLastError = true)]
internal static partial void CopyMemory(
IntPtr destination, IntPtr source, nuint length);
}

// 通过 Span 与原生内存交互
unsafe
{
var managed = new byte[] { 1, 2, 3, 4, 5 };
var native = Marshal.AllocHGlobal(5);

try
{
// 托管 → 原生
fixed (byte* pManaged = managed)
{
NativeMemory.CopyMemory(native, (IntPtr)pManaged, (nuint)managed.Length);
}

// 原生 → Span(无需复制)
Span<byte> span = new Span<byte>(native.ToPointer(), 5);
Console.WriteLine(span[0]); // 1
}
finally
{
Marshal.FreeHGlobal(native);
}
}

COM 互操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过 dynamic 简化 COM 调用
Type excelType = Type.GetTypeFromProgID("Excel.Application")!;
dynamic excel = Activator.CreateInstance(excelType)!;

excel.Visible = true;
excel.Workbooks.Add();
excel.Cells[1, 1] = "Hello";
excel.Cells[1, 2] = "World";

// 释放 COM 资源
Marshal.ReleaseComObject(excel);

// 或使用嵌入互操作类型(Embed Interop Types = true)
// 在项目引用中设置 EmbedInteropTypes=True 避免 PIA

源代码生成器(Source Generator)

这是 C# 9 引入的编译时元编程能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 1. 创建一个源代码生成器
[Generator]
public class AutoNotifyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 注册语法接收器
var declarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => ctx.Node as ClassDeclarationSyntax)
.Where(c => c is not null);

context.RegisterSourceOutput(declarations, GenerateCode);
}

private void GenerateCode(
SourceProductionContext context,
ClassDeclarationSyntax classDecl)
{
var className = classDecl.Identifier.Text;
var namespaceName = "Generated";

var code = $$"""
namespace {{namespaceName}}
{
public partial class {{className}}
{
public string GeneratedProperty => "由 Source Generator 生成";
}
}
""";

context.AddSource($"{className}.g.cs", code);
}
}

// 2. 标记属性(自定义 Attribute)
[AttributeUsage(AttributeTargets.Field)]
public class AutoNotifyAttribute : Attribute { }

// 3. 使用生成器(项目中引用)
[AutoNotify]
private string _name; // Source Generator 自动生成 public string Name { get; set; }

常用 Source Generator 库

功能
System.Text.Json 编译时 JSON 序列化
MediatR 请求处理管道
Mapperly 编译时对象映射
GeneratedRegex(.NET 7+) 编译时正则表达
StronglyTypedId 强类型 ID
1
2
3
4
5
// GeneratedRegex 示例(.NET 7+)
[GeneratedRegex(@"\d{3}-\d{4}")]
private static partial Regex PhoneRegex();

bool isValid = PhoneRegex().IsMatch("123-4567"); // true

实战:编译时 JSON 序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// System.Text.Json 的 Source Generator
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(Order[]))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

// 使用(零反射、零 IL 生成)
var options = new JsonSerializerOptions
{
TypeInfoResolver = AppJsonContext.Default
};

var person = new Person { Name = "张三", Age = 28 };
string json = JsonSerializer.Serialize(person, AppJsonContext.Default.Person);
var deserialized = JsonSerializer.Deserialize(json, AppJsonContext.Default.Person);

性能对比

方案 序列化/调用性能 启动速度 二进制体积
反射序列化 较慢
Source Generator 序列化 接近手写 略大(多生成代码)
动态代码生成(Reflection.Emit) 非常快 较慢(JIT)
P/Invoke(DllImport) 较快 中等 依赖原生库
P/Invoke(LibraryImport) 更快 依赖原生库

安全注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 验证指针有效性
IntPtr ptr = GetNativeBuffer();
if (ptr == IntPtr.Zero)
throw new InvalidOperationException("指针为空");

// 2. 使用 SafeHandle 替代 IntPtr
public class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public MySafeHandle() : base(true) { }

protected override bool ReleaseHandle()
{
return NativeMethods.FreeResource(handle) == 0;
}
}

// 3. 调用 SetLastError 并检查
if (!NativeMethods.SomeApi())
{
int error = Marshal.GetLastPInvokeError();
// 处理错误
}

// 4. 避免在 finalizer 中调用可能阻塞的 native 方法

提升互操作性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 禁用异常封送
[LibraryImport("mylib.dll", SetLastError = false)]

// 2. 使用 blittable 类型(避免封送)
public struct BlittablePoint // 仅包含 int、float 等简单类型
{
public int X;
public int Y;
}

// 3. 固定托管对象代替拷贝(fixed 语句)
fixed (int* p = array)
{
NativeProcess(p, array.Length);
}

// 4. 使用函数指针代替委托封送(C# 9+)
[LibraryImport("mylib.dll")]
internal static partial void RegisterCallback(
delegate* unmanaged[Cdecl]<int, void> callback);

总结:互操作让 .NET 能够充分利用已有的原生生态,源代码生成器则将运行时代价转移至编译时。在实际项目中,应优先选择 LibraryImport 而非 DllImport、SafeHandle 而非 IntPtr、Source Generator 而非反射。理解数据封送规则、blittable 类型和函数指针的用法,可使互操作代码既安全又高效。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: