Speaking of interop - the nice thing about .NET type system, is that it has facilities to do complete mapping to C, with the sole exception of setjmp/longjmp (which is generally a sore point with everything, even C++ doesn't handle it well). You already pointed out unsigned integers and structs; it's also worth pointing out raw (non-GC-aware) pointers, and explicit layout structs; the latter can handle C unions seamlessly, in particular (you just say that all fields have offset 0).
But they’re rarely found in APIs, and there’s Marshal class in the framework to implement manual marshaling by reading/writing stuff at unmanaged memory addresses, even without /unsafe compiler switch.
With a little syntactic sugar in C#, they do map, in the same exact way you do this in C - you just use a "fixed" array of one as the last element:
struct VariableLength {
public int fixedSize;
public fixed int varSize[1];
}
Of course, this also means that you have to use stackalloc or other manual allocation to actually get a properly sized block of memory, computing said size yourself; but you also have to do it in C with malloc:
byte* p = stackalloc byte[sizeof(VariableLength) + 10 * sizeof(int)];
var vl = (VariableLength*) p;
Once you have a pointer, though, you can do vl->varSize[i] etc - this works because the type of vl->varSize when you access it is int*.
Came here to say this — one of my first projects in the industry as an intern way back with .net 1.1 was to create an c# based application to parse through effectively binary serialized variable sized c structs.
"fixed" was a C# 2.0 addition, though. Back in 1.x days, you basically had to declare that field as a simple scalar instead, and then use & to manually obtain the pointer (but then you could index it with [] etc, same as in C, so it doesn't really make that much of a difference in this case; "fixed" sure did come in handy for other stuff though).