Quick answer: Mismatched string encoding between native side and C# marshaling produces garbage. Use [MarshalAs(UnmanagedType.LPUTF8Str)] for UTF-8 char*, LPWStr for UTF-16. Or accept IntPtr and convert with Marshal.PtrToStringUTF8.
Here is how to fix Unity P/Invoke calls that return readable English in editor but garbled bytes for non-ASCII (or even for ASCII on some platforms). The default marshaling assumes ANSI; modern native code uses UTF-8.
The Symptom
Native function returns a string. C# receives a string but the contents are mojibake or appear cut off at non-ASCII.
What Causes This
Encoding mismatch. Default LPStr is ANSI, but most modern native code uses UTF-8.
Wide vs narrow. char* (1 byte) vs wchar_t* (2 or 4 bytes); marshaling must know which.
Lifetime issues. Native pointer freed before marshaling copies it.
The Fix
Step 1: Use UTF-8 marshaling.
using System.Runtime.InteropServices;
[DllImport("mylib")]
[return: MarshalAs(UnmanagedType.LPUTF8Str)]
private static extern string GetGreeting();
Step 2: For UTF-16 native.
[DllImport("mylib", CharSet = CharSet.Unicode)]
private static extern string GetWideText();
Step 3: For ownership control, return IntPtr.
[DllImport("mylib")]
private static extern IntPtr GetText();
public static string GetTextManaged()
{
IntPtr p = GetText();
return Marshal.PtrToStringUTF8(p);
}
Useful when the native side allocates and you need to free explicitly.
Step 4: Free if native owns and gives back ownership.
[DllImport("mylib")] static extern void FreeText(IntPtr p);
try
{
return Marshal.PtrToStringUTF8(p);
}
finally
{
FreeText(p);
}
Step 5: Test with a non-ASCII string. Verify with é, ü, or emoji. ASCII may pass even with wrong encoding; international content reveals the bug.
“Match the encoding. UTF-8 attribute or PtrToStringUTF8. Free if owned. Test with non-ASCII.”
Related Issues
For IL2CPP stripping, see IL2CPP Stripping. For BurstCompile, see Burst Compile.
UTF-8 attribute. PtrToStringUTF8. Test non-ASCII. Strings round-trip clean.