ICustomMarshaler 是用于自定义托管与非托管数据转换的接口,当默认封送机制无法处理非标准结构体、C++ 类指针等特殊类型时必须使用;需实现 MarshalManagedToNative 和 MarshalNativeToManaged 等方法,并严格管理内存生命周期。

ICustomMarshaler 是什么,什么时候必须用它
当 C# 调用非托管代码(比如 C/C++ DLL)时,.NET 默认的封送(marshalling)机制无法处理某些特殊类型——比如非标准结构体布局、C++ 类指针、带长度前缀的字符串、自定义内存分配策略的缓冲区等。这时 ICustomMarshaler 就是唯一可控出口:它让你完全接管“托管 → 非托管”和“非托管 → 托管”两端的数据转换逻辑。
注意:它不适用于 P/Invoke 中简单类型(int、string、普通 struct),也不替代 [MarshalAs] 的常规配置;只有当你发现调用后出现访问冲突、数据错位、内存泄漏或 AccessViolationException 且排除了 calling convention 和字段对齐问题后,才该考虑它。
怎么写一个可用的 ICustomMarshaler 实现
必须实现两个核心方法:MarshalManagedToNative 和 MarshalNativeToManaged,并通常搭配 CleanUpNativeData 和 CleanUpManagedData 确保资源释放。常见错误是忽略返回值生命周期管理——比如在 MarshalManagedToNative 中分配了非托管内存,却没在 CleanUpNativeData 中 Marshal.FreeHGlobal 或调用对应 C 函数释放。
示例场景:封送一个 C 接口要求的 char** argv(命令行参数数组):
public class ArgvMarshaler : ICustomMarshaler
{
public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null) return IntPtr.Zero;
var strings = (string[])managedObj;
var ptrArray = Marshal.AllocHGlobal(strings.Length * IntPtr.Size);
for (int i = 0; i public object MarshalNativeToManaged(IntPtr pNativeData)
{
// 通常不反向构造(C 不传回 argv),返回 null 或 throw new NotSupportedException();
throw new NotSupportedException();
}
public void CleanUpNativeData(IntPtr pNativeData)
{
if (pNativeData == IntPtr.Zero) return;
// 先读出每个 char* 指针并释放
int len = /* 你得知道长度,比如从另一参数传入 */;
for (int i = 0; i < len; i++)
{
var ptr = Marshal.ReadIntPtr(pNativeData, i * IntPtr.Size);
Marshal.FreeHGlobal(ptr);
}
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object managedObj) { }
public int GetNativeDataSize() => -1; // 不固定大小}
如何在 P/Invoke 中正确绑定 ICustomMarshaler
不能直接在参数上写 [MarshalAs(UnmanagedType.CustomMarshaler)] 就完事——必须显式指定 MarshalType 属性,且该类型需有无参构造函数。否则运行时报 System.TypeLoadException: Could not load type。
-
[DllImport("mylib.dll")]方法签名中,参数需标记:[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ArgvMarshaler))] -
MarshalTypeRef必须指向具体类,不能是接口或抽象类 - 若 marshaler 需要构造参数(如编码方式),只能靠静态字段或外部配置,因为 .NET 封送器只调用无参构造
- 同一个
ICustomMarshaler实例会被多次复用,不要在字段里缓存 per-call 状态
容易被忽略的坑:线程、异常与生命周期
封送器运行在 P/Invoke 调用栈中,受调用线程上下文约束。如果 C 函数在后台线程回调,而你的 marshaler 里用了 UI 线程绑定的资源(比如 Control.Invoke),会直接死锁或抛异常。
更隐蔽的问题是异常传播:在 MarshalManagedToNative 中抛出异常,P/Invoke 会终止调用但不保证 CleanUpNativeData 被调用——已分配的非托管内存就此泄露。所以所有分配操作必须配对 try/finally,或用 using 包裹可释放句柄。
另外,.NET 6+ 对某些场景(如 SpanMemoryMarshal + AsPointer),优先考虑这些,而非硬上 ICustomMarshaler。










