CA1838: Avoid StringBuilder
parameters for P/Invokes
Property | Value |
---|---|
Rule ID | CA1838 |
Title | Avoid StringBuilder parameters for P/Invokes |
Category | Performance |
Fix is breaking or non-breaking | Non-breaking |
Enabled by default in .NET 8 | No |
Cause
A P/Invoke has a StringBuilder parameter.
Rule description
Marshalling of StringBuilder
always creates a native buffer copy, resulting in multiple allocations for one P/Invoke call. To marshal a StringBuilder
as a P/Invoke parameter, the runtime will:
- Allocate a native buffer.
- If it is an
In
parameter, copy the contents of theStringBuilder
to the native buffer. - If it is an
Out
parameter, copy the native buffer into a newly allocated managed array.
By default, StringBuilder
is In
and Out
.
For more information about marshalling strings, see Default marshalling for strings.
This rule is disabled by default, because it can require case-by-case analysis of whether the violation is of interest and potentially non-trivial refactoring to address the violation. Users can explicitly enable this rule by configuring its severity.
How to fix violations
In general, addressing a violation involves reworking the P/Invoke and its callers to use a buffer instead of StringBuilder
. The specifics would depend on the use cases for the P/Invoke.
Here is an example for the common scenario of using StringBuilder
as an output buffer to be filled by the native function:
// Violation
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern void Foo(StringBuilder sb, ref int length);
public void Bar()
{
int BufferSize = ...
StringBuilder sb = new StringBuilder(BufferSize);
int len = sb.Capacity;
Foo(sb, ref len);
string result = sb.ToString();
}
For use cases where the buffer is small and unsafe
code is acceptable, stackalloc can be used to allocate the buffer on the stack:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern unsafe void Foo(char* buffer, ref int length);
public void Bar()
{
int BufferSize = ...
unsafe
{
char* buffer = stackalloc char[BufferSize];
int len = BufferSize;
Foo(buffer, ref len);
string result = new string(buffer);
}
}
For larger buffers, a new array can be allocated as the buffer:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern void Foo([Out] char[] buffer, ref int length);
public void Bar()
{
int BufferSize = ...
char[] buffer = new char[BufferSize];
int len = buffer.Length;
Foo(buffer, ref len);
string result = new string(buffer);
}
When the P/Invoke is frequently called for larger buffers, ArrayPool<T> can be used to avoid the repeated allocations and memory pressure that comes with them:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern unsafe void Foo([Out] char[] buffer, ref int length);
public void Bar()
{
int BufferSize = ...
char[] buffer = ArrayPool<char>.Shared.Rent(BufferSize);
try
{
int len = buffer.Length;
Foo(buffer, ref len);
string result = new string(buffer);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
If the buffer size is not known until runtime, the buffer may need to be created differently based on the size to avoid allocating large buffers with stackalloc
.
The preceding examples use 2-byte wide characters (CharSet.Unicode
). If the native function uses 1-byte characters (CharSet.Ansi
), a byte
buffer can be used instead of a char
buffer. For example:
[DllImport("MyLibrary", CharSet = CharSet.Ansi)]
private static extern unsafe void Foo(byte* buffer, ref int length);
public void Bar()
{
int BufferSize = ...
unsafe
{
byte* buffer = stackalloc byte[BufferSize];
int len = BufferSize;
Foo(buffer, ref len);
string result = Marshal.PtrToStringAnsi((IntPtr)buffer);
}
}
If the parameter is also used as input, the buffers need to be populated with the string data with any null terminator explicitly added.
When to suppress warnings
Suppress a violation of this rule if you're not concerned about the performance impact of marshalling a StringBuilder
.
Suppress a warning
If you just want to suppress a single violation, add preprocessor directives to your source file to disable and then re-enable the rule.
#pragma warning disable CA1838
// The code that's violating the rule is on this line.
#pragma warning restore CA1838
To disable the rule for a file, folder, or project, set its severity to none
in the configuration file.
[*.{cs,vb}]
dotnet_diagnostic.CA1838.severity = none
For more information, see How to suppress code analysis warnings.