I am trying to implement the new pattern introduced in C# 7.3 that supports pinning of custom types using fixed statement. See article on the Docs
I am however concerned that in the code below I am returning a pointer to the string, and then leaving the fixed scope. Of course, this whole routine will be used by a "parent" fixed statement that will be "fixing" my custom type, however I am not sure if the string (which is a field in my custom type) will still remain fixed. So I don't know if this whole approach will work.
readonly struct PinnableStruct {
private readonly string _String;
private readonly int _Index;
public unsafe ref char GetPinnableReference() {
if (_String is null) return ref Unsafe.AsRef<char>(null);
fixed (char* p = _String) return ref Unsafe.AsRef<char>(p + _Index);
}
}
The code above will then be utilized by the following sample code:
static void SomeRoutine(PinnableStruct data) {
fixed(char* p = data) {
//iterate over characters in data and do something with them
}
}
There's no need for fixed on the string or pointers at this point and in fact it would be conceptually incorrect to do so. After your null check/return (which should also check for length 0), you can do this:
var strSpan = _String.AsSpan();
return ref strSpan[_Index];
This, as the function name specifies, returns a pinnable reference to the first character of the underlying string. What you've got above returns a reference to the first dereferenced element of a pinned pointer to the underlying string, but the lifetime of the pinning is limited to the scope of the fixed statement (could be wrong, but I don't think the returned ref keeps it pinned). Using a ref to a Span element avoids an unnecessary pinning.
Update - I looked into pinned pointers a bit more (a bunch of unfruitful google searches - fell back to decompiling/experimenting). It turns out returning a ref to an element of a pinned pointer does not preserve the pin. The reference returned is a reference to the resolved address - all context of pinning is lost.
I thought maybe changing _String to a Memory<char> (use AsMemory extension in constructor) could work. Then your GetPinnableReference could just return ref _String[0]; but AsMemory gives you a read-only memory object (can't return writable reference).
Short of implementing IPinnable with GCHandle and IDisposable or MemoryManager<T> (heavier than you want for sure), I think my Span<char> solution above might be the most safe/correct way to accomplish this.
Here's a disassembly of GetPinnableReference (don't mind my PowerPC.Test namespace - I'm making a .NET Standard 1.1 PowerPC binary disassembler/emulator/debugger and using these concepts heavily, hence my interest):
.method public hidebysig instance char& GetPinnableReference() cil managed
{
// Code size 34 (0x22)
.maxstack 2
.locals init (char* V_0,
string pinned V_1,
char& V_2)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld string PowerPC.Test.Console.Program/PinnableStruct::_string
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: conv.u
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: brfalse.s IL_0016
IL_000e: ldloc.0
IL_000f: call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::get_OffsetToStringData()
IL_0014: add
IL_0015: stloc.0
IL_0016: nop
IL_0017: ldloc.0
IL_0018: call !!0& [System.Runtime.CompilerServices.Unsafe]System.Runtime.CompilerServices.Unsafe::AsRef<char>(void*)
IL_001d: stloc.2
IL_001e: br.s IL_0020
IL_0020: ldloc.2
IL_0021: ret
} // end of method SomeInfo::GetPinnableReference
I don't expect the majority of readers to be able to read the IL, but it's ok. The answer we seek is right at the top (and confirmed in the IL following it). We've got three locals: char* V_0, string pinned V_1, and char& V_2. To me, this sheds a bit of light on why the fixed syntax is what it is. You have the fixed keyword represented by string pinned V_1, the pointer to char (from your code) represented by char* V_0, and the reference that gets returned represented by char& V_2. Three separate concepts represented by three separate code constructs.
So, your fixed statement pins the string and assigns the pinning reference to V_1 at IL_0002 to IL_0007, then it loads V_1 converts it to a pointer (conversion to unsigned integer) and stores it in V_0 (your char* p) at IL_0008 to IL_000a, finally it loads V_0, computes the offset of the char specified by _index, and stores it back in V_0 at IL_000e to IL_0015. Within the scope of the fixed statement, it loads V_0, converts it to a reference via Unsave.AsRef<char>, and stores it in V_2 (the reference that will be returned) at IL_0017 to IL_001d. I'm not sure why the C# compiler does this, but the last bit from IL_001e to IL_0021 is often how the compiler handles returning a value; it inserts a branch to the block that will return the value (even though in this case it's the next instruction), then it loads the return value from a local variable (that it created and IMO un-necessarily assigned to, but perhaps there is a logical explanation for this?), and finally returns the result you had obtained several instructions ago.
There you have it, an answer, finally! The pinning from your fixed statement can't be maintained because it's dereferencing an unmanaged pointer V_0/p that just happens in this case to be backed by a pinning reference, V_1, which is something the instructions to dereference the pointer don't know about). The CLR team could've been clever in the JIT and handled that case specially through analysis by associating the char reference with the pinned reference, but I think that'd be architecturally weird, hard to explain/document, and I doubt they went that route, so I didn't go look at the CLR code to verify.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With