Skip to content

Commit 96de41f

Browse files
CopilotBillWagnerCopilotgewarren
authored
Improve StringBuilder interop guidance: warn about allocations, promote ArrayPool (#52133)
* Initial plan * Update StringBuilder guidance in default-marshalling-for-strings.md Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> * Update docs/framework/interop/default-marshalling-for-strings.md * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix ArrayPool sample: use return value for string length and return buffer to pool Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> Co-authored-by: Bill Wagner <wiwagn@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com>
1 parent 3d382b5 commit 96de41f

1 file changed

Lines changed: 37 additions & 14 deletions

File tree

docs/framework/interop/default-marshalling-for-strings.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
22
title: "Default Marshalling for Strings"
33
description: Review the default marshalling behavior for strings in interfaces, platform invoke, structures, & fixed-length string buffers in .NET.
4-
ms.date: 10/04/2021
4+
ms.date: 03/11/2026
5+
ai-usage: ai-assisted
56
dev_langs:
67
- "csharp"
78
- "vb"
@@ -239,7 +240,7 @@ int GetWindowText(
239240
);
240241
```
241242

242-
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[]`.
243+
A `char[]` can be dereferenced and modified by the callee. The recommended approach is to use <xref:System.Buffers.ArrayPool`1> to rent a `char[]`, which avoids repeated heap allocations. The following code example demonstrates this pattern.
243244

244245
```csharp
245246
using System;
@@ -249,7 +250,7 @@ using System.Runtime.InteropServices;
249250
internal static class NativeMethods
250251
{
251252
[DllImport("User32.dll", CharSet = CharSet.Unicode)]
252-
public static extern void GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount);
253+
public static extern int GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount);
253254
}
254255

255256
public class Window
@@ -258,8 +259,15 @@ public class Window
258259
public string GetText()
259260
{
260261
char[] buffer = ArrayPool<char>.Shared.Rent(256 + 1);
261-
NativeMethods.GetWindowText(h, buffer, buffer.Length);
262-
return new string(buffer);
262+
try
263+
{
264+
int length = NativeMethods.GetWindowText(h, buffer, buffer.Length);
265+
return new string(buffer, 0, length);
266+
}
267+
finally
268+
{
269+
ArrayPool<char>.Shared.Return(buffer);
270+
}
263271
}
264272
}
265273
```
@@ -270,24 +278,39 @@ Imports System.Buffers
270278
Imports System.Runtime.InteropServices
271279

272280
Friend Class NativeMethods
273-
Public Declare Auto Sub GetWindowText Lib "User32.dll" _
274-
(hWnd As IntPtr, <Out> lpString() As Char, nMaxCount As Integer)
281+
Public Declare Auto Function GetWindowText Lib "User32.dll" _
282+
(hWnd As IntPtr, <Out> lpString() As Char, nMaxCount As Integer) As Integer
275283
End Class
276284

277285
Public Class Window
278286
Friend h As IntPtr ' Friend handle to Window.
279287
Public Function GetText() As String
280288
Dim buffer() As Char = ArrayPool(Of Char).Shared.Rent(256 + 1)
281-
NativeMethods.GetWindowText(h, buffer, buffer.Length)
282-
Return New String(buffer)
283-
End Function
289+
Try
290+
Dim length As Integer = NativeMethods.GetWindowText(h, buffer, buffer.Length)
291+
Return New String(buffer, 0, length)
292+
Finally
293+
ArrayPool(Of Char).Shared.Return(buffer)
294+
End Try
295+
End Function
284296
End Class
285297
```
286298

287-
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.
288-
289-
> [!NOTE]
290-
> 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).
299+
You might also consider passing a <xref:System.Text.StringBuilder> instead of a <xref:System.String>. The buffer that's created when a `StringBuilder` is marshalled 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.
300+
301+
> [!CAUTION]
302+
> 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:
303+
>
304+
> 1. A managed `StringBuilder` buffer.
305+
> 1. A native buffer allocated during marshalling.
306+
> 1. If `[Out]`, the native buffer contents are copied into a newly allocated managed array.
307+
> 1. A `string` allocated by `ToString()`.
308+
>
309+
> 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()`.
310+
>
311+
> 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.
312+
>
313+
> 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).
291314
292315
## See also
293316

0 commit comments

Comments
 (0)