Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions docs/framework/interop/default-marshalling-for-strings.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
title: "Default Marshalling for Strings"
description: Review the default marshalling behavior for strings in interfaces, platform invoke, structures, & fixed-length string buffers in .NET.
ms.date: 10/04/2021
ms.date: 03/10/2026
ai-usage: ai-assisted
dev_langs:
- "csharp"
- "vb"
Expand Down Expand Up @@ -239,7 +240,7 @@ int GetWindowText(
);
```

A `char[]` can be dereferenced and modified by the callee. The following code example demonstrates how `ArrayPool<char>` can be used to pre-allocate a `char[]`.
A `char[]` can be dereferenced and modified by the callee. The recommended approach is to use <xref:System.Buffers.ArrayPool%601> to rent a `char[]`, which avoids repeated heap allocations. The following code example demonstrates this pattern.

```csharp
using System;
Expand Down Expand Up @@ -284,10 +285,21 @@ Public Class Window
End Class
```

Another solution is to pass a <xref:System.Text.StringBuilder> as the argument instead of a <xref:System.String>. The buffer created when marshalling a `StringBuilder` can be dereferenced and modified by the callee, provided it does not exceed the capacity of the `StringBuilder`. It can also be initialized to a fixed length. For example, if you initialize a `StringBuilder` buffer to a capacity of `N`, the marshaller provides a buffer of size (`N`+1) characters. The +1 accounts for the fact that the unmanaged string has a null terminator while `StringBuilder` does not.

> [!NOTE]
> In general, passing `StringBuilder` arguments is not recommended if you're concerned about performance. For more information, see [String parameters](../../standard/native-interop/best-practices.md#string-parameters).
You might also consider passing a <xref:System.Text.StringBuilder> instead of a <xref:System.String>. The buffer created when marshalling a `StringBuilder` can be dereferenced and modified by the callee, provided it doesn't exceed the capacity of the `StringBuilder`. It can also be initialized to a fixed length. For example, if you initialize a `StringBuilder` buffer to a capacity of `N`, the marshaller provides a buffer of size (`N`+1) characters. The +1 accounts for the fact that the unmanaged string has a null terminator while `StringBuilder` doesn't.

> [!CAUTION]
> Avoid `StringBuilder` parameters when performance matters. Marshalling a `StringBuilder` *always* creates a native buffer copy. A typical call to get a string out of native code can result in four allocations:
>
> 1. A managed `StringBuilder` buffer **(1)**
> 2. A native buffer allocated during marshalling **(2)**
> 3. If `[Out]`, the native buffer contents are copied into a newly allocated managed array **(3)**
> 4. A `string` allocated by `ToString()` **(4)**
>
> Reusing the same `StringBuilder` across calls saves only one allocation. Using a character buffer rented from `ArrayPool<char>` is much more efficient—it reduces subsequent calls to just the allocation for `ToString()`.
>
> Additionally, the `StringBuilder` capacity does **not** include a hidden null terminator, which interop always accounts for. This is a common mistake, because most APIs want the size of the buffer *including* the null. This can result in wasted or unnecessary allocations, and it prevents the runtime from optimizing `StringBuilder` marshalling to minimize copies.
>
> For more information, see [String parameters](../../standard/native-interop/best-practices.md#string-parameters) and [CA1838: Avoid `StringBuilder` parameters for P/Invokes](../../fundamentals/code-analysis/quality-rules/ca1838.md).

## See also

Expand Down
Loading