首页 > 解决方案 > 固定底层字符串的 GetPinnableReference 实现

问题描述

我正在尝试实现 C# 7.3 中引入的新模式,该模式支持使用fixed语句固定自定义类型。请参阅文档上的文章

然而,我担心在下面的代码中我返回一个指向字符串的指针,然后离开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
  }
}

标签: .netpointersunsafec#-7.3

解决方案


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

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

正如函数名所指定的,这将返回对底层字符串第一个字符的可固定引用。你上面得到的返回一个对指向底层字符串的固定指针的第一个取消引用元素的引用,但固定的生命周期仅限于固定语句的范围(可能是错误的,但我不认为返回的 ref 将其固定)。对 Span 元素使用 ref 可以避免不必要的固定。

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

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

IPinnable没有用GCHandleand IDisposableor实现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 的指针(来自您的代码)char* V_0,以及由 表示的返回的引用char& V_2。由三个独立的代码结构表示的三个独立的概念。

因此,您的固定语句固定字符串并将固定引用分配给V_1at IL_0002IL_0007然后加载V_1将其转换为指针(转换为无符号整数)并将其存储在V_0(您的char* p) at IL_0008to 中IL_000a,最后加载V_0,计算偏移量char由 指定_index,并将其存储回V_0atIL_000eIL_0015。在固定语句的范围内,它加载 V_0,将其转换为引用 via Unsave.AsRef<char>,并将其存储在V_2(将返回的引用)at IL_0017to 中IL_001d。我不确定为什么 C# 编译器会这样做,但最后一点IL_001eIL_0021通常是编译器处理返回值的方式;它向将返回值的块插入一个分支(即使在这种情况下它是下一条指令),然后它从局部变量(它创建并且 IMO 不必要地分配给它)加载返回值,但也许有对此有一个合乎逻辑的解释吗?),最后返回您在几条指令前获得的结果。

终于有答案了!无法维护您的固定语句中的固定,因为它正在取消引用非托管指针V_0/p在这种情况下恰好由固定引用支持V_1,这是取消引用指针的指令不知道的内容)。CLR 团队可能在 JIT 中很聪明,并通过将 char 引用与固定引用相关联来专门通过分析来处理该案例,但我认为这在架构上会很奇怪,难以解释/记录,我怀疑他们这样做了route,所以我没有去看CLR代码验证。


推荐阅读