StructLayout–zastosowanie

W poprzednim poście omówiłem atrybut StructLayout. Dzisiaj z kolei więcej przykładów. Głównie StructLayout wprowadzono aby móc wykonywać kod niezarządzany w .NET. Czasami wciąż zachodzi potrzeba wykorzystania niektórych funkcji z WinAPI. Cześć funkcji przyjmuje jako parametr struktury danych, które oczywiście musimy zmapować na strukturę c#. Na przykład, załóżmy, że mamy następującą strukturę:

typedef struct _DISPLAY_DEVICE {
  DWORD cb;
  TCHAR DeviceName[32];
  TCHAR DeviceString[128];
  DWORD StateFlags;
  TCHAR DeviceID[128];
  TCHAR DeviceKey[128];
} 

Naszym zadaniem jest napisanie analogicznej w c#, którą potem będziemy mogli zapisać do powyższej. W C# może to wyglądać następująco:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct DISPLAY_DEVICE
{
    public int cb;
    public fixed char DeviceName[32];
    public fixed char DeviceString[128];
    public int StateFlags;
    public fixed char DeviceID[128];
    public fixed char DeviceKey[128];
}

Słowo kluczowe fixed już omawiałem kiedyś na blogu – zachęcam do wyszukania i poczytania jeśli nie jest ono jasne. Musimy określić Pack=1 ponieważ struktura jest mapowana (marshaling) na niezarządzaną a więc  poszczególne bajty muszą się pokrywać. Na przykład, DeviceString musi zaczynać się na tym samym miejscu w c# co w CPP.

Przed c# nie było możliwe zadeklarowanie tablic w powyższy sposób (bezpośrednio w strukturze). Jedynym sposobem była deklaracja pojedynczego elementu ale z FieldOffset takim, że jest tam wystarczająca ilość pamięci na poszczególne elementy. Wtedy za pomocą wskaźnika (unsafe) można było korzystać jak z normalnej tablicy. Nie będę tego tutaj omawiać, bo od c# 2.0 można już deklarować to w powyższy sposób.

Obiekty COM to nie jedyne zastosowanie. Załóżmy, że piszemy loader jakiegoś binarnego pliku (BMP, pliki map itp). Wtedy jeśli w pliku struktura ma PACKING=1  (bo na przykład została napisana w CPP) wtedy aby ją odczytać również musimy mieć taką samą  w C#. Oczywiście mam na myśli sytuację gdy chcemy przeczytać całą strukturę za jednym razem zamiast czytania poszczególnych bajtów i następnego przydzielania ich do poszczególnych pól struktury. Pamiętajmy, że domyślne zachowanie Packing zależy od aktualnej platformy (architektury CPU). Jeśli jednak chcemy strukturę przechowywać w pliku wtedy oczekujemy takiego samego zachowania na wszystkich platformach.

Innym interesującym zastosowaniem są unie. Unia w programowaniu to struktura danych, której rozmiar jest równy maksymalnemu polu w niej. Z tego względu jednocześnie można tam umieścić wyłącznie jedną wartość – inne będą za każdym razem nadpisywane. Załóżmy, że z takiej struktury chcemy zrobić unię:

struct Sample
{
   public int ValueA;
   public int ValueB;
}

Odpalmy poniższy test aby zweryfikować nasze oczekiwania odnośnie struktury:

Sample sample=new Sample();
sample.ValueA = 1;
sample.ValueB = 2;
Debug.Assert(sample.ValueA==1);
Debug.Assert(sample.ValueB == 2);
Debug.Assert(Marshal.SizeOf(sample)==8);

Pola ValueA oraz ValueB są niezależne od siebie i mogą przechowywać jednocześnie różne wartości. Potrzeba na to 8 bajtów. Deklaracja unii z kolei wygląda następująco:

[StructLayout(LayoutKind.Explicit)]
struct Sample
{
   [FieldOffset(0)]
   public int ValueA;
   [FieldOffset(0)]
   public int ValueB;
}

ValueA i ValueB zaczynają się od tego samego miejsca w pamięci a zatem pokrywają się. Zweryfikujmy to:

Sample sample=new Sample();
sample.ValueA = 1;
Debug.Assert(sample.ValueA == 1);
Debug.Assert(sample.ValueB == 1);

sample.ValueB = 2;
Debug.Assert(sample.ValueA==2);
Debug.Assert(sample.ValueB == 2);

Debug.Assert(Marshal.SizeOf(sample)==4);

3 thoughts on “StructLayout–zastosowanie”

  1. Mogłeś wspomnieć, do czego unie mogą być przydatne. No chyba, że jest to zaplanowane na kolejny odcinek 🙂

  2. “Obiekty COM to nie jedyne zastosowanie. Załóżmy, że piszemy loader jakiegoś binarnego pliku […] Jeśli jednak chcemy strukturę przechowywać w pliku wtedy oczekujemy takiego samego zachowania na wszystkich platformach.”
    Jakiś czas temu przesyłałem dane w pewnym ustalonym formacie przez sieć i żeby było to wygodniejsze w użyciu, opakowywałem je w różne struktury właśnie. A kiedy tych danych robi się dużo, to wszelkie wyrównywanie zaczyna być lekko denerwujące, bo w końcu przesyłamy “powietrze”. Również wtedy przydatne było opakowanie struktury. Dobrze o tym wiedzieć, jeżeli mamy dużo danych, zależy nam na pamięci lub dane te w jakiś sposób opuszczają pamięć naszego programu i mogą być przeniesione do innego (np. poprzez zapis do pliku czy przesłanie przez sieć).

Leave a Reply

Your email address will not be published.