Mit C# arbeiten - Verwenden von Win32 und anderen Bibliotheken

Veröffentlicht: 09. Dez 2002 | Aktualisiert: 21. Jun 2004

Von Eric Gunnerson

In meinem letzten Artikel habe ich Ihnen einen Überblick über die verschiedenen Möglichkeiten der Verwendung von vorhandenem Code in C# vermittelt. Dieses Mal stürzen wir uns auf die Verwendung von Win32® und anderen vorhandenen Bibliotheken in unserem Code.

* * *

Auf dieser Seite

Ein einfaches Beispiel Ein einfaches Beispiel
Enumerationen und Konstanten Enumerationen und Konstanten
Arbeiten mit Strukturen Arbeiten mit Strukturen
Zeichenfolgen Zeichenfolgen
Einfache Zeichenfolgen Einfache Zeichenfolgen
Zeichenfolgenpuffer Zeichenfolgenpuffer
Strukturen mit eingebetteten Zeichenfolgenarrays Strukturen mit eingebetteten Zeichenfolgenarrays
Funktionen mit Rückrufen Funktionen mit Rückrufen
Erweiterte Funktionen Erweiterte Funktionen
Andere Optionen für die Attribute Andere Optionen für die Attribute
Laden aus unterschiedlichen Speicherorten Laden aus unterschiedlichen Speicherorten
Nächster Monat  Nächster Monat

Downloaden Sie "csharp09192002_sample.exe" vom MSDN Code Center (in Englisch).

Zwei häufige Fragen von C#-Benutzern lauten: "Wieso muss ich speziellen Code schreiben, um Funktionen zu verwenden, die in Windows® integriert sind? Warum kann das Framework mir das nicht abnehmen?" Während der Entwicklung von .NET machte sich das Frameworks-Team an die Bereitstellung von Win32 für .NET-Programmierer und musste schnell feststellen, dass der Win32-API-Satz *riesig* ist. Da das Team nicht über genügend Ressourcen für das Codieren, Testen und Dokumentieren verwalteter Schnittstellen für die gesamte Win32-API verfügte, mussten Prioritäten gesetzt werden. Deshalb konzentrierte man sich auf die wichtigsten Schnittstellen. Viele gängige Operationen besitzen verwaltete Schnittstellen, jedoch werden keine vollständigen Bereiche von Win32 abgedeckt.

Plattformstart bzw. Platform Invoke (P/Invoke) ist die gängigste Methode hierfür. Für die Verwendung von P/Invoke schreiben Sie einen Prototyp, der beschreibt, wie die Funktion aufgerufen werden soll. Die Laufzeit verwendet diese Informationen anschließend, um den Aufruf auszuführen. Die andere Möglichkeit besteht darin, die Funktionen über die verwalteten Erweiterungen für C++ zu wrappen. Dies werde ich jedoch in einem späteren Artikel behandeln.

Die Vorgehensweise lässt sich am besten anhand eines Beispiels verdeutlichen. In einigen Fällen präsentiere ich Ihnen nur einen Teil des Codes. Der gesamte Code ist über den Download verfügbar.

Ein einfaches Beispiel

In unserem ersten Beispiel rufen wir die Beep()-API auf, um einen Sound zu erzeugen. Zunächst muss ich die richtige Definition für Beep() schreiben. Wenn ich mir die MSDN-Definition ansehe, stelle ich fest, dass folgender Prototyp gilt:

BOOL Beep(
  DWORD dwFreq,      // Soundfrequenz
  DWORD dwDuration   // Sounddauer
);

Um dies in C# zu schreiben, muss ich die Win32-Typen in entsprechende C#-Typen übersetzen. Da DWORD eine 4-Byte-Ganzzahl (Integer) ist, würden wir entweder int oder uint als C#-Entsprechung verwenden. Da int ein CLS-kompatibler Typ ist (und somit von allen .NET-Sprachen verwendet werden kann), wird diese Version häufiger verwendet als uint. In den meisten Fällen ist der Unterschied nicht relevant. Der bool-Typ entspricht BOOL. Daher können wir in C# den folgenden Prototyp schreiben:

public static extern bool Beep(int frequency, int duration);

Dies ist weitgehend eine Standarddefinition. Wir haben jedoch extern verwendet, um darauf hinzuweisen, dass der eigentliche Code für diese Funktion sich an anderer Stelle befindet. Dieser Prototyp teilt der Laufzeit mit, wie die Funktion aufgerufen werden soll. Nun müssen wir angeben, wo die Funktion zu finden ist.

Dazu müssen wir erneut MSDN aufsuchen. In den Referenzinformationen entdecken wir, dass Beep() in kernel32.lib definiert ist. Das heißt, der Laufzeitcode ist in der Datei kernel32.dll enthalten. Wir versehen unseren Prototypen mit einem DllImport-Attribut, um dies der Laufzeit mitzuteilen:

[DllImport("kernel32.dll")]

Das war schon alles. Hier nun ein vollständiges Beispiel, das die Art von schrägen Sounds erzeugt, die in den 60er Jahren in schlechten Science Fiction Filmen gang und gäbe waren.

using System;
using System.Runtime.InteropServices;
namespace Beep
{
   class Class1
   {
      [DllImport("kernel32.dll")]
      public static extern bool Beep(int frequency, int duration);
      static void Main(string[] args)
      {
         Random random = new Random();
         for (int i = 0; i < 10000; i++)
         {
            Beep(random.Next(10000), 100);
         }
      }
   }
}

Das wird garantiert alle in Hörweite in den Wahnsinn treiben. Da Sie mit DllImport willkürlichen Code in Win32 aufrufen können, besteht die Möglichkeit, dass fehlerhafter bzw. böswilliger Code entsteht. Daher erfordert die Laufzeit für das Durchführen von P/Invoke-Aufrufen, dass Sie ein voll vertrauenswürdiger Benutzer sind.

 

Enumerationen und Konstanten

Beep() ist für die Wiedergabe eines willkürlichen Sounds geeignet. Doch mitunter wollen wir den Sound wiedergeben, der einem bestimmten Soundtyp zugeordnet ist. Daher verwenden wir stattdessen MessageBeep(). MSDN bietet den folgenden Prototyp:

BOOL MessageBeep(
  UINT uType   // Soundtyp
);

Das sieht einfach aus. Beim Lesen der Anmerkungen ergeben sich jedoch zwei interessante Fakten.

Zunächst verwendet der uType-Parameter einen Satz vordefinierter Konstanten.

Zweitens ist einer der möglichen Parameterwerte -1. Das heißt, der Parameter ist zwar für die Verarbeitung von uint definiert, int ist jedoch geeigneter.
Die Verwendung einer Enumeration ist die logische Vorgehensweise für uType-Parameter. MSDN führt die benannten Konstanten auf, gibt jedoch keinen Hinweis auf die Werte. Dazu müssen wir uns die APIs ansehen.

Wenn Sie Visual Studio® und C++ installiert haben, finden Sie das Plattform-SDK unter \Programme\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include.

Um die Konstanten zu finden, habe ich in diesem Verzeichnis einfach findstr ausgeführt:

findstr "MB_ICONHAND" *.h

Ich habe die Konstanten in winuser.h gefunden und für die Erstellung meiner Enumeration und meines Prototyps verwendet:

public enum BeepType
{
   SimpleBeep = -1,
   IconAsterisk = 0x00000040,
   IconExclamation = 0x00000030,
   IconHand = 0x00000010,
   IconQuestion = 0x00000020,
   Ok = 0x00000000,
}
[DllImport("user32.dll")]
public static extern bool MessageBeep(BeepType beepType);

Dies kann ich nun wie folgt aufrufen:

MessageBeep(BeepType.IconQuestion);

 

Arbeiten mit Strukturen

Ich wollte schon öfter den Akkustatus meines Laptops ermitteln. Win32 bietet Energieverwaltungsfunktionen für das Abrufen dieser Informationen.
Eine Suche im MSDN führt schnell zu der GetSystemPowerStatus()-Funktion.

BOOL GetSystemPowerStatus(
  LPSYSTEM_POWER_STATUS lpSystemPowerStatus
);

Diese Funktion verarbeitet einen Zeiger auf eine Struktur. Darauf sind wir bisher noch nicht eingegangen. Für das Arbeiten mit Strukturen müssen wir eine Struktur in C# definieren. Wir beginnen mit der nicht verwalteten Definition:

typedef struct _SYSTEM_POWER_STATUS {
  BYTE   ACLineStatus;         
  BYTE   BatteryFlag;          
  BYTE   BatteryLifePercent;   
  BYTE   Reserved1;            
  DWORD  BatteryLifeTime;      
  DWORD  BatteryFullLifeTime;  
} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;

Anschließend schreiben wir eine C#-Version, indem wir die C-Typen durch ihre C#-Entsprechungen ersetzen.

struct SystemPowerStatus
{
   byte ACLineStatus;
   byte batteryFlag;
   byte batteryLifePercent;
   byte reserved1;
   int  batteryLifeTime;
   int  batteryFullLifeTime;
}

Nun können wir einfach den C#-Prototyp schreiben:

[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus(
   ref SystemPowerStatus systemPowerStatus);

In diesem Prototyp verwenden wir "ref", um anzugeben, dass wir einen Zeiger an die Struktur übergeben, anstatt der Struktur nach Wert. Dies ist die normale Vorgehensweise beim Arbeiten mit Strukturen, die von Zeigern übergeben werden.

Diese Funktion arbeitet einwandfrei, aber die Felder ACLineStatus und batteryFlag sollten besser als Enumerationen definiert werden:

enum ACLineStatus: byte 
   {
      Offline = 0,
      Online = 1,
      Unknown = 255,
   }
   enum BatteryFlag: byte
   {
      High = 1,
      Low = 2,
      Critical = 4,
      Charging = 8,
      NoSystemBattery = 128,
      Unknown = 255,
   }

Beachten Sie, dass wir byte als Basistyp für die Enumeration verwenden, da die Strukturfelder Bytes sind.

 

Zeichenfolgen

Während .NET nur einen Zeichenfolgentyp kennt, gibt es in nicht verwalteten Umgebungen mehrere Varianten. Es gibt Zeichenzeiger und Strukturen mit eingebetteten Zeichenarrays, die jeweils korrekt gemarshallt werden müssen.

Zudem werden in Win32 zwei unterschiedliche Zeichenfolgendarstellungen verwendet:

  • ANSI

  • Unicode

Unter Windows wurden ursprünglich Einbytezeichen verwendet. Diese beanspruchten wenig Speicherplatz, die Mehrbytecodierung für viele Sprachen war jedoch sehr komplex. Windows NT® hingegen verwendete eine Zweibyte-Unicode-Codierung. Um diesen Unterschied zu berücksichtigen, setzt die Win32-API einen cleveren Trick ein. Sie definiert einen TCHAR-Typ, der auf Win9x-Plattformen ein Einbytezeichen und auf WinNT-Plattformen ein Zweibyte-Unicode-Zeichen ist. Für jede Funktion, die eine Zeichenfolge oder Struktur mit Zeichendaten verarbeitet, werden zwei Versionen dieser Struktur definiert. Dabei gibt ein A-Suffix an, dass es sich um ANSI handelt, und ein W-Suffix, dass es sich um Unicode handelt. Wenn Sie ein C++-Programm für Einbytes kompilieren, erhalten Sie die A-Variante, und beim Kompilieren für Unicode die W-Variante. Die Win9x-Plattformen enthalten die ANSI-Version und die WinNT-Plattformen die W-Version.

Da die Entwickler von P/Invoke von ihren Benutzern nicht verlangen wollten, zunächst die verwendete Plattform zu ermitteln, haben sie integrierte Unterstützung für die automatische Verwendung der A- oder W-Version bereitgestellt. Falls die von Ihnen aufgerufene Funktion nicht vorhanden ist, sucht die Interoperabilitätsschicht nach der A- oder W-Version und verwendet diese stattdessen.

Bei der Zeichenfolgenunterstützung gilt es einige Feinheiten zu beachten, die sich am besten anhand eines Beispiels veranschaulichen lassen.

 

Einfache Zeichenfolgen

Hier sehen Sie ein einfaches Beispiel für eine Funktion, die einen Zeichenfolgenparameter verarbeitet:

BOOL GetDiskFreeSpace(
  LPCTSTR lpRootPathName,          // Stammpfad
  LPDWORD lpSectorsPerCluster,     // Sektoren pro Cluster
  LPDWORD lpBytesPerSector,        // Bytes pro Sektor
  LPDWORD lpNumberOfFreeClusters,  // Freie Cluster
  LPDWORD lpTotalNumberOfClusters  // Cluster gesamt
);

Der Stammpfad ist definiert als LPCTSTR. Dies ist die plattformunabhängige Version eines Zeichenfolgenzeigers.

Da es keine Funktion mit dem Namen GetDiskFreeSpace() gibt, sucht der Marshaller automatisch nach der "A"- oder "W"-Variante und ruft die entsprechende Funktion auf. Wir verwenden ein Attribut, um dem Marshaller mitzuteilen, welchen Zeichenfolgentyp die API erfordert.

Hier nun in aller Vollständigkeit mein erster Definitionsversuch für die Funktion:

[DllImport("kernel32.dll")]
static extern bool GetDiskFreeSpace(
   [MarshalAs(UnmanagedType.LPTStr)]
   string rootPathName,
   ref int sectorsPerCluster,
   ref int bytesPerSector,
   ref int numberOfFreeClusters,
   ref int totalNumberOfClusters);

Leider hat der Code nicht funktioniert. Das Problem ist, dass der Marshaller standardmäßig die ANSI-Version einer API zu finden versucht, unabhängig davon, auf welchem System wir uns befinden. Und da LPTStr bedeutet, dass auf Windows NT-Plattformen Unicode-Zeichenfolgen verwendet werden sollten, versuchen wir letztendlich die ANSI-Funktion mit einer Unicode-Zeichenfolge aufzurufen. Das funktioniert natürlich nicht.

Wir haben zwei Möglichkeiten, um die Fehler zu beheben. Der einfachste Weg besteht darin, das MarshalAs-Attribut zu entfernen. Wenn Sie das tun, rufen Sie immer die A-Version der Funktion auf. Wenn diese auf allen für Sie relevanten Plattformvarianten vorhanden ist, ist das in Ordnung. Dadurch wird Ihr Code jedoch verlangsamt, da der Marshaller Ihre .NET-Zeichenfolge von Unicode in Multi-Byte konvertiert, die A-Version der Funktion aufruft (so dass die Zeichenfolge wieder in Unicode zurückkonvertiert wird) und anschließend die W-Version der Funktion aufruft.

Um dies zu verhindern, müssen Sie den Marshaller anweisen, auf Win9x-Plattformen nach der A-Version und auf NT-Plattformen nach der W-Version zu suchen. Zu diesem Zweck setzen Sie CharSet als Bestandteil des DllImport-Attributs:

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]

Meiner unwissenschaftlichen Einschätzung nach ist diese Lösung ca. 5 Prozent schneller als die vorhergehende Option. Das Festlegen des CharSet-Attributs und die Verwendung von LPTStr für Zeichenfolgentypen funktioniert für einen Großteil der Win32-API. Es gibt jedoch Funktionen, die nicht den A/W-Mechanismus verwenden und für die Sie eine andere Lösung finden müssen.

 

Zeichenfolgenpuffer

Der Zeichenfolgentyp (string) in .NET ist unveränderlich, das heißt der Wert ändert sich nie. Bei Funktionen, die einen Zeichenfolgenwert in einen Zeichenfolgenpuffer kopieren, macht eine Zeichenfolge keinen Sinn. Dabei wird bestenfalls der temporäre Puffer beschädigt, der vom Marshaller bei der Übersetzung einer Zeichenfolge erstellt wird. Im schlimmsten Fall wird der verwaltete Heap beschädigt, was gravierende Folgen nach sich ziehen kann. In jedem Fall ist es höchst unwahrscheinlich, dass der richtige Wert zurückgegeben wird.

Damit diese Lösung funktioniert, müssen wir einen anderen Typ verwenden. Der StringBuilder-Typ wurde für die Verwendung als Puffer entworfen. Wir werden ihn nun anstelle von string verwenden. Hier ein Beispiel:

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetShortPathName(
   [MarshalAs(UnmanagedType.LPTStr)]
   string path,
   [MarshalAs(UnmanagedType.LPTStr)]
   StringBuilder shortPath,
   int shortPathLength);

Die Verwendung dieser Funktion ist ganz einfach:

StringBuilder shortPath = new StringBuilder(80);
int result = GetShortPathName(
@"d:\test.jpg", shortPath, shortPath.Capacity);
string s = shortPath.ToString();

Beachten Sie, dass Capacity für StringBuilder als Puffergröße übergeben wird.

 

Strukturen mit eingebetteten Zeichenfolgenarrays

Einige Funktionen verwenden Strukturen mit eingebetteten Zeichenfolgenarrays. So verwendet die GetTimeZoneInformation()-Funktion z.B. einen Zeiger auf die folgende Struktur:

typedef struct _TIME_ZONE_INFORMATION { 
    LONG       Bias; 
    WCHAR      StandardName[ 32 ]; 
    SYSTEMTIME StandardDate; 
    LONG       StandardBias; 
    WCHAR      DaylightName[ 32 ]; 
    SYSTEMTIME DaylightDate; 
    LONG       DaylightBias; 
} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

Die Verwendung dieses Codes mit C# erfordert zwei Strukturen. Die SYSTEMTIME-Struktur lässt sich ganz einfach einrichten:

struct SystemTime
   {
      public short wYear;
      public short wMonth;
      public short wDayOfWeek;
      public short wDay;
      public short wHour;
      public short wMinute;
      public short wSecond;
      public short wMilliseconds;
   }

Hier warten keine großen Überraschungen auf uns. Die Definition für TimeZoneInformation hingegen ist etwas komplexer:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct TimeZoneInformation
{ 
   public int bias;
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
   public string standardName;
   SystemTime standardDate;
   public int standardBias;
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
   public string daylightName;
   SystemTime daylightDate;
   public int daylightBias;
}

Bei dieser Definition sind insbesondere zwei Details zu beachten. Das erste ist das MarshalAs-Attribut:

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

Wenn wir uns die Dokumente für ByValTStr ansehen, stellen wir fest, dass es für eingebettete Zeichenfolgenarrays verwendet wird. SizeConst wird verwendet, um die Größe der Arrays festzulegen.

Als ich dies zum ersten Mal codierte, traten prompt Ausführungsmodulfehler auf. Das bedeutet für gewöhnlich, dass im Rahmen der Interoperabilität ein Teil des Arbeitsspeichers überschrieben wird, das heißt, die Struktur hatte die falsche Größe. Ich habe Marshal.SizeOf() verwendet, um die vom Marshaller verwendete Größe zu ermitteln - 108 Byte. Bei meinen Nachforschungen erinnerte ich mich bald, dass der Standardzeichenfolgentyp für Interop ANSI bzw. Einbyte ist. Die Zeichenfolgen in der Funktionsdefinition sind jedoch als WCHAR typisiert, also Zweibyte. Hier hatten wir die Ursache des Problems.

Ich habe es durch Hinzufügen des StructLayout-Attributs behoben. Sequenzielles Layout ist für Strukturen die Standardlösung, das heißt, alle Felder sind in der Reihenfolge ihrer Auflistung angeordnet. Der CharSet-Wert ist auf Unicode gesetzt, damit immer der richtige Zeichenfolgentyp verwendet wird.

Sobald ich diese Korrektur vorgenommen hatte, arbeitete die Funktion einwandfrei. Sie fragen sich vielleicht, wieso ich nicht CharSet.Auto für diese Funktion verwendet habe. Dies ist eine jener Funktionen, die keine A- und W-Varianten haben. Da sie stets Unicode-Zeichenfolgen verwendet, habe ich sie entsprechend fest codiert.

 

Funktionen mit Rückrufen

Wenn Win32-Funktionen mehr als ein Datenelement zurückgeben müssen, tun sie dies für gewöhnlich über einen Rückrufmechanismus. Der Entwickler übergibt einen Funktionszeiger an die Funktion und die Entwicklerfunktion wird für jedes enumerierte Element aufgerufen.

Anstelle von Funktionszeigern verwendet C# Delegaten. Diese werden beim Aufrufen von Win32-Funktionen als Ersatz für Funktionszeiger verwendet.

Ein Beispiel für eine solche Funktion ist die EnumDesktops()-Funktion:

BOOL EnumDesktops(
  HWINSTA hwinsta,            // Handle für Fensterstation
  DESKTOPENUMPROC lpEnumFunc, // Rückruffunktion
  LPARAM lParam               // Wert für Rückruffunktion
);

Der HWINSTA-Typ wird durch IntPtr ersetzt und LPARAM wird durch int ersetzt. DESKTOPENUMPROC erfordert etwas mehr Arbeit. Hier ist die MSDN-Definition:

BOOL CALLBACK EnumDesktopProc(
  LPTSTR lpszDesktop,  // Desktopname
  LPARAM lParam        // Benutzerdefinierter Wert
);

Wir können dies in den folgenden Delegaten konvertieren:

delegate bool EnumDesktopProc(
   [MarshalAs(UnmanagedType.LPTStr)]
   string desktopName,
   int lParam);

Nachdem dies definiert ist, können wir die Definition für EnumDesktops() schreiben:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool EnumDesktops(
   IntPtr windowStation,
   EnumDesktopProc callback,
   int lParam);

Das reicht aus, um die Funktion zum Laufen zu bringen.

Beim Verwenden von Delegaten für Interoperabilität sollten Sie einen wichtigen Hinweis beachten. Der Marshaller erstellt einen Funktionszeiger, der auf den Delegaten verweist. Dieser Funktionszeiger wird an die nicht verwaltete Funktion übergeben. Der Marshaller erkennt jedoch nicht, was die nicht verwaltete Funktion mit dem Funktionszeiger macht. Somit geht er davon aus, dass dieser nur während des Funktionsaufrufs gültig sein muss.

Dies führt dazu, dass Sie beim Aufrufen einer Funktion wie SetConsoleCtrlHandler(), bei der der Funktionszeiger für eine spätere Verwendung aufgespart wird, sicherstellen müssen, dass in Ihrem Code auf den Delegaten verwiesen wird. Wenn Sie dies nicht tun, scheint die Funktion zwar zu arbeiten, eine künftige Garbagecollection löscht jedoch den Delegaten und zieht gravierende Folgen nach sich.

 

Erweiterte Funktionen

Alle bisher vorgestellten Beispiele sind relativ einfach, doch es gibt viele wesentlich komplexere Win32-Funktionen. Hier ein Beispiel:

DWORD SetEntriesInAcl(
  ULONG cCountOfExplicitEntries,           // Anzahl der Einträge
  PEXPLICIT_ACCESS pListOfExplicitEntries, // Puffer
  PACL OldAcl,                             // Original-ACL
  PACL *NewAcl                             // Neue ACL
);

Die ersten beiden Parameter können problemlos verarbeitet werden: ulong ist einfach und der Puffer kann mit UnmanagedType.LPArray gemarshallt werden.

Der dritte und der vierte Parameter stellen jedoch ein Problem dar. Dieses besteht darin, wie eine ACL definiert ist. Die ACL-Struktur definiert nur den Header einer ACL und der Rest des Puffers besteht aus ACEs. Der ACE kann einer von mehreren unterschiedlichen Typen von ACEs sein. Diese ACEs haben unterschiedliche Längen.

Dieses Problem kann in C# gelöst werden, wenn Sie bereit sind, die gesamte Pufferzuweisung vorzunehmen und relativ viel unsicheren Code zu verwenden. Es wird jedoch eine Menge Arbeit und sehr schwer zu debuggen sein. Es ist viel einfacher, diese API in C++ zu erstellen.

 

Andere Optionen für die Attribute

Die Attribute DLLImport und StructLayout haben eine Reihe von Optionen, die bei der Verwendung von P/Invoke nützlich sein können. Aus Gründen der Vollständigkeit habe ich sie nachfolgend allesamt aufgelistet.

DLLImport
CallingConvention
Damit können Sie dem Marshaller mitteilen, welche Aufrufkonvention die Funktion verwendet. Sie sollten dies auf die Aufrufkonvention Ihrer Funktion setzen. Wenn Sie diese Einstellung falsch vornehmen, wird Ihr Code nicht funktionieren. Wenn Ihre Funktion jedoch eine Cdecl-Funktion ist und Sie sie mit StdCall aufrufen (Standard), arbeitet Ihre Funktion zwar, doch die Funktionsparameter werden nie aus dem Stapel entfernt. Auf diese Weise wird der Stapel überfüllt.

CharSet
Steuert, ob die A- oder W-Variante aufgerufen wird.

EntryPoint
Diese Eigenschaft wird verwendet, um den Namen festzulegen, nach dem der Marshaller in der DLL sucht. Wenn Sie diese Eigenschaft festlegen, können Sie Ihre C#-Funktion anschließend nach Belieben umbenennen.

ExactSpelling
Setzen Sie diese Eigenschaft auf TRUE. Der Marshaller deaktiviert daraufhin die A- und W-Suche.

PreserveSig
Aufgrund der COM-Interoperabilität wirkt eine Funktion mit final out-Parameter, als ob sie diesen Wert zurückgibt. Das wird durch diese Eigenschaft deaktiviert.

SetLastError
Stellt sicher, dass SetLastError() für die Win32-API aufgerufen wird, damit Sie feststellen können, was passiert ist.

StructLayout
LayoutKind
Das Standardlayout für Strukturen ist sequenziell und dies funktioniert in den meisten Fällen. Wenn Sie die totale Kontrolle darüber haben möchten, wo die Strukturmember platziert werden, können Sie LayoutKind.Explicit verwenden und anschließend ein FieldOffset-Attribut für jedes Member der Struktur festlegen. Dies erfolgt für gewöhnlich, wenn Sie eine Union erstellen müssen.

CharSet
Steuert, wie der standardmäßige Zeichentyp für ByValTStr-Member lautet.

Pack
Legt die Verpackungsgröße der Struktur fest. Auf diese Weise wird die Ausrichtung der Struktur gesteuert. Wenn die C-Struktur eine andere Verpackung verwendet, müssen Sie diese festlegen.

Size
Legt die Größe der Struktur fest. Wird im Allgemeinen nicht verwendet, kann jedoch von Nutzen sein, wenn Sie am Ende der Struktur zusätzlichen Speicherplatz zuweisen müssen.

 

Laden aus unterschiedlichen Speicherorten

Es gibt keine Möglichkeit festzulegen, wo DLLImport zur Laufzeit nach einer Datei suchen soll. Es gibt jedoch einen Trick, mit dem Sie dies umgehen können.

DllImport ruft LoadLibrary() für diese Aufgabe auf. Wenn eine bestimmte DLL in einen Prozess geladen wurde, hat LoadLibrary() Erfolg, selbst wenn der angegebene Pfad für den Ladevorgang anders lautet.

Das heißt, wenn Sie LoadLibrary() direkt aufrufen, können Sie Ihre DLL von jedem beliebigen Speicherort aus laden, und LoadLibrary() für DllImport verwendet diese Version.

Aufgrund dieses Verhaltens ist es möglich, LoadLibrary() vorzeitig aufzurufen, um Ihre Aufrufe an eine andere DLL weiterzuleiten. Wenn Sie eine Bibliothek schreiben, können Sie dies verhindern, indem Sie GetModuleHandle() aufrufen. Auf diese Weise stellen Sie sicher, dass die Bibliothek nicht bereits geladen wurde, bevor Sie Ihren ersten P/Invoke-Aufruf ausführen.

Fehlerbehebung für "P/Invoke"
Wenn Ihre P/Invoke-Aufrufe fehlschlagen, liegt dies häufig daran, dass einige Typen falsch definiert sind. Nachfolgend einige gängige Probleme:

  • Long != long. long ist in C++ eine 4-Byte-Ganzzahl, in C# hingegen eine 8-Byte-Ganzzahl.

  • Falsches Festlegen des Zeichenfolgentyps.

 

Nächster Monat

Im nächsten Monat beschäftigen wir uns mit dem Wrappen von Funktionen in C++.