Csharp:Interop

Fra CodeWiki

Gå til: navigasjon, søk

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.

C datatype cheat sheet
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

dataprogrammering
generelt