C# Возврат указателя, созданного с помощью stackalloc внутри функции

У меня есть код C#, который взаимодействует с кодом C++, выполняющим операции со строками.

У меня есть этот фрагмент кода в статическом вспомогательном классе:

internal static unsafe byte* GetConstNullTerminated(string text, Encoding encoding)
{
    int charCount = text.Length;
    fixed (char* chars = text)
    {
        int byteCount = encoding.GetByteCount(chars, charCount);
        byte* bytes = stackalloc byte[byteCount + 1];
        encoding.GetBytes(chars, charCount, bytes, byteCount);
        *(bytes + byteCount) = 0;
        return bytes;
    }
}

Как видите, он возвращает указатель на байты, созданные с помощью ключевого слова stackalloc.
Однако из Спецификации C# 18.8:

Все выделенные в стеке блоки памяти, созданные во время выполнения члена функции, автоматически отбрасываются, когда этот член функции возвращается.

Означает ли это, что указатель действительно недействителен, как только метод возвращается?

Текущее использование метода:

byte* bytes = StringHelper.GetConstNullTerminated(value ?? string.Empty, Encoding);
DirectFunction(NativeMethods.SCI_SETTEXT, UIntPtr.Zero, (IntPtr) bytes);

Следует ли изменить код на

...
int byteCount = encoding.GetByteCount(chars, charCount);
byte[] byteArray = new byte[byteCount + 1];
fixed (byte* bytes = byteArray)
{
    encoding.GetBytes(chars, charCount, bytes, byteCount);
    *(bytes + byteCount) = 0;
}
return byteArray;

И снова использовать fixed для возвращаемого массива, чтобы передать указатель на метод DirectFunction?

Я пытаюсь свести к минимуму количество использований fixed (включая операторы fixed в других перегрузках GetByteCount() и GetBytes() из Encoding).

tl;dr

  1. Является ли указатель недействительным, как только метод возвращается? Является ли он недействительным на момент передачи DirectFunction()?

  2. Если да, то как лучше всего использовать наименьшее количество операторов fixed для выполнения задачи?


person A. A. Ron    schedule 06.05.2017    source источник


Ответы (2)


Означает ли это, что указатель действительно недействителен, как только метод возвращается?

Да, технически он недействителен, хотя почти наверняка не будет обнаружен. Этот сценарий вызывается самостоятельно через unsafe. Любое действие с этой памятью теперь имеет неопределенное поведение. Все, что вы делаете, но в частности вызов методов, может случайным образом перезаписать эту память — или нет — в зависимости от относительных размеров и глубины кадра стека.

Этот сценарий является одним из тех, на которые нацелены предполагаемые будущие изменения ref, что означает: разрешение stackalloc на ref (а не указатель), при этом компилятор знает, что это тип, ссылающийся на стек, ref или ref-подобный тип, и тем самым запрещая ref-возврат этого значения.

В конце концов, в тот момент, когда вы набираете unsafe, вы говорите: «Я беру на себя полную ответственность, если что-то пойдет не так». В данном случае это действительно неправильно.


Допустимо использование указателя перед выходом из метода, поэтому одним из жизнеспособных подходов может быть (при условии, что вам нужен достаточно универсальный API) позволить вызывающей стороне передать делегат или интерфейс, который указывает, что вызывающая сторона хочет, чтобы вы делали с указателем, т.е.

StringHelper.GetConstNullTerminated(value ?? string.Empty, Encoding,
    ptr => DirectFunction(NativeMethods.SCI_SETTEXT, UIntPtr.Zero, (IntPtr) ptr));

с:

unsafe delegate void PointerAction(byte* ptr);
internal static unsafe void GetConstNullTerminated(string text, Encoding encoding,
    PointerAction action)
{
    int charCount = text.Length;
    fixed (char* chars = text)
    {
        int byteCount = encoding.GetByteCount(chars, charCount);
        byte* bytes = stackalloc byte[byteCount + 1];
        encoding.GetBytes(chars, charCount, bytes, byteCount);
        *(bytes + byteCount) = 0;
        action(bytes);
    }
}

Также обратите внимание, что очень большие строки могут привести к переполнению стека.

person Marc Gravell    schedule 06.05.2017
comment
Никогда не думал о передаче делегата. Это решение будет таким же быстрым, как ручное копирование-вставка-встраивание GetConstNullTerminated в места назначения, если нет снижения производительности при использовании делегатов. Теперь мне нужно провести небольшое исследование производительности делегатов... - person A. A. Ron; 06.05.2017
comment
Производительность делегата @A.A.Ron будет совершенно неизмеримой по сравнению с работой по кодированию и двойным обходом входной строки (один раз для длины, один раз для кодирования) - person Marc Gravell; 06.05.2017
comment
Действительно ли делегаты улучшат производительность по сравнению с простым выделением памяти в куче? Делегат по-прежнему требует выделения кучи. В зависимости от размера передаваемых строк вы можете легко выделить больше памяти для хранения делегата, чем фактической строки. - person Jarra McIntyre; 07.05.2017
comment
@Jarra Показанная лямбда не захватывает никаких переменных и, как таковая, фактически поддерживается статическим полем, в котором хранится повторно используемый экземпляр делегата, поэтому после первого вызова нет постоянных затрат на выделение. В более общем случае экземпляр делегата невелик — концептуально просто целевой экземпляр и указатель метода — по сути, 16 байт на x64 (плюс заголовок объекта, плюс некоторые другие накладные расходы) — не дорого, но, конечно, нулевое выделение (как показано) дешевле. - person Marc Gravell; 07.05.2017

stackalloc приводит к выделению памяти в стеке. Стек автоматически раскручивается, когда функция возвращается. C# защищает вас от создания зависающего указателя, не позволяя вам вернуть указатель, так как нет возможности, чтобы память оставалась действительной после раскручивания стека при возврате функции.

Если вы хотите, чтобы память жила за пределами функции, выделяющей ее, вы не можете выделять ее в стеке. Вы должны выделить в куче через new.

person Jarra McIntyre    schedule 06.05.2017
comment
C# защищает вас от создания зависшего указателя, не позволяя вернуть указатель C# вообще не жалуется, и код компилируется без проблем. - person A. A. Ron; 06.05.2017
comment
Мои извинения. Я думал, что в C# есть какой-то базовый анализ побега, чтобы ловить такие случаи. - person Jarra McIntyre; 07.05.2017