Csharp:Interop
Fra CodeWiki
Interop
Interop er teknikken å bruke programmer som er skrevet i et annet språk en C# med et annet ABI Dette er eksempelvis COM programmer eller Dll filer skrevet i C
DllImport attributten
Denne artikkelen vil ta for seg import av funksjoner fra biblioteker skrevet i C. Målet er å lage en veldig grunnleggende implementasjon av OpenGL i C#
.NET laster DLL filer som blir bundet på denne måten når første funksjonen blir kalt, alstå vil en ikke få en eventuell feilmelding om at dll filen mangler før første funksjonen fra den dll filen blir kalt.
Når en først skal importere funksjonene, må en ofte se på C deklerasjonen av disse, først kikker vi på funksjonene fra GDI32.dll som heter ChoosePixelFormat, SetPixelFormat, og GetDC som kreves for å initialisere vinduet vi skal tegne på.
Her er C deklerasjonen til disse funksjonene:
// C++ int ChoosePixelFormat( HDC hdc, CONST PIXELFORMATDESCRIPTOR * ppfd ); BOOL SetPixelFormat( HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR * ppfd ); HDC GetDC( HWND hWnd // handle to window );
og strukturen PIXELFORMATDESCRIPTOR:
// C++ typedef struct tagPIXELFORMATDESCRIPTOR { // pfd WORD nSize; WORD nVersion; DWORD dwFlags; BYTE iPixelType; BYTE cColorBits; BYTE cRedBits; BYTE cRedShift; BYTE cGreenBits; BYTE cGreenShift; BYTE cBlueBits; BYTE cBlueShift; BYTE cAlphaBits; BYTE cAlphaShift; BYTE cAccumBits; BYTE cAccumRedBits; BYTE cAccumGreenBits; BYTE cAccumBlueBits; BYTE cAccumAlphaBits; BYTE cDepthBits; BYTE cStencilBits; BYTE cAuxBuffers; BYTE iLayerType; BYTE bReserved; DWORD dwLayerMask; DWORD dwVisibleMask; DWORD dwDamageMask; } PIXELFORMATDESCRIPTOR;
Når vi skal oversette disse, må vi først vite litt om datatypene som brukes. Eneste måten å finne ut av dette på, er hvis en ser i dokumentene der de er definert.
| char | sbyte |
| unsigned char | byte |
| short | short |
| short int | short |
| unsigned short | ushort |
| int | int |
| unsigned int | uint |
| long int | int |
| unsigned long int | uint |
| long long | long |
| unsigned long long | ulong |
| float | float |
| double | double |
| long double | double |
| wchar_t | char |
| wchar_t* | [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string |
| char* | string |
| HWND | IntPtr |
| HDC | IntPtr |
| BYTE | byte |
| WORD | ushort |
| DWORD | uint |
| BOOL | bool |
| LPSTR | string |
| LPWSTR | [MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string |
| LPCSTR | string |
| LPVOID | void* |
Noen pekere må taes utifra sammenhengen, noen ganger skal programmet gi verdi tilbake i noen av pekerne, og da er det bedre å bruke ref eller out syntaksen i C#
Dermed har vi nok informasjon til å oversette disse funksjonene, og strukturen
// C# [System.Runtime.InteropServices.DllImport("gdi32")] public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor ppfd); [System.Runtime.InteropServices.DllImport("gdi32")] public static extern int SetPixelFormat(IntPtr hdc, int pixelformat, ref PixelFormatDescriptor ppfd); // Her bruker vi ref istedet for peker [System.Runtime.InteropServices.DllImport("user32")] public static extern IntPtr GetDC(IntPtr hwnd); public enum PixelFormatFlagsEnum : uint { /* PIXELFORMATDESCRIPTOR flags */ PFD_DOUBLEBUFFER = 0x00000001, PFD_STEREO = 0x00000002, PFD_DRAW_TO_WINDOW = 0x00000004, PFD_DRAW_TO_BITMAP = 0x00000008, PFD_SUPPORT_GDI = 0x00000010, PFD_SUPPORT_OPENGL = 0x00000020, PFD_GENERIC_FORMAT = 0x00000040, PFD_NEED_PALETTE = 0x00000080, PFD_NEED_SYSTEM_PALETTE = 0x00000100, PFD_SWAP_EXCHANGE = 0x00000200, PFD_SWAP_COPY = 0x00000400, PFD_SWAP_LAYER_BUFFERS = 0x00000800, PFD_GENERIC_ACCELERATED = 0x00001000, PFD_SUPPORT_DIRECTDRAW = 0x00002000, /* PIXELFORMATDESCRIPTOR flags for use in ChoosePixelFormat only */ PFD_DEPTH_DONTCARE = 0x20000000, PFD_DOUBLEBUFFER_DONTCARE = 0x40000000, PFD_STEREO_DONTCARE = 0x80000000 } public enum PixelTypeEnum : byte { /* pixel types */ PFD_TYPE_RGBA = 0, PFD_TYPE_COLORINDEX = 1, } public enum LayerTypeEnum : byte { PFD_MAIN_PLANE = 0, PFD_OVERLAY_PLANE = 1, PFD_UNDERLAY_PLANE = 0xff } public struct PixelFormatDescriptor { public ushort nSize; public ushort nVersion; public PixelFormatFlagsEnum dwFlags; public PixelTypeEnum iPixelType; public byte cColorBits; public byte cRedBits; public byte cRedShift; public byte cGreenBits; public byte cGreenShift; public byte cBlueBits; public byte cBlueShift; public byte cAlphaBits; public byte cAlphaShift; public byte cAccumBits; public byte cAccumRedBits; public byte cAccumGreenBits; public byte cAccumBlueBits; public byte cAccumAlphaBits; public byte cDepthBits; public byte cStencilBits; public byte cAuxBuffers; public LayerTypeEnum iLayerType; public byte bReserved; public uint dwLayerMask; public uint dwVisibleMask; public uint dwDamageMask; }
Disse kreves for å gjøre C# vinduet klar for OpenGL.
Det en gjør her, er at noen felt må fylles inn med riktig verdier, først skal vi finne det mest passende pixelformatet som vi vil bruke. Det som først må gjøres, er å deklarere en variabel som inneholder strukturen, og sette felt slik at vi får det som vi vil. Deretter finner vi det mest passende pixelformatet, og setter det til kontrollen.
// C# public bool InitOpenGLWindow(Control dst) { PixelFormatDescriptor pfd = new PixelFormatDescriptor(); pfd.iLayerType = OpenGL.LayerTypeEnum.PFD_MAIN_PLANE; pfd.nSize = 40; // Skal være sizeof(PixelFormatDescriptor) som er det samme som 40 pfd.nVersion = 1; // Må alltid være 1 pfd.iPixelType = OpenGL.PixelTypeEnum.PFD_TYPE_RGBA; pfd.cColorBits = 32; // 32 bpp fargebuffer pfd.cDepthBits = 32; // 32-bit Z-buffer pfd.dwFlags = PixelFormatFlagsEnum.PFD_DRAW_TO_WINDOW | PixelFormatFlagsEnum.PFD_SUPPORT_OPENGL | PixelFormatFlagsEnum.PFD_DOUBLEBUFFER | PixelFormatFlagsEnum.PFD_GENERIC_ACCELERATED; IntPtr hdc = GetDC(dst.Handle); int PixelFormat = ChoosePixelFormat(hdc, ref pfd); if(SetPixelFormat(hdc, PixelFormat, ref pfd) != 0) throw new System.NotSupportedException("GDI failed to initialize window"); }
Nå skal GDI gjort vinduet klart for OpenGL, da må vi importere noen funksjoner fra OpenGL32.dll
// C++ HGLRC wglCreateContext( HDC hdc ); BOOL wglMakeCurrent( HDC hdc, HGLRC hglrc );
Dette blir oversatt til C# slik:
// C# [System.Runtime.InteropServices.DllImport("OpenGL32.dll")] public static extern IntPtr wglCreateContext(IntPtr hdc); [System.Runtime.InteropServices.DllImport("OpenGL32.dll")] public static extern int wglMakeCurrent(IntPtr hdc, IntPtr hGlrc);
Da kan vi utvide funksjonen vår littegranne
// C# public bool InitOpenGLWindow(Control dst) { PixelFormatDescriptor pfd = new PixelFormatDescriptor(); pfd.iLayerType = OpenGL.LayerTypeEnum.PFD_MAIN_PLANE; pfd.nSize = 40; // Skal være sizeof(PixelFormatDescriptor) pfd.nVersion = 1; // Må alltid være 1 pfd.iPixelType = OpenGL.PixelTypeEnum.PFD_TYPE_RGBA; pfd.cColorBits = 32; // 32 bpp fargebuffer pfd.cDepthBits = 32; // 32-bit Z-buffer pfd.dwFlags = PixelFormatFlagsEnum.PFD_DRAW_TO_WINDOW | PixelFormatFlagsEnum.PFD_SUPPORT_OPENGL | PixelFormatFlagsEnum.PFD_DOUBLEBUFFER | PixelFormatFlagsEnum.PFD_GENERIC_ACCELERATED; IntPtr hdc = GetDC(dst.Handle); int PixelFormat = ChoosePixelFormat(hdc, ref pfd); if(SetPixelFormat(hdc, PixelFormat, ref pfd) != 0) throw new System.NotSupportedException("GDI failed to initialize window"); IntPtr gl_handle = wglCreateContext(hdc); wglMakeCurrent(hdc, gl_handle); }
Når denne funksjonen blir kalt, vil vinduet eller kontrollen som settes som parameter være klar for OpenGL kall.
Da kan vi kikke litt på noen OpenGL funksjoner, for å se hvordan de kan importeres riktig.
Et problem er glVertexPointer funksjonen, som tar void* som et parameter. her har en to valg: bruk generics, eller en må låse et vertex array før det kan sendes til OpenGL. Det vil være problemer forbundet med begge to, å bruke generics vil problemet være at en ikke kan bruke interleaved arrays, altså et array med mye informasjon, problemet med å låse er at arrayet kan bli flyttet før OpenGL rører arrayet (i teorien) så det beste er å faktisk låse arrayet til det ikke lenger er i bruk, eller må endres. Vi går igjennom alle tre alternativer her.
// C++ void glVertexPointer( GLint size, GLenum type, GLsizei stride, const GLvoid *pointer );
Først tar vi for oss generics, dette forutsetter at arrayet ihvertfall starter med vertex data
[DllImport("OpenGL32")] public static extern void glVertexPointer<T>(int size, uint type, int stride, T[] pointer);
Teknikken er ganske enkel, en slipper å låse arrayet direkte, og funksjonen er enkel å bruke
Deretter kikker vi på å låse arrayet med fixed Merk at vi må definere funksjonen som unsafe
[DllImport("OpenGL32")] public unsafe static extern void glVertexPointer(int size, uint type, int stride, void* pointer); public void UseVertexPointer(MyVertexData[] my_array) { fixed(MyVertexData* pointer = my_array) { glVertexPointer(3, GL_FLOAT, 0, (void*)pointer); } }
Det som er ulempen her, er at garbage collectoren i C# kan teknisk sett flytte arrays og data rundt, fixed hindrer dette innenfor fixed blokken. Dette kan fikses ved å låse arrayet med Marshal klassen
[DllImport("OpenGL32")] public unsafe static extern void glVertexPointer(int size, uint type, int stride, IntPtr pointer); public class VertexBuffer { private System.Runtime.Interop.GCHandle m_handle; private MyVertexData[] m_arr; public IntPtr Address { get { return m_handle.AddrOfPinnedObject(); } } public MyVertexData[] Array { get { return m_arr; } } public VertexBuffer(MyVertexData[] arr) { m_arr = arr; } public void Lock() { m_handle = System.Runtime.Interop.GCHandle.Alloc(m_arr, System.Runtime.InteropServices.GCHandleType.Pinned); } public void Unlock() { m_handle.Free(); } } public void AssignVertexBuffer(VertexBuffer vb) { vb.Lock(); glVertexPointer(vb.Array.Length, 3, GL_FLOAT, vb.Address); Draw(); // En eller annen tegnefunksjon vb.Unlock(); }
Vi vil ikke gå noe nærmere inn på hvordan OpenGL fungerer utover dette.
MarshalAs attributten
Denne attributten brukes i sammenheng med DllImport for å fortelle .NET hva slags datatype som er forventet inn til funksjonen. Ofte brukes dette f.eks. til funksjonspekere eller forskjellige formater av strings (LPSTR, LPCSTR, LPWSTR, LPCWSTR, BSTR osv.) eller for felt som er en array av spesifikk størrelse.
Det er en egen syntaks for å bruke MarshalAs på returverdien til en funksjon. For eksempel hvis funksjonen returnerer en LPWSTR
[return: MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr), DllImport("Foo")]string Foo()
Når en skal bruke felt som er et array av spesifikk størrelse, for eksempel i en struktur, bruker en også MarshalAs
public struct Foo { [MarshalAs(UnmanagedType.ByValArray, SizeConst=255)] int[] Foo2; }
Dette forteller .NET at dette feltet egentlig er 255 int etter hverandre.
Ofte vil .NET gjette riktig datatype som brukes, så det er kun unntaksvis en bruker MarshalAs
