Реализация 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
}
}
1 ответ
На этом этапе нет необходимости фиксировать строку или указатели, и на самом деле это было бы концептуально некорректно. После вашей нулевой проверки / возврата (которая также должна проверять длину 0), вы можете сделать это:
var strSpan = _String.AsSpan();
return ref strSpan[_Index];
Это, как указывает имя функции, возвращает закрепляемую ссылку на первый символ базовой строки. То, что вы получили выше, возвращает ссылку на первый разыменованный элемент закрепленного указателя на нижележащую строку, но время жизни закрепления ограничено областью действия фиксированного оператора (это может быть неправильно, но я не думаю, что возвращенный реф держит его приколол). Использование ссылки на элемент Span позволяет избежать ненужного закрепления.
Обновление - я немного больше посмотрел на закрепленные указатели (куча бесполезных поисков в Google - вернулся к декомпиляции / экспериментам). Оказывается, возвращение ссылки на элемент закрепленного указателя не сохраняет этот вывод. Возвращенная ссылка является ссылкой на разрешенный адрес - весь контекст закрепления потерян.
Я думал, может быть, меняется _String
к Memory<char>
(использование AsMemory
расширение в конструкторе) может работать. Тогда ваш GetPinnableReference
мог просто return ref _String[0];
но AsMemory
дает вам доступный только для чтения объект памяти (не может вернуть доступную для записи ссылку).
Не хватает реализации IPinnable
с GCHandle
а также IDisposable
или же MemoryManager<T>
(тяжелее, чем вы хотите наверняка), я думаю, что мой Span<char>
Решение, приведенное выше, может быть наиболее безопасным / правильным способом для достижения этой цели.
Вот разборка GetPinnableReference (не обращайте внимания на мое пространство имен PowerPC.Test - я делаю двоичный дизассемблер / эмулятор / отладчик PowerPC.NET Standard 1.1 и использую эти концепции, поэтому я заинтересован):
.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_0
, string pinned V_1
, а также char& V_2
, Для меня это проливает немного света на то, почему фиксированный синтаксис такой, какой он есть. У вас есть фиксированное ключевое слово, представленное string pinned V_1
, указатель на символ (из вашего кода), представленный char* V_0
и возвращаемая ссылка представлена char& V_2
, Три отдельные концепции представлены тремя отдельными конструкциями кода.
Итак, ваш фиксированный оператор закрепляет строку и назначает ссылку закрепления V_1
в IL_0002
в IL_0007
затем загружается V_1
преобразует его в указатель (преобразование в целое число без знака) и сохраняет его в V_0
(ваш char* p
) в IL_0008
в IL_000a
наконец то загружается V_0
, вычисляет смещение char
определяется _index
и сохраняет его обратно в V_0
в IL_000e
в IL_0015
, В рамках фиксированного оператора он загружает V_0, преобразует его в ссылку через Unsave.AsRef<char>
и хранит его в V_2
(ссылка, которая будет возвращена) в IL_0017
в IL_001d
, Я не уверен, почему компилятор C# делает это, но последний бит из IL_001e
в IL_0021
часто, как компилятор обрабатывает возвращаемое значение; он вставляет ветвь в блок, который будет возвращать значение (даже если в этом случае это следующая инструкция), затем он загружает возвращаемое значение из локальной переменной (которую он создал и IMO необязательно назначается, но, возможно, есть логичное объяснение этому?) и, наконец, возвращает результат, который вы получили несколько инструкций назад.
Вот вам ответ, наконец-то! Невозможно сохранить закрепление от вашего фиксированного оператора, потому что он разыменовывает неуправляемый указатель V_0
/p
в данном случае это просто подтверждается закреплением ссылки, V_1
, что является инструкцией по разыменованию указателя, о котором не знают). Команда CLR могла бы быть умнее в JIT и обработать этот случай специально с помощью анализа, связав ссылку на символ с закрепленной ссылкой, но я думаю, что это было бы странно с архитектурной точки зрения, трудно объяснить / документировать, и я сомневаюсь, что они пошли так маршрут, поэтому я не пошел смотреть код CLR, чтобы проверить.