Wie kann der von Marshal.AllocHGlobal zugewiesene Speicher auf Null gesetzt werden?
-
18-09-2019 - |
Frage
Ich ordne in meiner Anwendung etwas nicht verwalteten Speicher zu Marshal.AllocHGlobal
.Anschließend kopiere ich eine Reihe von Bytes an diesen Speicherort und konvertiere das resultierende Speichersegment in ein struct
bevor Sie den Speicher über wieder freigeben Marshal.FreeHGlobal
.
Hier ist die Methode:
public static T Deserialize<T>(byte[] messageBytes, int start, int length)
where T : struct
{
if (start + length > messageBytes.Length)
throw new ArgumentOutOfRangeException();
int typeSize = Marshal.SizeOf(typeof(T));
int bytesToCopy = Math.Min(typeSize, length);
IntPtr targetBytes = Marshal.AllocHGlobal(typeSize);
Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy);
if (length < typeSize)
{
// Zero out additional bytes at the end of the struct
}
T item = (T)Marshal.PtrToStructure(targetBytes, typeof(T));
Marshal.FreeHGlobal(targetBytes);
return item;
}
Dies funktioniert jedoch größtenteils, wenn ich weniger Bytes als die Größe habe struct
erfordert, dann werden den letzten Feldern 'zufällige' Werte zugewiesen (ich verwende LayoutKind.Sequential
auf der Zielstruktur).Ich möchte diese hängenden Felder so effizient wie möglich beseitigen.
Zum Kontext deserialisiert dieser Code hochfrequente Multicast-Nachrichten, die von C++ unter Linux gesendet werden.
Hier ist ein fehlgeschlagener Testfall:
// Give only one byte, which is too few for the struct
var s3 = MessageSerializer.Deserialize<S3>(new[] { (byte)0x21 });
Assert.AreEqual(0x21, s3.Byte);
Assert.AreEqual(0x0000, s3.Int); // hanging field should be zero, but isn't
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct S3
{
public byte Byte;
public int Int;
}
Das wiederholte Ausführen dieses Tests führt dazu, dass die zweite Behauptung jedes Mal mit einem anderen Wert fehlschlägt.
BEARBEITEN
Am Ende habe ich verwendet Leppies Vorschlag des Gehens unsafe
und nutzen stackalloc
.Dadurch wurde ein Byte-Array zugewiesen, das nach Bedarf auf Null gesetzt wurde, und der Durchsatz wurde je nach Nachrichtengröße um 50 % bis 100 % verbessert (größere Nachrichten hatten einen größeren Nutzen).
Die endgültige Methode ähnelte letztendlich der folgenden:
public static T Deserialize<T>(byte[] messageBytes, int startIndex, int length)
where T : struct
{
if (length <= 0)
throw new ArgumentOutOfRangeException("length", length, "Must be greater than zero.");
if (startIndex < 0)
throw new ArgumentOutOfRangeException("startIndex", startIndex, "Must be greater than or equal to zero.");
if (startIndex + length > messageBytes.Length)
throw new ArgumentOutOfRangeException("length", length, "startIndex + length must be <= messageBytes.Length");
int typeSize = Marshal.SizeOf(typeof(T));
unsafe
{
byte* basePtr = stackalloc byte[typeSize];
byte* b = basePtr;
int end = startIndex + Math.Min(length, typeSize);
for (int srcPos = startIndex; srcPos < end; srcPos++)
*b++ = messageBytes[srcPos];
return (T)Marshal.PtrToStructure(new IntPtr(basePtr), typeof(T));
}
}
Leider ist hierfür immer noch ein Anruf erforderlich Marshal.PtrToStructure
um die Bytes in den Zieltyp zu konvertieren.
Lösung
Warum nicht einfach prüfen, ob start + length
ist drinnen typesize
?
Übrigens:Ich würde einfach gehen unsafe
hier und verwenden Sie eine for-Schleife, um den zusätzlichen Speicher auf Null zu setzen.
Auch das bringt Ihnen Vorteile stackalloc
Das ist viel sicherer und schneller als AllocGlobal
.
Andere Tipps
[DllImport("kernel32.dll")]
static extern void RtlZeroMemory(IntPtr dst, int length);
...
RtlZeroMemory(targetBytes, typeSize);
Dies wird unter Windows gut funktionieren:
namespace KernelPInvoke
{
/// <summary>
/// Implements some of the C functions declared in string.h
/// </summary>
public static class MemoryWrapper
{
[DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);
[DllImport("kernel32.dll", EntryPoint = "MoveMemory", SetLastError = false)]
static extern void MoveMemory(IntPtr destination, IntPtr source, uint length);
[DllImport("kernel32.dll", EntryPoint = "RtlFillMemory", SetLastError = false)]
static extern void FillMemory(IntPtr destination, uint length, byte fill);
}
var ptr = Marshal.AllocHGlobal(size);
try
{
MemoryWrapper.FillMemory(ptr, size, 0);
// further work...
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
Ja als Jon Seigel sagte, Sie können es mit Marshal.WriteByte auf Null setzen
Im folgenden Beispiel setze ich den Puffer auf Null, bevor ich die Struktur kopiere.
if (start + length > messageBytes.Length)
throw new ArgumentOutOfRangeException();
int typeSize = Marshal.SizeOf(typeof(T));
int bytesToCopy = Math.Min(typeSize, length);
IntPtr targetBytes = Marshal.AllocHGlobal(typeSize);
//zero out buffer
for(int i=0; i < typeSize; i++)
{
Marshal.WriteByte(targetBytes, i, 0);
}
Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy);
Ich habe so etwas noch nie in C# gemacht, aber ich habe Marshal.WriteByte(IntPtr, Int32, Byte) in MSDN gefunden.Probieren Sie das aus.
for(int i=0; i < buffSize / 8; i += 8 )
{
Marshal.WriteInt64(buffer, i, 0x00);
}
for(int i= buffSize % 8 ; i < -1 ; i-- )
{
Marshal.WriteByte (buffer, buffSize - i, 0x00);
}
Ich denke, Sie werden feststellen, dass es mit 64-Bit-Wrights um ein Vielfaches schneller geht als mit 8-Bit-Wrights (die Sie noch für die letzten paar Bytes benötigen).
Ich denke, der beste Weg, einen Puffer auf Null zu setzen, ist dieser, wenn Sie nicht in die andere Richtung gehen möchten oder können:
for(int i=0; i<buffSize; i++)
{
Marshal.WriteByte(buffer, i, 0x00);
}