If possible, you should absolutely use the span approaches on this same question by canton7, here.
This answer is just to complete some information from the question, specifically:
I don't mind using unsafe in this particular case (however coming from C, I am not aware of the consequences of doing so)
The main time this information is relevant is for when:
- spans aren't available in your target platform (or can't be used with the API you're consuming)
- you're talking to unmanaged libraries via P/Invoke
Just to add some terminology: byte* is an unmanaged pointer ("unsafe"), and ref byte is a managed pointer ("safe", relatively speaking). Spans are basically "managed pointers, plus a length"; so: a Span<byte> (or ReadOnlySpan<byte>) is broadly comparable (in unmanaged land) to a pair of byte* and int (length). The huge difference is that the garbage collector understands managed pointers (including tracking what they touch, and fixing them if it decides to move memory around), and does not even look at unmanaged pointers (they are just opaque native integers) - so: keeping everything managed avoids a lot of problems.
As a consequence, if you obtain the unmanaged pointer to data, it is your job to make sure that the data can't possibly get moved while you're using it, which would make your pointer invalid. To do this, you usually use the fixed keyword, i.e. (in an unsafe block):
uint[] data = ...
fixed (uint* u32ptr = data)
{
// we can coerce pointers:
byte* bptr = (byte*)u32ptr;
// TODO: and now we can use that byte* (along with data.Length * sizeof(uint))
// to pass to e.g. Encoding methods
return Encoding.UTF32.GetString(bptr, data.Length * sizeof(uint));
}
The important thing is not to store such a pointer (u32ptr or bptr here) in such a way that it could escape the fixed block. Storing it on a field, for example. As long as you don't do that, you're fine: the garbage collector knows how to interpret fixed, and knows that it can't move data while the thread is inside that region; fixed is a very low cost way of temporarily pinning data. There is a second way, used for longer running requirements - usually because you're handing managed memory to an unmanaged API that then holds the pointer as a field for longer than the P/Invoke call(s) that you might use inside a fixed region - GCHandle with a type of GCHandleType.Pinned - but that should be avoided usually.