开场:
凌晨两点,游戏服务器的崩溃日志疯狂刷屏。你刚接入的某个 C 库通过 P/Invoke 传回了一串乱码,Profiler 指着托管堆里某个 string 的地址,而那个对象已经被 GC 悄悄搬走了。
这种跨语言调用的噩梦,几乎每个 Unity 开发者都遇到过。当你把 C# 字符串塞进 DllImport 的那一刻,CLR 就要把它翻译成 C 端能懂的 char*。这个翻译过程就是 Marshalling(封送)。它不是一行 [MarshalAs] 注解那么简单,而是数据格式、内存布局、GC 生命周期三方规则的精密博弈——任何一方理解不到位,换来的就是乱码、泄漏或玄学崩溃。
本文沿着一个字符串从托管堆到非托管内存的完整旅程,把每一步\”内部怎么走、为何这么设计\”讲透,最后给出 Unity 工程的落地清单。
一、两个世界的字符串:为什么必须\”翻译\”
要理解封送,先看双方的数据结构差异有多大:
- C# string:托管堆上的对象,UTF-16 编码,带对象头与长度字段,不可变(immutable),地址随时可能被 GC 压缩堆时移动。
- C char*:指向一段连续字节的裸指针,编码可能是 ANSI/UTF-8/UTF-16,以 \\0 结尾(长度不靠字段,靠扫描终止符),地址固定但生命周期全




