An introduction to low-level C# and figuring out if any idiomatic patterns exist for working with native memory.
March 13, 2023
Tagged: c# programming pinvoke
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.
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:
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.
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 andstruct
instances as fields of aclass
. While local variables are always on the stack,class
instances are always allocated on the heap. When aclass
instance is allocated on the heap, memory is reserved for all of its fields. When a field is astruct
, 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
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
public class CoolClass {
public Span<int> numbers;
}
Nope! Spanref 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”.
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 functionverticies
is effectively pinnedUnsafe.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 fineThe 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?
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?
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:
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:
fixed
/stackalloc
when working inside of single functionsGCHandle
/“NativeArray” when working across functionsPublished on March 13, 2023.
Tagged: c# programming pinvoke