From b8016eeeb9ac6c39108ece52fa2ec6f0b8009b6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:46:25 +0000 Subject: [PATCH 1/6] Initial plan From d56875844e332e6c34781f8c188cfc3d42ed9a41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:49:13 +0000 Subject: [PATCH 2/6] Update StringBuilder guidance in default-marshalling-for-strings.md Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> --- .../default-marshalling-for-strings.md | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/framework/interop/default-marshalling-for-strings.md b/docs/framework/interop/default-marshalling-for-strings.md index 752025773934f..dbedc88caf53d 100644 --- a/docs/framework/interop/default-marshalling-for-strings.md +++ b/docs/framework/interop/default-marshalling-for-strings.md @@ -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" @@ -239,7 +240,7 @@ int GetWindowText( ); ``` -A `char[]` can be dereferenced and modified by the callee. The following code example demonstrates how `ArrayPool` can be used to pre-allocate a `char[]`. +A `char[]` can be dereferenced and modified by the callee. The recommended approach is to use to rent a `char[]`, which avoids repeated heap allocations. The following code example demonstrates this pattern. ```csharp using System; @@ -284,10 +285,21 @@ Public Class Window End Class ``` -Another solution is to pass a as the argument instead of a . 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 instead of a . 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` 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 From 341435375d0f0fdd352abf5853d47a8b2fece44a Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 11 Mar 2026 08:49:31 -0400 Subject: [PATCH 3/6] Update docs/framework/interop/default-marshalling-for-strings.md --- docs/framework/interop/default-marshalling-for-strings.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/framework/interop/default-marshalling-for-strings.md b/docs/framework/interop/default-marshalling-for-strings.md index dbedc88caf53d..28e34e974d255 100644 --- a/docs/framework/interop/default-marshalling-for-strings.md +++ b/docs/framework/interop/default-marshalling-for-strings.md @@ -290,10 +290,10 @@ You might also consider passing a instead of a > [!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)** +> 1. A managed `StringBuilder` buffer. +> 2. A native buffer allocated during marshalling. +> 3. If `[Out]`, the native buffer contents are copied into a newly allocated managed array. +> 4. A `string` allocated by `ToString()`. > > Reusing the same `StringBuilder` across calls saves only one allocation. Using a character buffer rented from `ArrayPool` is much more efficient—it reduces subsequent calls to just the allocation for `ToString()`. > From fdfbbd539a81f727a4740dfe0ba2927b4f5c5b88 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 12 Mar 2026 10:47:16 -0400 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/framework/interop/default-marshalling-for-strings.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/framework/interop/default-marshalling-for-strings.md b/docs/framework/interop/default-marshalling-for-strings.md index 28e34e974d255..011a08c98df06 100644 --- a/docs/framework/interop/default-marshalling-for-strings.md +++ b/docs/framework/interop/default-marshalling-for-strings.md @@ -291,9 +291,9 @@ You might also consider passing a instead of a > 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. -> 2. A native buffer allocated during marshalling. -> 3. If `[Out]`, the native buffer contents are copied into a newly allocated managed array. -> 4. A `string` allocated by `ToString()`. +> 1. A native buffer allocated during marshalling. +> 1. If `[Out]`, the native buffer contents are copied into a newly allocated managed array. +> 1. A `string` allocated by `ToString()`. > > Reusing the same `StringBuilder` across calls saves only one allocation. Using a character buffer rented from `ArrayPool` is much more efficient—it reduces subsequent calls to just the allocation for `ToString()`. > From b1acaa3ed10c2073107e0ff08be58fc02eaac994 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Thu, 12 Mar 2026 10:47:35 -0400 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/framework/interop/default-marshalling-for-strings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/interop/default-marshalling-for-strings.md b/docs/framework/interop/default-marshalling-for-strings.md index 011a08c98df06..5ee185aa185b5 100644 --- a/docs/framework/interop/default-marshalling-for-strings.md +++ b/docs/framework/interop/default-marshalling-for-strings.md @@ -1,7 +1,7 @@ --- 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: 03/10/2026 +ms.date: 03/11/2026 ai-usage: ai-assisted dev_langs: - "csharp" From 88bb6e7ae3d7b7ef23ffaf81f55b3799f7cf5857 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:51:15 +0000 Subject: [PATCH 6/6] Fix ArrayPool sample: use return value for string length and return buffer to pool Co-authored-by: BillWagner <493969+BillWagner@users.noreply.github.com> --- .../default-marshalling-for-strings.md | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/framework/interop/default-marshalling-for-strings.md b/docs/framework/interop/default-marshalling-for-strings.md index 5ee185aa185b5..3c12a5fabf1ad 100644 --- a/docs/framework/interop/default-marshalling-for-strings.md +++ b/docs/framework/interop/default-marshalling-for-strings.md @@ -250,7 +250,7 @@ using System.Runtime.InteropServices; internal static class NativeMethods { [DllImport("User32.dll", CharSet = CharSet.Unicode)] - public static extern void GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount); + public static extern int GetWindowText(IntPtr hWnd, [Out] char[] lpString, int nMaxCount); } public class Window @@ -259,8 +259,15 @@ public class Window public string GetText() { char[] buffer = ArrayPool.Shared.Rent(256 + 1); - NativeMethods.GetWindowText(h, buffer, buffer.Length); - return new string(buffer); + try + { + int length = NativeMethods.GetWindowText(h, buffer, buffer.Length); + return new string(buffer, 0, length); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } } ``` @@ -271,17 +278,21 @@ Imports System.Buffers Imports System.Runtime.InteropServices Friend Class NativeMethods - Public Declare Auto Sub GetWindowText Lib "User32.dll" _ - (hWnd As IntPtr, lpString() As Char, nMaxCount As Integer) + Public Declare Auto Function GetWindowText Lib "User32.dll" _ + (hWnd As IntPtr, lpString() As Char, nMaxCount As Integer) As Integer End Class Public Class Window Friend h As IntPtr ' Friend handle to Window. Public Function GetText() As String Dim buffer() As Char = ArrayPool(Of Char).Shared.Rent(256 + 1) - NativeMethods.GetWindowText(h, buffer, buffer.Length) - Return New String(buffer) - End Function + Try + Dim length As Integer = NativeMethods.GetWindowText(h, buffer, buffer.Length) + Return New String(buffer, 0, length) + Finally + ArrayPool(Of Char).Shared.Return(buffer) + End Try + End Function End Class ```