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);
Mogłeś wspomnieć, do czego unie mogą być przydatne. No chyba, że jest to zaplanowane na kolejny odcinek 🙂
Dokladnie:)
“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ć).