固定底层字符串的 GetPinnableReference 实现

GetPinnableReference Implementation that Pins Underlying String

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

但是我担心在下面的代码中我返回了一个指向字符串的指针,然后离开了fixed范围。当然,整个例程将由 "parent" fixed 语句使用,该语句将成为 "fixing" 我的自定义类型,但是我不确定字符串(这是我自定义的字段类型)仍将保持固定。所以我不知道这整个方法是否有效。

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

此时不需要固定在字符串或指针上,事实上这样做在概念上是不正确的。在你的 null check/return (也应该检查长度 0)之后,你可以这样做:

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

正如函数名所指定的那样,return是对基础字符串第一个字符的固定引用。你在 return 上面得到的是对指向底层字符串的固定指针的第一个取消引用元素的引用,但固定的生命周期仅限于固定语句的范围(可能是错误的,但我不不要以为 returned ref 会保留它)。使用对 Span 元素的引用可避免不必要的固定。

更新 - 我更多地研究了固定指针(一堆没有结果的 google 搜索 - 退回到 decompiling/experimenting)。事实证明,return对固定指针的元素进行引用不会保留该 pin。引用 returned 是对已解析地址的引用 - 所有固定上下文都已丢失。

我想也许可以将 _String 更改为 Memory<char>(在构造函数中使用 AsMemory 扩展名)。然后你的 GetPinnableReference 可能只是 return ref _String[0];AsMemory 给你一个只读内存对象(不能 return 可写引用)。

没有用 GCHandleIDisposableMemoryManager<T> 实现 IPinnable(肯定比你想要的要重),我认为我上面的 Span<char> 解决方案可能是实现此目的最 safe/correct 的方法。

这是 GetPinnableReference 的反汇编(不要介意我的 PowerPC.Test 命名空间 - 我正在制作 .NET Standard 1.1 PowerPC 二进制文件 disassembler/emulator/debugger 并大量使用这些概念,因此我很感兴趣):

.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 表示的获取 returned 的引用。三个独立的概念由三个独立的代码结构表示。

因此,您的固定语句固定字符串并将 V_1 的固定引用分配给 IL_0002IL_0007,然后加载 V_1 并将其转换为指针(转换为无符号整数)并将其存储在 V_0(您的 char* p)中 IL_0008IL_000a,最后加载 V_0,计算char_index 指定,并将其存储回 V_0IL_000eIL_0015。在 fixed 语句的范围内,它加载 V_0,通过 Unsave.AsRef<char> 将其转换为引用,并将其存储在 V_2 中(将被 returned 的引用)在 IL_0017IL_001d。我不确定为什么 C# 编译器会这样做,但是从 IL_001eIL_0021 的最后一位通常是编译器如何处理 returning 一个值;它向将 return 值的块插入一个分支(即使在这种情况下它是下一条指令),然后它从局部变量(它创建并且 IMO un-必须分配给,但也许对此有一个合乎逻辑的解释?),最后 return 是您之前通过几条指令获得的结果。

终于有了答案!固定语句中的固定无法维护,因为它取消引用了一个非托管指针 V_0/p,在这种情况下恰好由固定引用 V_1 支持,它是一些取消引用指针的指令不知道的东西)。 CLR 团队本可以在 JIT 中聪明一点,通过将 char 引用与固定引用相关联的分析来专门处理这种情况,但我认为这在体系结构上很奇怪,很难 explain/document,我怀疑他们走了那条路,所以我没有去看CLR代码来验证。