k-hole is the game development and design blog of kyle kukshtel
subscribe to my newsletter to get posts delivered directly to your inbox

Low-Level Interop and Memory Management in C# for Allocation-free P/Invoke Code


An introduction to low-level C# and figuring out if any idiomatic patterns exist for working with native memory.

March 13, 2023 c# programming pinvoke

lowmem

I’ve been recently picking up work on Dinghy again and as part of it have been looking at different approaches for doing bindings. Previously I was using Mono’s CppSharp to generate bindings for Sokol, which worked” but also generated a ton of cruft around function calls. The main issue was that PInvoking to the extern methods used a lot of marshalling, so they went through great pains to track pointer references to make things more GC proof. So you’d get a lot of functions with a lot of things like this:

SgStencilAttachmentAction(global::Sokol.SgStencilAttachmentAction _0) {
    __Instance = Marshal.AllocHGlobal(sizeof(global::Sokol.SgStencilAttachmentAction.__Internal));
  __ownsNativeInstance = true;
  NativeToManagedMap[__Instance] = this;
  *((global::Sokol.SgStencilAttachmentAction.__Internal*) __Instance) = *((global::Sokol.SgStencilAttachmentAction.__Internal*) _0.__Instance);
}

Again, it worked (mostly, it actually wasn’t working in a few places), but it was ugly and was doing a lot more than it needed to. So I set about trying to find other options.

Discovering Blittable Bindings

I ran across Sokol-CS by BottlenoseLabs, which was really interesting because it claimed to generate fully blittable bindings. Blittable here meaning the memory layout between both the C layer and C# layer are the same, saving you the overhead of needing to move between different types.

A simple example of this is some extern function like

[DLLImport(MyLib)]
public static extern Binding(string name)

This function calls to some C-exposed function called Binding that takes in a string name. Except the C function doesn’t actually use a string. C has no concept of strings” so the actual function signature in C is something like

CAPI Binding(const char *)

When bound as the previous example, C# will do some extra computation when the binding function is invoked in order to handle a string param in the function name (aka Marshalling”). This is great/convenient if we’re binding an operation with low overhead, but for anything in the hot path” of code it means you’re paying a marshalling tax every time the function is invoked. This can be really bad!

Blittable bindings fix this, with the caveat that the onus is on YOU on the C# end to manage the type. This also means you will quickly find yourself in unsafe and fixed land, which can really be daunting if you’ve never done that before. Consider this bit of code from Sokol-CS:

var action = default(sg_pass_action);
ref var colorAttachment = ref action.colors[0];
colorAttachment.action = sg_action.SG_ACTION_CLEAR;
colorAttachment.value = Rgba32F.Black;
sg_begin_default_pass(&action, width, height);
sg_apply_pipeline(_state.Pipeline);
sg_apply_bindings((sg_bindings*) Unsafe.AsPointer(ref _state.Bindings));

You can use address operators in C#? Is that a pointer?? Turns out, yes! C# generally handles memory for you, but you can reach deeper under the hood if you really want performance. I wanted that performance, so I took it upon myself to learn what I could! This is also a very different dotnet domain that you would typically run into, so resources are pretty sparse. I recommend reading these official documentation pages to get a feel for things:

  1. Memory and Span usage guidelines
  2. Unsafe code, pointer types, and function pointers
  3. Pointer related operators - take the address of variables, dereference storage locations, and access memory locations
  4. The Managed Heap and Garbage Collection in the CLR
  5. P/Invoke Tutorial - Pinning

After looking at the above and asking a lot of questions, I started looking more at Sokol-CS to see if it’s what I wanted. The backbone of the bindings were a tool called C2CS that converted C header files to C# code. In practice the tool seemed good and was able to generate bindings, but I couldn’t figure out how to properly configure it to run on arbitary libraries. Additionally, it was done by a small org, so I was worried about spending time learning something that wouldn’t be developed.

I wanted to choose a binding solution that seemed better supported, and was ideally pretty idomatic in terms of its bindings. That journey led me to find ClangSharpPInvokeGenerator, handled and maintained by dotnet itself!

This post gives a great overview of how the tool works generally, but basically its a command line program that uses Clang to generate an AST of a C header file, and then turns that into C# (very similar to C2CS). It also comes with a ton of options, allowing you to customize the bindgen how you see fit. After some initial config stuff, I was rolling with it and had generated working bindings.

Working with native memory

However, I ran into some issues with native memory management and generally interoping with a realtime graphics library that I also wanted to discuss. Specifically, when working with a library like Sokol, you’re doing a lot of struct building and passing back and forth between the C/C# boundary. This introduces some API complexity and puts you in the realm of unmanaged memory management, so there are lots of failure points that become possible that you wouldn’t expect if you came from pure safe” C# coding land.

To that end I wanted to talk about something specific I was dealing with and use it as a way to talk about different ways to allocate and pin” (ie. make sure something doesn’t move due to GC) native objects from inside C#. Hopefully this will be helpful to others as well!

Problem: I want to create a stack-allocated struct that can be used across different functions.

Seems easy enough right? Well… consider this (from the great blog of Jackson Dunstan):

There is a big difference between struct instances as local variables and struct instances as fields of a class. While local variables are always on the stack, class instances are always allocated on the heap. When a class instance is allocated on the heap, memory is reserved for all of its fields. When a field is a struct, all of those fields will be reserved.

So in the case where I have this code:

public class CoolClass {
    public struct NativeStruct;
}

Where” NativeStruct is in memory shifts around as the GC works and plays with any instance I have of CoolClass. For my use, this isn’t good. I want a stable” reference to a stuct as I need to pass it around and modify it between different functions.

Here’s some things that don’t work first, but are useful to know for the domain anyways

Stackalloc

The stackalloc keyword allows you to allocate memory for something on the unmanaged stack. Notably, you can use this in non-unsafe contexts, so is great for working with something like Span:

int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++) {
    numbers[i] = i;
}

So can I just store the Span on the class like this?

public class CoolClass {
    public Span<int> numbers;
}

Nope! Span types can only be class members on ref struct types. This is mainly to make sure that you aren’t trying to do unsafe memory things on heap-allocated types in safe” contexts. I think its an arbitrary limitation just meant to make sure developers don’t shoot themselves in the foot.

A Note on Span

I had a hard time understanding what Span actually does, but the best way to think about it is like saying interpret this block of memory as an array of T”.

Sokol-CS Style

Worth nothing that Sokol-CS uses this pattern a fair bit in examples:

var vertices = (Span<Vertex>)stackalloc Vertex[3];
var desc = new sg_buffer_desc
{
  usage = sg_usage.SG_USAGE_IMMUTABLE,
  type = sg_buffer_type.SG_BUFFERTYPE_VERTEXBUFFER
};
            
ref var reference = ref MemoryMarshal.GetReference(vertices);
desc.data.ptr = Unsafe.AsPointer(ref reference);
desc.data.size = (uint)(Marshal.SizeOf<Vertex>() * vertices.Length);

return sg_make_buffer(&desc);

This works because:

  • stackalloc memory is pinned in the scope of the function
  • Becade of the previous statement, getting a reference to verticies is effectively pinned
  • Getting a pointer from that pinned reference then works (Unsafe.AsPointer)
  • sg_make_buffer gets called with all the pinned data, and then the memory alloc’d with stackalloc is freed.
  • desc is never used again, so GC can clean everything up fine

Fixed

The fixed keyword is the thing you’d likely first encounter when searching for an answer to this type of issue. fixed will pin some memory in place for you to operate on it, like this:

unsafe {
  byte[] bytes = { 1, 2, 3 };
  fixed (byte* pointerToFirst = bytes) {
      Console.WriteLine($"The address of the first array element: {(long)pointerToFirst:X}.");
      Console.WriteLine($"The value of the first array element: {*pointerToFirst}.");
    }
}

Notice that the pinned value is only available in the context created by the fixed statement. Trying to acces pointerToFirst after the fixed context will throw a compiler error. We can’t just make our whole program execution fixed, so this also doesn’t work for us (we want access between methods, not just inside a single one).

Worth nothing that it would probably work to store byte[] on the class, and when we want to access the byte[] pointer we use fixed. However, this would mean that any class that operates on byte[] natively would need to be unsafe, and also require some overhead to managing the object. Additionally, if our interop code needs a stable” pointer to an object, this wouldn’t be guaranteed between method calls, as the GC may move around the byte[].

Okay so what actually works?

GCHandle

Using GCHandle was the first thing I actually got working. You can do this:

public class CoolClass {
    static sgp_vec2[] points_buffer = new sgp_vec2[4096];
    static GCHandle bufferHandle
    public CoolClass() {
    bufferHandle = GCHandle.Alloc(points_buffer,GCHandleType.Pinned);
  }
}

By calling GCHandle.Alloc, you protect the object from garbage collection. Seems simple enough right? However, there are a few complications. Once you are done with the object, you have to remember to call GCHandle.Free() otherwise you will have memory leaks.

Additionally, and perhaps more importantly, we’re passing around a GCHandleObject, not an actual pointer. Because of the aforementioned need for blittable types, we do indeed want actual pointer types. This means that actually using” the handle for calls can be a bit onerous:

Sokol.sgp_draw_points((sgp_vec2*) bufferHandle.AddrOfPinnedObject(), c)

Again, not the worst, and it does work!

So what could be better?

NativeArray”

Big shoutout to Martins on the Handmade Discord for giving me this idea. Basically, we can circumvent all the nice” C# options and just allocate memory directly. We can then add some syntatic sugar on top of it to make it better to interop with, and also give it some utility to prevent leaks.

Benefits:

  • Easy Creation
  • Easy Access API
  • Pinned By Default
  • Free’d by default with GC

Downsides:

  • ???

Here’s the class in full:

public class NativeArray<T> {
    public unsafe T* Ptr {get; protected set;}
    int len;
    public NativeArray(int size) {
        len = size;
        unsafe {
            Ptr = (T*)NativeMemory.Alloc((nuint)size,(nuint)sizeof(T));
        }
    }

    ~NativeArray() {
        Free();
    }

    public void Free() {
        unsafe {
            if(Ptr != null) {
                NativeMemory.Free(Ptr);
                Ptr = null;
            }
        }
    }

    public ref T this[int index]
    {
        get {
            unsafe {
                return ref Ptr[index];
            }
        }
    }
}

Basically, just allocate a chunk of memory the size of the passed in object and store the pointer to that memory. Because of pointer operations, you can also index the memory directly.

Using it is as simple as:

var myNativeArray = new NativeArray<MyNativeType>(10);
myNativeArray[0] = ...

Because the object itself is managed like anything else, we can store it as a class field, pass it around, etc. and not worry about the GC moving the native memory it has a pointer to:

public class MyClass {
  public NativeArray<MyType> MyNativeArray;
}

Unity actually has a similar type that I suspect does the same thing.


Hope you learned some stuff about native memory mangagement in C# as part of this post! My biggest takeaways are:

  • Use fixed/stackalloc when working inside of single functions
  • Use GCHandle/“NativeArray” when working across functions


Date
March 13, 2023




subscribe to my newsletter to get posts delivered directly to your inbox