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 the StringBuilder 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.

See also