Мне нужно вызвать API Win32, который выдает строку с нулевым завершением в буфер, а затем читает эту строку из буфера.
Недавно я узнал о Span<T> и пытаюсь понять, следует ли мне его использовать или делать что-то традиционным способом, используя класс Marshal.
Ниже вы увидите два разных способа достижения одной и той же цели: один с помощью Span<T>, другой с помощью Marshal.
Я хочу знать, разумно ли я здесь использую Span<T>? И будет ли производительность такой же или лучше при использовании Span<T>?
И в качестве примечания: существует ли «более правильный» способ извлечь строку с нулевым завершением из Span<byte>, или я это делаю нормально?
Использование Маршала:
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetProductString(IntPtr HidDeviceObject, IntPtr Buffer, uint BufferLength);
public static string GetProductNameUsingMarshal(IntPtr handle)
{
var stringPtr = Marshal.AllocHGlobal(254);
try
{
if (!Win32.HidD_GetProductString(handle, stringPtr, 254)
{
throw new Win32Exception(Marshal.GetLastPInvokeError(), Marshal.GetLastPInvokeErrorMessage());
}
return Marshal.PtrToStringUni(stringPtr);
}
finally
{
Marshal.FreeHGlobal(stringPtr);
}
}
Использование диапазона:
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetProductString(IntPtr HidDeviceObject, ref byte Buffer, uint BufferLength);
public static string GetProductNameUsingSpan(IntPtr handle)
{
var buffer = new Span<byte>(new byte[254]);
var success = Win32.HidD_GetProductString(handle, ref MemoryMarshal.GetReference(buffer), 254);
if (!success)
{
throw new Win32Exception(Marshal.GetLastPInvokeError(), Marshal.GetLastPInvokeErrorMessage());
}
var charSpan = MemoryMarshal.Cast<byte, char>(buffer);
return new string(charSpan.Slice(0, charSpan.IndexOf(char.MinValue)));
}
[DllImport("hid.dll", SetLastError = true)] public static extern bool HidD_GetProductString(IntPtr HidDeviceObject, [Out] out string Buffer, uint BufferLength); вар успех = Win32.HidD_GetProductString (дескриптор, выход вар thestring, 254); System.AccessViolationException: 'Попытка чтения или записи защищенной памяти. Часто это указывает на то, что другая память повреждена».
[Out] string Buffer, а не [Out] out string Buffer.
Сначала попробовал, тот же результат.
Вы выделили ли вы память в этой строке перед вызовом (сделали ее длиной 254 символа)? Альтернативно, StringBuilder Buffer и передайте StringBuilder с Capacity из 254.





Между ними нет большой разницы. Marshal старше, но в некотором смысле безопаснее для PInvoke, поскольку не мешает управляемой куче. Span может быть лучше во многих отношениях, поскольку позволяет напрямую обрабатывать буферы без копирования и назначать их обычному byte[] массиву в управляемой куче.
Что бы вы ни делали, не приводите ref к указателю *, иначе вы получите дыру в GC (GC не может отслеживать указатели * и может переместить или перезаписать буфер).
Но и то, и другое ненужно. Вы можете передать буфер byte[] напрямую.
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetProductString(IntPtr HidDeviceObject, [Out] byte[] Buffer, uint BufferLength);
public static string GetProductNameUsingSpan(IntPtr handle)
{
var buffer = new byte[254];
var success = Win32.HidD_GetProductString(handle, buffer, buffer.Length);
if (!success)
{
throw new Win32Exception(Marshal.GetLastPInvokeError(), Marshal.GetLastPInvokeErrorMessage());
}
var charSpan = MemoryMarshal.Cast<byte, char>(buffer.AsSpan());
return new string(charSpan.Slice(0, charSpan.IndexOf(char.MinValue)));
}
Обратите внимание, что 254 — это количество байтов, вы получаете только половину символов UTF16 (и вычтите 1 для терминатора NUL).
Для других функций, возвращающих ANSI, прямое приведение к char совершенно неверно. Вам нужно использовать Encoding.ASCII.GetString, чтобы перекодировать его.
Почему вы делаете все это вместо того, чтобы поставить
[Out] string Buffer?