Sometimes you just want a small array without the heap allocations and GC. Existing solutions like stackalloc require unsafe code, don’t allow for dynamic growth, and don’t support foreach loops. So today we’ll design and build a code generator that puts a new tool in your toolbox!

Goals

The goal today is to create a code generator that outputs a C# struct type that behaves like an array. The output types are generally called SmallBuffer as they’re meant for small numbers of elements that conveniently fit onto the stack. It can be stored on the heap, such as by being a field of a class, but it’s primarily used as a local variable on the stack.

The following table summarizes its characteristics in comparison to other common array-like collection types:

Collection Allocation Location Managed Elements Bounds Checks Foreach Dynamic GC
SmallBuffer Stack, Heap Local, Field Yes Exceptions, Assertions, None If unmanaged Optional (capped) No
Managed array Heap Local, Field Yes Exceptions, None Yes No Yes
List Heap Local, Field Yes Exceptions Yes Yes (uncapped) Yes
stackalloc Stack Local No None No No No
fixed buffer Stack, Heap Local,Field No None No No No
NativeArray Heap Local, Field No Exceptions, None Yes No No
NativeList Heap Local, Field No Exceptions or none Yes Yes No

This type has many nice advantages: allocation on the stack or the heap, use as a local or a field, support for managed elements, flexible bounds checks, and no triggering of the GC. On the other hand, foreach support is only available with unmanaged elements and dynamic resizing is limited to a fixed capacity. Additionally, SmallBuffer types can use Unity features such as the Burst compiler, but can also drop the Unity dependency when those features aren’t needed. They can take advantage of C# 7 features, but also support older versions of C# such as in Unity 2017.4.

As for the code generator for SmallBuffer types, we’ll write it in pure C# so that it can be used in Unity editor scripts and as a command line tool. The code generator should be free of dependencies on Unity or C# 7, execute quickly, and easily fit into any build pipeline or workflow.

Design

Now let’s talk about how to achieve these goals. At its simplest, we’re looking for a type like this:

[StructLayout(LayoutKind.Sequential)]
public struct SmallBufferInt4
{
    // Elements of the "array"
    private int m_Element0;
    private int m_Element1;
    private int m_Element2;
    private int m_Element3;
 
    // Index into the array
    public int this[int index]
    {
        get
        {
            switch (index)
            {
                case 0: return m_Element0;
                case 1: return m_Element1;
                case 2: return m_Element2;
                case 3: return m_Element3;
            }
        }
        set
        {
            switch (index)
            {
                case 0: m_Element0 = value; break;
                case 1: m_Element1 = value; break;
                case 2: m_Element2 = value; break;
                case 3: m_Element3 = value; break;
            }
        }
    }
 
    // The length is constant
    public const int Length = 4;
}

While these switch statements are necessary for managed types like string, we can simply index into the sequential fields using a little unsafe code:

[StructLayout(LayoutKind.Sequential)]
public unsafe struct SmallBufferInt4
{
    private int m_Element0;
    private int m_Element1;
    private int m_Element2;
    private int m_Element3;
 
    public int this[int index]
    {
        get
        {
            fixed (int* elements = &m_Element0)
            {
                return elements[index];
            }
        }
        set
        {
            fixed (int* elements = &m_Element0)
            {
                return elements[index] = value;
            }
        }
    }
 
    public const int Length = 4;
}

If C# 7 support is enabled, we can omit the set and use a ref return:

[StructLayout(LayoutKind.Sequential)]
public unsafe struct SmallBufferInt4
{
    private int m_Element0;
    private int m_Element1;
    private int m_Element2;
    private int m_Element3;
 
    public ref int this[int index]
    {
        get
        {
            fixed (int* elements = &m_Element0)
            {
                return ref elements[index];
            }
        }
    }
 
    public const int Length = 4;
}

Error-checking should go into [BurstDiscard] functions with contents depending on the specified error-handling strategy. These can then be called when necessary, such as when the index parameter to the indexer is out of bounds.

// Exceptions
[Unity.Burst.BurstDiscard]
public void RequireIndexInBounds(int index)
{
    if (index < 0 || index >= 4)
    {
        throw new System.InvalidOperationException(
            "Index out of bounds: " + index);
    }
}
 
// Unity assertions
[Unity.Burst.BurstDiscard]
public void RequireIndexInBounds(int index)
{
    UnityEngine.Assertions.Assert.IsTrue(
        index >= 0 && index < 4,
        "Index out of bounds: " + index);
}
 
// None
[Unity.Burst.BurstDiscard]
public void RequireIndexInBounds(int index)
{
}

To support dynamic resizing, up to the fixed capacity of course, we add length and version fields as well as functions like Add, Insert, RemoveAt, RemoveRange, and Clear as well as helper functions like GetElement and SetElement that skip the error-checking. This also converts the Length constant into a Count property and adds a Capacity constant to match the API of List<T>.

private int m_Length;
private int m_Version;
 
public int Count
{
    get
    {
        return m_Length;
    }
}
 
public const int Capacity = 4;
 
public void RemoveAt(int index)
{
    RequireIndexInBounds(index);
    for (int i = index; i < m_Length - 1; ++i)
    {
        SetElement(i, GetElement(i + 1));
    }
    m_Length--;
    m_Version++;
}
 
private int GetElement(int index)
{
    fixed (int* elements = &m_Element0)
    {
        return elements[index];
    }
}
 
private void SetElement(int index, int value)
{
    fixed (int* elements = &m_Element0)
    {
        elements[index] = value;
    }
}

Then we can add support for foreach loops by including a GetEnumerator method that returns a nested Enumerator type with a Current property and MoveNext method. This is safe in C# 7 because the Enumerator type is a ref struct. This allows us to store pointer fields in it without worrying that its lifetime will extend beyond the lifetime of the SmallBuffer it points into.

public ref struct Enumerator
{
    private readonly int* m_Elements;
 
    private int m_Index;
 
    private readonly int m_OriginalVersion;
 
    private readonly int* m_Version;
 
    private readonly int m_Length;
 
    public Enumerator(int* elements, int* version, int length)
    {
        m_Elements = elements;
        m_Index = -1;
        m_OriginalVersion = *version;
        m_Version = version;
        m_Length = length;
    }
 
    public bool MoveNext()
    {
        RequireVersionMatch();
        m_Index++;
        return m_Index < m_Length;
    }
 
    public ref int Current
    {
        get
        {
            RequireVersionMatch();
            RequireIndexInBounds();
            return ref m_Elements[m_Index];
        }
    }
 
    [Unity.Burst.BurstDiscard]
    public void RequireVersionMatch()
    {
        if (m_OriginalVersion != *m_Version)
        {
            throw new System.InvalidOperationException(
                "Buffer modified during enumeration");
        }
    }
 
    [Unity.Burst.BurstDiscard]
    public void RequireIndexInBounds()
    {
        if (m_Index < 0 || m_Index >= m_Length)
        {
            throw new System.InvalidOperationException(
                "Index out of bounds: " + m_Index);
        }
    }
}
 
public Enumerator GetEnumerator()
{
    // Safe because Enumerator is a 'ref struct'
    fixed (int* elements = &m_Element0)
    {
        return new Enumerator(elements);
    }
}
Implementation

The code generator will exist in a single C# file containing a simple API made up of a single Generate function and a couple of enum types to specify the configuration. Its input consists of the following parameters:

  • string namespaceName: Namespace to put the generated type in
  • string typeName: Name of the generated type
  • int capacity: Capacity of the buffer to generate in number of elements
  • bool isFixedLength: If the generated buffer has a fixed length
  • ErrorHandlingStrategy boundsCheckStrategy: Error handling strategy the generated type should use to handle out-of-bounds errors. None, UnityAssertions, or Exceptions are supported.
  • ErrorHandlingStrategy versionCheckStrategy: Error handling strategy the generated type should use to handle version check errors (i.e. if the buffer is changed during enumeration).
  • string elementTypeName: Name of the type of elements stored in the buffer
  • ElementType elementType: Type of the element. Managed, Unmanaged, UnmanagedWithUnityBurstSupport, UnmanagedWithCsharp7Support, and UnmanagedWithCsharp7SupportAndUnityBurstSupport are supported.

Given those inputs, SmallBufferGenerator.Generate returns a string of the C# source code containing the SmallBuffer type. It should be saved to file and built into a project. Alternatively, it can easily be copied to the clipboard or transferred over a network for extra flexibility.

Now that we’ve thought through the design of the code generator, its implementation is extremely straightforward. A StringBuilder is used to build up the source code string through a series of Append and AppendLine calls. The code generator is mostly a linear series of steps to write out the source. It’s verbose, but easy to understand imperative-style code. Have a read, or simply copy this into a C# project:

using System.Text;
 
/// <summary>
/// Code generator for structs representing a small buffer
/// </summary>
/// 
/// <author>
/// Jackson Dunstan, https://JacksonDunstan.com/articles/5051
/// </author>
/// 
/// <license>
/// MIT
/// </license>
public static class SmallBufferGenerator
{
    /// <summary>
    /// Types of error handling
    /// </summary>
    public enum ErrorHandlingStrategy
    {
        /// <summary>
        /// Perform no error checking at all
        /// </summary>
        None,
 
        /// <summary>
        /// Check errors using Unity assertions
        /// </summary>
        UnityAssertions,
 
        /// <summary>
        /// Check errors using exceptions
        /// </summary>
        Exceptions,
    }
 
    /// <summary>
    /// Types of buffer elements
    /// </summary>
    public enum ElementType
    {
        /// <summary>
        /// Managed types
        /// </summary>
        Managed,
 
        /// <summary>
        /// Unmanaged types
        /// </summary>
        Unmanaged,
 
        /// <summary>
        /// Unmanaged types with support for usage in a Unity Burst-compiled job
        /// </summary>
        UnmanagedWithUnityBurstSupport,
 
        /// <summary>
        /// Unmanaged types with support for usage in a C# 7 application
        /// </summary>
        UnmanagedWithCsharp7Support,
 
        /// <summary>
        /// Unmanaged types with support for usage in a C# 7 application and
        /// in a Unity Burst-compiled job
        /// </summary>
        UnmanagedWithCsharp7SupportAndUnityBurstSupport,
    }
 
    /// <summary>
    /// One level of indentation
    /// </summary>
    private const char OneIndent = '\t';
 
    /// <summary>
    /// Generate a C# source file for a small buffer type
    /// </summary>
    /// 
    /// <returns>
    /// The generated C# source file
    /// </returns>
    /// 
    /// <param name="namespaceName">
    /// Namespace to put the generated type in
    /// </param>
    /// 
    /// <param name="typeName">
    /// Name of the generated type
    /// </param>
    /// 
    /// <param name="capacity">
    /// Capacity of the buffer to generate in number of elements
    /// </param>
    /// 
    /// <param name="isFixedLength">
    /// If the generated buffer has a fixed length
    /// </param>
    /// 
    /// <param name="boundsCheckStrategy">
    /// Error handling strategy the generated type should use to handle
    /// out-of-bounds errors
    /// </param>
    /// 
    /// <param name="versionCheckStrategy">
    /// Error handling strategy the generated type should use to handle
    /// version check errors (i.e. if the buffer is changed during enumeration)
    /// </param>
    /// 
    /// <param name="elementTypeName">
    /// Name of the type of elements stored in the buffer. Should be usable
    /// without any 'using' statements. Pass null to make the type generic.
    /// </param>
    /// 
    /// <param name="elementType">
    /// Type of the element
    /// </param>
    public static string Generate(
        string namespaceName,
        string typeName,
        int capacity,
        bool isFixedLength,
        ErrorHandlingStrategy boundsCheckStrategy,
        ErrorHandlingStrategy versionCheckStrategy,
        string elementTypeName,
        ElementType elementType)
    {
        // Replace type name with 'T' to make it generic
        bool isGeneric;
        if (elementTypeName == null)
        {
            isGeneric = true;
            elementTypeName = "T";
        }
        else
        {
            isGeneric = false;
        }
 
        // Decide if Unity Burst is enabled or not
        bool enableBurst;
        switch (elementType)
        {
            case ElementType.UnmanagedWithUnityBurstSupport:
            case ElementType.UnmanagedWithCsharp7SupportAndUnityBurstSupport:
                enableBurst = true;
                break;
            default:
                enableBurst = false;
                break;
        }
 
        // Decide if C# 7 is enabled or not
        bool enableCsharp7;
        switch (elementType)
        {
            case ElementType.UnmanagedWithCsharp7Support:
            case ElementType.UnmanagedWithCsharp7SupportAndUnityBurstSupport:
                enableCsharp7 = true;
                break;
            default:
                enableCsharp7 = false;
                break;
        }
 
        int indentLevel = 0;
 
        // File header
        StringBuilder output = new StringBuilder(1024 * 64);
        output.AppendLine("////////////////////////////////////////////////////////////////////////////////");
        output.AppendLine("// Warning: This file was automatically generated by SmallBufferGenerator.");
        output.AppendLine("//          If you edit this by hand, the next run of SmallBufferGenerator");
        output.AppendLine("//          will overwrite your edits.");
        output.AppendLine("////////////////////////////////////////////////////////////////////////////////");
        output.AppendLine();
 
        // Begin namespace
        output.Append("namespace ");
        output.AppendLine(namespaceName);
        output.AppendLine("{");
        indentLevel++;
 
        // Begin struct
        output.Append(OneIndent, indentLevel);
        output.AppendLine("[System.Runtime.InteropServices.StructLayout(");
        indentLevel++;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("System.Runtime.InteropServices.LayoutKind.Sequential)]");
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.Append("public ");
        if (elementType != ElementType.Managed)
        {
            output.Append("unsafe ");
        }
        output.Append("struct ");
        output.Append(typeName);
        if (isGeneric)
        {
            output.Append('<');
            output.Append(elementTypeName);
            output.AppendLine(">");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.Append("where T : unmanaged");
            indentLevel--;
        }
        output.AppendLine();
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
 
        if (elementType != ElementType.Managed && enableCsharp7)
        {
            // Begin enumerator struct
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public ref struct Enumerator");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
 
            // Enumerator buffer field
            output.Append(OneIndent, indentLevel);
            output.Append("private readonly ");
            output.Append(elementTypeName);
            output.AppendLine("* m_Elements;");
 
            // Enumerator index field
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("private int m_Index;");
 
            // Enumerator version field
            if (!isFixedLength)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine();
                output.Append(OneIndent, indentLevel);
                output.AppendLine("private readonly int m_OriginalVersion;");
 
                output.Append(OneIndent, indentLevel);
                output.AppendLine();
                output.Append(OneIndent, indentLevel);
                output.AppendLine("private readonly int* m_Version;");
 
                output.Append(OneIndent, indentLevel);
                output.AppendLine();
                output.Append(OneIndent, indentLevel);
                output.AppendLine("private readonly int m_Length;");
            }
 
            // Enumerator constructor
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.Append("public Enumerator(");
            output.Append(elementTypeName);
            output.Append("* elements");
            if (!isFixedLength)
            {
                output.Append(", int* version, int length");
            }
            output.AppendLine(")");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Elements = elements;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Index = -1;");
            if (!isFixedLength)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("m_OriginalVersion = *version;");
                output.Append(OneIndent, indentLevel);
                output.AppendLine("m_Version = version;");
                output.Append(OneIndent, indentLevel);
                output.AppendLine("m_Length = length;");
            }
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
 
            // Enumerator MoveNext method
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public bool MoveNext()");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            if (!isFixedLength)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("RequireVersionMatch();");
            }
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Index++;");
            output.Append(OneIndent, indentLevel);
            output.Append("return m_Index < ");
            if (isFixedLength)
            {
                output.Append(capacity);
            }
            else
            {
                output.Append("m_Length");
            }
            output.AppendLine(";");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
 
            // Enumerator Current property
            output.Append(OneIndent, indentLevel);
            output.Append("public ref ");
            output.Append(elementTypeName);
            output.AppendLine(" Current");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("get");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            if (!isFixedLength)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("RequireVersionMatch();");
            }
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds();");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("return ref m_Elements[m_Index];");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            if (!isFixedLength)
            {
                // RequireVersionMatch method
                output.Append(OneIndent, indentLevel);
                output.AppendLine();
                if (enableBurst)
                {
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("[Unity.Burst.BurstDiscard]");
                }
                output.Append(OneIndent, indentLevel);
                output.AppendLine("public void RequireVersionMatch()");
                output.Append(OneIndent, indentLevel);
                output.AppendLine("{");
                indentLevel++;
                switch (versionCheckStrategy)
                {
                    case ErrorHandlingStrategy.UnityAssertions:
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine(
                            "UnityEngine.Assertions.Assert.IsTrue(");
                        indentLevel++;
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine("m_OriginalVersion == *m_Version,");
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine(
                            "\"Buffer modified during enumeration\");");
                        indentLevel--;
                        break;
                    case ErrorHandlingStrategy.Exceptions:
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine(
                            "if (m_OriginalVersion != *m_Version)");
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine("{");
                        indentLevel++;
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine(
                            "throw new System.InvalidOperationException(");
                        indentLevel++;
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine(
                            "\"Buffer modified during enumeration\");");
                        indentLevel--;
                        indentLevel--;
                        output.Append(OneIndent, indentLevel);
                        output.AppendLine("}");
                        break;
                }
                indentLevel--;
                output.Append(OneIndent, indentLevel);
                output.AppendLine("}");
            }
 
            // RequireIndexInBounds method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            if (enableBurst)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("[Unity.Burst.BurstDiscard]");
            }
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public void RequireIndexInBounds()");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            AppendBoundsCheck(
                output,
                "m_Index",
                indentLevel,
                isFixedLength,
                capacity,
                boundsCheckStrategy);
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // End enumerator struct
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
 
        // Element fields
        for (int i = 0; i < capacity; ++i)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.Append("private ");
            if (elementType != ElementType.Managed && enableCsharp7)
            {
                output.Append("readonly ");
            }
            output.Append(elementTypeName);
            output.Append(" m_Element");
            output.Append(i);
            output.AppendLine(";");
        }
 
        if (!isFixedLength)
        {
            // Version field
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("private int m_Version;");
 
            // Length field
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("private int m_Length;");
        }
 
        // Indexer
        output.Append(OneIndent, indentLevel);
        output.AppendLine();
        output.Append(OneIndent, indentLevel);
        output.Append("public ");
        if (elementType != ElementType.Managed && enableCsharp7)
        {
            output.Append("ref ");
        }
        output.Append(elementTypeName);
        output.AppendLine(" this[int index]");
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("get");
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("RequireIndexInBounds(index);");
        output.Append(OneIndent, indentLevel);
        output.Append("return ");
        if (elementType != ElementType.Managed && enableCsharp7)
        {
            output.Append("ref ");
        }
        output.AppendLine("GetElement(index);");
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
        if (elementType == ElementType.Managed || !enableCsharp7)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine("set");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds(index);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(index, value);");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
 
        // GetElement
        output.Append(OneIndent, indentLevel);
        output.AppendLine();
        output.Append(OneIndent, indentLevel);
        output.Append("private ");
        if (elementType != ElementType.Managed && enableCsharp7)
        {
            output.Append("ref ");
        }
        output.Append(elementTypeName);
        output.AppendLine(" GetElement(int index)");
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
        if (elementType == ElementType.Managed)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine("switch (index)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            for (int i = 0; i < capacity; ++i)
            {
                output.Append(OneIndent, indentLevel);
                output.Append("case ");
                output.Append(i);
                output.Append(": return ");
                if (enableCsharp7)
                {
                    output.Append("ref ");
                }
                output.Append("m_Element");
                output.Append(i);
                output.AppendLine(";");
            }
            output.Append(OneIndent, indentLevel);
            output.Append("default: return default(");
            output.Append(elementTypeName);
            output.AppendLine(");");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
        else
        {
            output.Append(OneIndent, indentLevel);
            output.Append("fixed (");
            output.Append(elementTypeName);
            output.AppendLine("* elements = &m_Element0)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.Append("return ");
            if (enableCsharp7)
            {
                output.Append("ref ");
            }
            output.AppendLine("elements[index];");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
 
        // SetElement
        output.Append(OneIndent, indentLevel);
        output.AppendLine();
        output.Append(OneIndent, indentLevel);
        output.Append("private void SetElement(int index, ");
        output.Append(elementTypeName);
        output.AppendLine(" value)");
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
        if (elementType == ElementType.Managed)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine("switch (index)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            for (int i = 0; i < capacity; ++i)
            {
                output.Append(OneIndent, indentLevel);
                output.Append("case ");
                output.Append(i);
                output.Append(": m_Element");
                output.Append(i);
                output.AppendLine(" = value; break;");
            }
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
        else
        {
            output.Append(OneIndent, indentLevel);
            output.Append("fixed (");
            output.Append(elementTypeName);
            output.AppendLine("* elements = &m_Element0)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("elements[index] = value;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
 
        // Length constant or property
        output.Append(OneIndent, indentLevel);
        output.AppendLine();
        if (isFixedLength)
        {
            output.Append(OneIndent, indentLevel);
            output.Append("public const int Length = ");
            output.Append(capacity);
            output.AppendLine(";");
        }
        else
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public int Count");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("get");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("return m_Length;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
 
        // Capacity constant
        if (!isFixedLength)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.Append("public const int Capacity = ");
            output.Append(capacity);
            output.AppendLine(";");
        }
 
        // GetEnumerator method
        if (elementType != ElementType.Managed && enableCsharp7)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public Enumerator GetEnumerator()");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("// Safe because Enumerator is a 'ref struct'");
            output.Append(OneIndent, indentLevel);
            output.Append("fixed (");
            output.Append(elementTypeName);
            output.AppendLine("* elements = &m_Element0)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            if (isFixedLength)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("return new Enumerator(elements);"); 
            }
            else
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("fixed (int* version = &m_Version)");
                output.Append(OneIndent, indentLevel);
                output.AppendLine("{");
                indentLevel++;
                output.Append(OneIndent, indentLevel);
                output.AppendLine(
                    "return new Enumerator(elements, version, m_Length);");
                indentLevel--;
                output.Append(OneIndent, indentLevel);
                output.AppendLine("}");
            }
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
 
        if (!isFixedLength)
        {
            // Add method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.Append("public void Add(");
            output.Append(elementTypeName);
            output.AppendLine(" item)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireNotFull();");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(m_Length, item);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Length++;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Version++;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // Clear method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public void Clear()");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("for (int i = 0; i < m_Length; ++i)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.Append("SetElement(i, default(");
            output.Append(elementTypeName);
            output.AppendLine("));");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Length = 0;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Version++;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // Insert method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.Append("public void Insert(int index, ");
            output.Append(elementTypeName);
            output.AppendLine(" value)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireNotFull();");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds(index);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("for (int i = m_Length; i > index; --i)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(i, GetElement(i - 1));");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(index, value);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Length++;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Version++;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // RemoveAt method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public void RemoveAt(int index)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds(index);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("for (int i = index; i < m_Length - 1; ++i)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(i, GetElement(i + 1));");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Length--;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Version++;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // RemoveRange method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public void RemoveRange(int index, int count)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds(index);");
            switch (boundsCheckStrategy)
            {
                case ErrorHandlingStrategy.UnityAssertions:
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("UnityEngine.Assertions.Assert.IsTrue(");
                    indentLevel++;
                    output.Append(OneIndent, indentLevel);
                    output.Append("count >= 0");
                    output.AppendLine(",");
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("\"Count must be positive: \" + count);");
                    indentLevel--;
                    break;
                case ErrorHandlingStrategy.Exceptions:
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("if (count < 0)");
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("{");
                    indentLevel++;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("throw new System.ArgumentOutOfRangeException(");
                    indentLevel++;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("\"count\",");
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("\"Count must be positive: \" + count);");
                    indentLevel--;
                    indentLevel--;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("}");
                    break;
            }
            output.Append(OneIndent, indentLevel);
            output.AppendLine("RequireIndexInBounds(index + count - 1);");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("int indexAfter = index + count;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("int indexEndCopy = indexAfter + count;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("if (indexEndCopy >= m_Length)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("indexEndCopy = m_Length;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("int numCopies = indexEndCopy - indexAfter;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("for (int i = 0; i < numCopies; ++i)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(index + i, GetElement(index + count + i));");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("for (int i = indexAfter; i < m_Length - 1; ++i)");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("SetElement(i, GetElement(i + 1));");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Length -= count;");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("m_Version++;");
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
 
            // RequireNotFull method
            output.Append(OneIndent, indentLevel);
            output.AppendLine();
            if (enableBurst)
            {
                output.Append(OneIndent, indentLevel);
                output.AppendLine("[Unity.Burst.BurstDiscard]");
            }
            output.Append(OneIndent, indentLevel);
            output.AppendLine("public void RequireNotFull()");
            output.Append(OneIndent, indentLevel);
            output.AppendLine("{");
            indentLevel++;
            switch (boundsCheckStrategy)
            {
                case ErrorHandlingStrategy.UnityAssertions:
                    output.Append(OneIndent, indentLevel);
                    output.Append(
                        "UnityEngine.Assertions.Assert.IsTrue(m_Length != ");
                    output.Append(capacity);
                    output.AppendLine(", \"Buffer overflow\");");
                    break;
                case ErrorHandlingStrategy.Exceptions:
                    output.Append(OneIndent, indentLevel);
                    output.Append("if (m_Length == ");
                    output.Append(capacity);
                    output.AppendLine(")");
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("{");
                    indentLevel++;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine(
                        "throw new System.InvalidOperationException(");
                    indentLevel++;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("\"Buffer overflow\");");
                    indentLevel--;
                    indentLevel--;
                    output.Append(OneIndent, indentLevel);
                    output.AppendLine("}");
                    break;
            }
            indentLevel--;
            output.Append(OneIndent, indentLevel);
            output.AppendLine("}");
        }
 
        // RequireIndexInBounds method
        output.Append(OneIndent, indentLevel);
        output.AppendLine();
        if (enableBurst)
        {
            output.Append(OneIndent, indentLevel);
            output.AppendLine("[Unity.Burst.BurstDiscard]");
        }
        output.Append(OneIndent, indentLevel);
        output.AppendLine("public void RequireIndexInBounds(int index)");
        output.Append(OneIndent, indentLevel);
        output.AppendLine("{");
        indentLevel++;
        AppendBoundsCheck(
            output,
            "index",
            indentLevel,
            isFixedLength,
            capacity,
            boundsCheckStrategy);
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
 
        // End of struct
        indentLevel--;
        output.Append(OneIndent, indentLevel);
        output.AppendLine("}");
 
        // End of namespace
        output.AppendLine("}");
 
        return output.ToString();
    }
 
    private static void AppendBoundsCheck(
        StringBuilder output,
        string indexName,
        int indentLevel,
        bool isFixedLength,
        int capacity,
        ErrorHandlingStrategy boundsCheckStrategy)
    {
        switch (boundsCheckStrategy)
        {
            case ErrorHandlingStrategy.UnityAssertions:
                output.Append(OneIndent, indentLevel);
                output.AppendLine("UnityEngine.Assertions.Assert.IsTrue(");
                indentLevel++;
                output.Append(OneIndent, indentLevel);
                output.Append(indexName);
                output.Append(" >= 0 && ");
                output.Append(indexName);
                output.Append(" < ");
                if (isFixedLength)
                {
                    output.Append(capacity);
                }
                else
                {
                    output.Append("m_Length");
                }
                output.AppendLine(",");
                output.Append(OneIndent, indentLevel);
                output.Append("\"Index out of bounds: \" + ");
                output.Append(indexName);
                output.AppendLine(");");
                indentLevel--;
                break;
            case ErrorHandlingStrategy.Exceptions:
                output.Append(OneIndent, indentLevel);
                output.Append("if (");
                output.Append(indexName);
                output.Append(" < 0 || ");
                output.Append(indexName);
                output.Append(" >= ");
                if (isFixedLength)
                {
                    output.Append(capacity);
                }
                else
                {
                    output.Append("m_Length");
                }
                output.AppendLine(")");
                output.Append(OneIndent, indentLevel);
                output.AppendLine("{");
                indentLevel++;
                output.Append(OneIndent, indentLevel);
                output.AppendLine("throw new System.InvalidOperationException(");
                indentLevel++;
                output.Append(OneIndent, indentLevel);
                output.Append("\"Index out of bounds: \" + ");
                output.Append(indexName);
                output.AppendLine(");");
                indentLevel--;
                indentLevel--;
                output.Append(OneIndent, indentLevel);
                output.AppendLine("}");
                break;
        }
    }
}
Usage

To use the code generator, follow these steps:

  1. Build the above source code into a Unity or non-Unity project
  2. Call SmallBufferGenerator.Generate
  3. Save the C# output to a C# file in a Unity or non-Unity project
  4. Build the project and use the generated type
Examples

Now let’s look at some examples of using a generated SmallBuffer type. First, here’s a fixed-size array:

int GetWinningTeamIndex(Player[] players)
{
    // Assume there are four teams
    // This array has four int elements
    SmallBufferInt4 pointTotals = default;
 
    // Total the points for each team
    foreach (Player player in players)
    {
        pointTotals[player.TeamIndex] += player.NumPoints;
    }
 
    // Find the highest point total
    int maxPoints = pointTotals[0];
    int winningTeamIndex = 0;
    for (int i = 1; i < pointTotals.Length; ++i)
    {
        int points = pointTotals[i];
        if (points > maxPoints)
        {
            maxPoints = points;
            winningTeamIndex = i;
        }
    }
 
    return winningTeamIndex;
}

Consider how this would have looked without SmallBuffer:

int GetWinningTeamIndex(Player[] players)
{
    // Assume there are four teams
    int pointTotals0 = 0;
    int pointTotals1 = 0;
    int pointTotals2 = 0;
    int pointTotals3 = 0;
 
    // Total the points for each team
    foreach (Player player in players)
    {
        switch (player.TeamIndex)
        {
            case 0: pointTotals0 += player.NumPoints; break;
            case 1: pointTotals1 += player.NumPoints; break;
            case 2: pointTotals2 += player.NumPoints; break;
            case 3: pointTotals3 += player.NumPoints; break;
        }
    }
 
    // Find the highest point total
    int maxPoints = pointTotals0;
    int winningTeamIndex = 0;
    if (pointTotals1 > maxPoints)
    {
        maxPoints = pointTotals1;
        winningTeamIndex = 1;
    }
    if (pointTotals2 > maxPoints)
    {
        maxPoints = pointTotals2;
        winningTeamIndex = 2;
    }
    if (pointTotals3 > maxPoints)
    {
        maxPoints = pointTotals3;
        winningTeamIndex = 3;
    }
 
    return winningTeamIndex;
}

This version includes duplication in the local variables, the foreach loop’s switch, and the calculation at the end. It’s also slower because there are more branches than with a SmallBuffer that can use indexing.

Now let’s look at an example of a dynamically-resizing SmallBuffer:

// Get the locations and average location of the players on a team
Vector3 GetPlayerLocations(
    Player[] players,
    int teamIndex,
    ref SmallBufferDynamic20Vector3 locations) // up to 20 Vector3 elements
{
    // Get the locations
    foreach (Player player in players)
    {
        if (player.TeamIndex == teamIndex)
        {
            locations.Add(player.Location);
        }
    }
 
    // Find the average location
    Vector3 average = new Vector3(0, 0, 0);
    foreach (ref Vector3 location in locations)
    {
        average += location;
    }
    return average / locations.Count;
}

In this function, we take a SmallBuffer containing 20 Vector3 elements as an ref parameter so it’s passed by reference. We simply loop over the players calling Add on the SmallBuffer as we find members of the team. This assumes there are up to 20 players on a team and we’ll get an assertion or exception if we’re wrong. Then we use a foreach loop with a ref variable to enumerate the found locations and sum them up.

The calling code might look like this:

SmallBufferDynamic20Vector3 locations = default;
Vector3 averageLocation = GetPlayerLocations(m_Players, m_Team, ref locations);
foreach (ref Vector3 location in locations)
{
    // ... use 'location'
}

Now let’s look at how we’d do this without SmallBuffer:

// Get the locations and average location of the players on a team
Vector3 GetPlayerLocations(
    Player[] players,
    int teamIndex,
    NativeList<Vector3> locations)
{
    // Get the locations
    foreach (Player player in players)
    {
        if (player.TeamIndex == teamIndex)
        {
            locations.Add(player.Location);
        }
    }
 
    // Find the average location
    Vector3 average = new Vector3(0, 0, 0);
    foreach (ref Vector3 location in locations)
    {
        average += location;
    }
    return average / locations.Count;
}
 
NativeList<Vector3> locations = new NativeList<Vector3>(20, Allocator.Temp);
Vector3 averageLocation = GetPlayerLocations(m_Players, m_Team, locations);
foreach (ref Vector3 location in locations)
{
    // ... use 'location'
}
locations.Dispose();

The code is similar, but with two key differences. First, NativeList<Vector3> is allocating its memory on the heap rather than the stack. Second, we must remember to call locations.Dispose() after we’re done with it. This presents a resource-management issue where we might accidentally use it after calling Dispose or forget to call Dispose altogether. No such management is necessary with a SmallBuffer.

Conclusion

The SmallBuffer code generator and the struct types it produces provide a new tool in our toolbox. It’s useful when we need a small-sized array and want to avoid heap allocation, unsafe code, exceptions, and the GC. By no means does it replace other collection types such as managed arrays and NativeArray<T>, but it may come in handy in your projects.