When you pass a string between managed and unmanaged code, in most cases, the default marshalling behavior makes a copy of the string and converts it between the .NET and default platform formats, if necessary. You can change this behavior with a MarshalAsAttribute. Refer to Strings used in platform invoke for more information.
Note: Different platforms use different memory allocators. When native code returns a buffer that the .NET marshaller later frees, you must allocate it with the allocator the runtime expects: the COM allocator (CoTaskMemAlloc) on Windows and the C allocator (malloc) on other platforms. Allocating and freeing memory with mismatched functions can crash your program.
The following examples demonstrate ways to pass string data between managed and unmanaged functions.
#include <stdlib.h> // malloc
#include <string.h> // strlen, memcpy
#if defined(_WIN32)
#include <objbase.h> // CoTaskMemAlloc
#endif
#if defined(_WIN32)
void* Allocate(size_t size)
{
// .NET runtime assumes the COM allocator when freeing memory returned from native code on Windows-based platforms
return CoTaskMemAlloc(size);
}
#else
void* Allocate(size_t size)
{
// .NET runtime assumes the C allocator when freeing memory returned from native code on non-Windows platforms
return malloc(size);
}
#endif
char* AllocateAndCopyString(const char* value)
{
size_t bufferSize = strlen(value) + 1;
void* ret = Allocate(bufferSize);
memcpy(ret, value, bufferSize);
return (char*)ret;
}
extern "C" {
void SendString(const char* message) {
printf("%s\n", message); // Shown in Player.log
fflush(stdout);
}
const char* GetUnmanagedString() {
return AllocateAndCopyString("Hello from C\u002B\u002B"); // The runtime frees this with the matching allocator
}
void SendStringArray(const char** messages, int length) {
for(int i = 0; i < length; i++) {
printf("%s\n", messages[i]);
}
fflush(stdout);
}
void SendCollectionsString(const char* strPtr, int length) {
printf("%.*s\n", length, strPtr);
fflush(stdout);
}
}
Note: Refer to Call unmanaged functions from managed code for information about the annotations needed to compile and call functions such as this as part of a dynamically loaded library.
Note: Marshalling strings as LPUTF8Str (as the C# example below does) passes correct UTF–8 bytes to native code, but printing them with printf doesn’t display the same on every platform. On platforms whose console and C runtime default to UTF–8 (such as macOS, Linux, Android, and iOS), the printf calls above display non-ASCII text correctly. On Windows, the console and C runtime default to a non-UTF–8 code page, so a plain printf of UTF–8 text can appear garbled even though the marshalled data is correct. To print non-ASCII text on Windows, configure the console for UTF–8 (for example with SetConsoleOutputCP(CP_UTF8)) or convert the UTF–8 bytes to UTF–16 with MultiByteToWideChar and print them with the wide-character APIs.
This C# example uses the unmanaged functions to demonstrate how to:
using UnityEngine;
using System;
using System.Runtime.InteropServices;
public class StringMarshalExample : MonoBehaviour
{
// Pass string parameter. Marshal as UTF-8 so non-ASCII text is preserved;
// the default string marshalling isn't Unicode-safe on every platform.
[DllImport("__Internal")]
private static extern void SendString([MarshalAs(UnmanagedType.LPUTF8Str)] string message);
// Return string from native. Marshalling the return value as LPUTF8Str makes
// the runtime copy the string into managed memory and free the native buffer
// with the platform's default allocator (CoTaskMemFree on Windows, free
// elsewhere), which matches the Allocate() function in the native code.
[DllImport("__Internal")]
[return: MarshalAs(UnmanagedType.LPUTF8Str)]
private static extern string GetUnmanagedString();
// Pass string array parameter. Marshal each element as UTF-8 for the same
// reason as SendString; without ArraySubType the elements default to the
// platform's ANSI marshalling, which isn't Unicode-safe.
[DllImport("__Internal")]
private static extern void SendStringArray(
[In, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPUTF8Str)] string[] strings,
int length);
void Start()
{
// Pass strings to native function
SendString("Hello from C#");
SendString("七転び八起き");
// Get a string from the native function. The runtime copies it into managed
// memory and frees the native buffer for you.
string managedStr = GetUnmanagedString();
Debug.Log(managedStr);
// Pass string arrays to native function
string[] strings = new string[] { "one", "two", "three" };
SendStringArray(strings, strings.Length);
}
}
Note: The scripting runtime expects managed strings to be immutableYou cannot change the contents of an immutable (read-only) package. This is the opposite of mutable. Most packages are immutable, including packages downloaded from the package registry or by Git URL.
See in Glossary. Even though you might be able to write unmanaged code that changes the underlying data referenced by a string variable, doing so can introduce subtle and not-so-subtle bugs in your program.
The Unity.Collections namespace contains a few types that you can use to work with strings stored in unmanaged memory. Use these types to avoid the frequent string copy and garbage collection that occurs with the System.String class.
The following examples demonstrate a few ways that you can pass strings stored in Unity.Collections types to unmanaged functions (such as the SendCollectionsString function in the previous C/C++ code example). These examples rely on pointers and require an unsafe context.
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using System;
using System.Runtime.InteropServices;
using System.Text;
public class CollectionStringExample
{
// Pass a pointer to a Unity.Collections object containing a string
[DllImport("__Internal")]
private static extern void SendCollectionsString(IntPtr textData, int letterCount);
public static void RunExamples()
{
unsafe // Unsafe because of use of pointers to Collections buffers
{
// Pass pointer to NativeText instance
var nativeText = new NativeText("十人十色", Allocator.Temp);
SendCollectionsString((IntPtr)nativeText.GetUnsafePtr(), nativeText.Length);
nativeText.Dispose();
// Pass pointer to FixedString instance
var fixedStr = new FixedString128Bytes("Fixed string");
SendCollectionsString((IntPtr)fixedStr.GetUnsafePtr(), fixedStr.Length);
// Pass pointer to NativeArray containing a string of UTF8 characters
byte[] utf8Bytes = Encoding.UTF8.GetBytes("雨降って地固まる"); // Managed strings are UTF-16
var nativeArray = new NativeArray<byte>(utf8Bytes, Allocator.Temp);
void* unsafePtr = nativeArray.GetUnsafePtr();
SendCollectionsString((IntPtr)unsafePtr, nativeArray.Length);
nativeArray.Dispose();
}
}
}
Note: Change DllImport to use the library name if you use precompiled, dynamically linked libraries. Use the special string, __Internal for source code plug-ins and statically linked libraries. Refer to DllImport attribute for more information.