固定底层字符串的GetPinnableReference实现

时间:2018-10-09 17:54:47

标签: .net pointers unsafe c#-7.3

我正在尝试实现C#7.3中引入的新模式,该模式支持使用fixed语句固定自定义类型。 See article on the Docs

但是,我担心在下面的代码中我将返回指向字符串的指针,然后离开fixed范围。当然,整个例程将由将“修复”我的自定义类型的“父” fixed语句使用,但是我不确定字符串(这是我的自定义类型中的一个字段)是否仍然会保持固定。所以我不知道这整个方法是否行得通。

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);
  }
}

上面的代码将被以下示例代码利用:

static void SomeRoutine(PinnableStruct data) {
  fixed(char* p = data) {
    //iterate over characters in data and do something with them
  }
}

1 个答案:

答案 0 :(得分:1)

此时无需在字符串或指针上进行固定,实际上这样做在概念上是不正确的。空检查/返回(还应该检查长度0)之后,您可以执行以下操作:

var strSpan = _String.AsSpan();
return ref strSpan[_Index];

正如函数名所指定的那样,它返回对基础字符串的第一个字符的可固定引用。上面的操作返回对固定字符串的第一个取消引用的引用,该固定指针指向基础字符串,但是固定的有效期仅限于固定语句的范围(可能是错误的,但我认为返回的ref使其保持固定)。对Span元素使用ref可以避免不必要的固定。

更新-我进一步研究了固定指针(一堆无效的Google搜索-退回到了反编译/实验)。事实证明,将ref返回到固定指针的元素不会保留该引脚。返回的引用是对已解析地址的引用-固定的所有上下文都将丢失。

我认为也许将_String更改为Memory<char>(在构造函数中使用AsMemory扩展名)是可行的。这样,您的GetPinnableReference就可以return ref _String[0];了,但是AsMemory给了您一个只读的内存对象(无法返回可写引用)。

IPinnableGCHandleIDisposable(比您确定的要重)来实现MemoryManager<T>的简短内容,我想我上面的Span<char>解决方案可能是完成此操作的最安全/正确的方法。

这里是对GetPinnableReference的反汇编(不要介意我的PowerPC.Test命名空间-我正在制作.NET Standard 1.1 PowerPC二进制反汇编程序/仿真器/调试器,并大量使用了这些概念,因此引起了我的兴趣):

.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

我不希望大多数读者能够阅读IL,但这没关系。我们寻求的答案就在顶部(并在其后的IL中得到确认)。我们有三个本地人:char* V_0string pinned V_1char& V_2。对我而言,这为固定语法的含义提供了一点启示。您拥有由string pinned V_1表示的固定关键字,由char* V_0表示的char指针(来自您的代码)和由char& V_2表示的返回引用。由三个独立的代码构造代表的三个独立的概念。

因此,您的固定语句将字符串固定,并在V_1处将对IL_0002的固定引用分配给IL_0007,然后加载V_1将其转换为指针(转换到无符号整数),并将其存储在V_0char* p的{​​{1}}(您的IL_0008)中,最后加载IL_000a,计算{{由V_0指定的1}},并将其存储回char的{​​{1}}到_index中。在固定语句的范围内,它将加载V_0,并通过V_0将其转换为引用,然后将其存储在IL_000e到{{ 1}}。我不确定为什么C#编译器会这样做,但是从IL_0015Unsave.AsRef<char>的最后一位通常是编译器如何处理返回值的方式。它在要返回该值的块中插入一个分支(即使在这种情况下,这是下一条指令),然后从本地变量(它创建并不必要地分配给IMO)加载返回值,但是也许逻辑解释?),最后返回您之前获得的几条指令的结果。

终于有了答案!无法维护固定语句的固定,因为它解引用了非托管指针V_2 / IL_0017,在这种情况下恰好由固定引用IL_001d作为后盾,解引用指针的指令不知道的东西)。 CLR团队本来可以在JIT中很聪明,并且通过将char引用与固定引用相关联来通过分析来专门处理该案例,但是我认为这样做在结构上很奇怪,难以解释/记录,我怀疑他们这样做了路线,所以我没有去查看CLR代码进行验证。