Externe dlls von C# aus aufrufen

Externe dlls von C# aus aufrufen

Wer unter Windows mit C# programmiert kommt irgendwann an die Grenzen von .net – wo Microsoft aufgehört hat, Funktionalität bereitzustellen.

Nach und nach werden immer mehr Funktionen bereitgestellt, teilweise auch solche, die man nie vermutet hätte, aber.. für viele interessante Dinge muß man nach wie vor externen Code aufrufen, wo dem Programmierer lediglich der Name der .dll sowie der Name der Funktion (und natürlich die Parameter) bekannt sind.

(Diese werden dann wie zu guten alten C-Zeiten dynamisch zur Laufzeit geladen (im Gegensatz zur statischen Bindung) – Fehler zeigen sich also meistens erst zur Laufzeit, vorher werden lediglich Unstimmigkeiten zwischen Definition und Aufruf erkannt, aber z. B. keine falsche Definition oder eine fehlende .dll).

Eine sehr umfangreiche Sammlung von Signaturen

In dem Zusammenhang möchte ich erstmal auf eine sehr umfangreiche Sammlung von Beschreibungen so ziemlich jeder externen Funktion, die evtl. irgendeinmal gebrauchen werden könnte, aufmerksam machen. Diese findet sich bei Pinvoke.net. Dort findet sich für eine Reihe von Windowsfunktionen die passende Signatur (Parameter sowie Aufrufkonventionen) für die Benutzung in C#, oder auch in VB.net. Passend gibt es auch manchmal einen kleinen Schnipsel Code, der die Einbindung noch einfacher macht. Damit steht dem systemweiten Keyboard- oder Maushook nichts mehr im Weg.

Char* und Stringbuilder

Bei genauem Nachdenken offensichtlich, aber für den einen oder anderen evtl. doch nützlich: wie werden C-style char* oder auch char[] Parameter aufgerufen? .net stellt hierfür den Stringbuilder zur Verfügung. Hier sollte man darauf achten, daß Stringbuilder ebenso wie char* stets eine Referenz darstellt. Ein ohne weitere Parameter übergebener Stringbuilder ist somit bereits ein Ein-/Ausgabewert. Sollte er zur Ausgabe benötigt werden, ist somit ein extra out unnötig, und verursacht sogar noch Probleme, wird hier doch noch einmal zusätzlich dereferenziert, und die erwarteten Referenzen passen nicht mehr.

Weiter wichtig ist die Initialisierung des Stringbuilders, sollte er Ergebnisse aufnehmen müssen. (Zu C-Zeiten waren Strings einfach Zeichenketten – ohne die Möglichkeit, die Zeichenkette zu verlängern oder zu verkürzen, worüber man heute vermutlich kaum noch nachdenkt.) Hier sollte man einen Buffer allokieren, der groß genug ist, um das Ergebnis aufzunehmen. In fast allen Fällen reichen hier ca. 300 Zeichen (> MAX_PATH), wenn sonst nichts erwähnt wird. Dieser sollte über die Length Eigenschaft gesetzt werden. Siehe dazu auch die Dokumentation von MS.

Sollte der Parameter übrigens ausschließlich zur Eingabe gedacht sein, reicht auch ein simpler String.

Besonderheiten beim Marshalling

Interessant sind manchmal noch die Feinheiten beim Aufruf. So lassen sich insbesondere noch die Aufrufvarianten stdcall oder cdecl auswählen sowie die Art, wie Text extern behandelt wird: handelt es sich um Multibyte (ANSI) Charakter oder um Unicode-Zeichen?

Eine bekannte (?) Funktion von damals, wo man vor diese Auswahl gestellt wird, ist die MessageBox Funktion, die es so eigentlich gar nicht gibt – je nachdem, in welchem Umfeld man diese verwendet, wird entweder die MessageBoxA-(Ansi)-Variante oder auch die MessageBoxW-(Unicode/Widechar)-Variante verwendet. Die entsprechenden Parameter sieht man beispielhaft (für andere Funktionen) bei pinvoke.net hier:
Beispiel mit Unicode

1
2
3
4
5
6
7
8
9
10
[DllImport("urlmon.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false)]
    static extern int FindMimeFromData(IntPtr pBC,
          [MarshalAs(UnmanagedType.LPWStr)] string pwzUrl,
         [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.I1, SizeParamIndex=3)]
        byte[] pBuffer,
          int cbSize,
             [MarshalAs(UnmanagedType.LPWStr)]  string pwzMimeProposed,
          int dwMimeFlags,
          out IntPtr ppwzMimeOut,
          int dwReserved);

Beispiel mit Ansi

1
2
[DllImport("gsdll32.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    private static extern void gsapi_delete_instance(System.IntPtr pinstance);

cdecl wird man in den wenigsten Fällen brauchen, lediglich, wenn es in der Schnittstellenbeschreibung steht, sollte man an diesem Parameter drehen (default: stdcall). Hiermit wird festgelegt, wer den (Aufruf)Stack abräumt. Wird dieser entweder doppelt oder gar nicht aufgeräumt, merkt man das sehr schnell an einer Exception. Insofern sollte man an diesem Parameter besser nicht drehen.

Diese Parameter beeinflussen ausschließlich, was hinter den Kulissen geschieht. Man bekommt die Funktionalität im Wesentlichen dann mit, wenn etwas nicht funktioniert, und man anfängt, hier nach Fehlern zu suchen.

int und IntPtr

int sowie IntPtr (und oft auch ein handle) sind in den meisten Fällen gleich – unter C gab es ja auch – ist der Code ersteinmal übersetzt – keinen Unterschied zwischen einem Integer oder einem Pointer. Was dann zu der beliebten, mächtigen Pointerarithmetik geführt hat. (Irgendwie haben wohl zu viele Leute zu viele Probleme damit gehabt, daher gibt es das in .net so gut wie gar nicht mehr.)

Aufpassen muß man hier nur (wie auch früher) in Bezug auf die Anzahl der Bytes. Diese Werte können sich für 32bit sowie 64bit Systeme unterscheiden. Ich habe noch nicht viel Erfahrung mit 64bit – evtl. sind hier sowohl int wie auch IntPtr 64-bittig. Ob dann die Win32-Funktionen aber auch plötzlich 64bittig sein sollten weiß ich nicht – in der Doku bei MS habe ich jedenfalls so etwas noch nicht gesehen und würde wohl auch zu gewissen Problemen führen.

Fazit

Durch die Möglichkeit, auch Win32-Funktionen (sowie beliebige andere, in C/C++, Delphi etc. geschriebene .dlls) zu benutzen, läßt sich .net noch deutlich erweitern. Dies gilt insbesondere in Bereichen, wo es ursprünglich nur bedingt für gedacht ist, wie z. B. alles, was etwas näher am System dran war (Keyboard-Hooks, Abfrage von Hardware, …).

One thought on “Externe dlls von C# aus aufrufen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.