März 2017

Band 32, Nummer 3

C++: Vereinfachen der Programmierung von sicheren Arrays in C++ mit CComSafeArray

Von Giovanni Dicanio | März 2017

Es ist allgemein üblich, komplexe Softwaresysteme mithilfe von Komponenten zu erstellen, die in verschiedenen Sprachen geschrieben wurden. C++-Hochleistungscode kann z. B. in eine C-Schnittstelle, eine DLL (Dynamic-Link Library) oder in eine COM-Komponente eingebettet werden. Sie können auch einen Windows-Dienst in C++ schreiben, der einige COM-Schnittstellen bereitstellt. In C# geschriebene Clients können mit einem solchen Dienst über COM-Interop interagieren.

Häufig möchten Sie Daten in der Form von Arrays zwischen diesen Komponenten austauschen. Möglicherweise verwenden Sie einige C++-Komponenten, die mit Hardware interagieren und ein Array aus Daten generieren (etwa ein Array aus Bytes), die die Pixel eines aus einem Eingabegerät gelesenen Bilds darstellen, oder ein Array aus Gleitkommawerten, die aus einem Sensor gelesene Messwerte darstellen. Oder Sie nutzen einen in C++ geschriebenen Windows-Dienst, der mit anderen Low-Level-Modulen interagiert und Arrays aus Zeichenfolgen zurückgibt, die Sie in GUI-Clients nutzen möchten, die in C# oder in einer Skriptingsprache geschrieben wurden. Das Übergeben solcher Daten über Modulgrenzen hinweg ist nicht trivial und erfordert die Verwendung gut durchdachter und gestalteter Datenstrukturen.

Die Windows-Programmierplattform bietet eine passende verwendungsbereite Datenstruktur, die zu diesem Zweck eingesetzt werden kann: das SAFEARRAY, dessen Definition Sie im Windows Developer Center (bit.ly/2fLXY6K finden). Im Grunde beschreibt die SAFEARRAY-Datenstruktur eine bestimmte Instanz eines sicheren Arrays und gibt dabei Attribute wie die Anzahl seiner Dimensionen sowie einen Zeiger auf die tatsächlichen Daten des sicheren Arrays an. Ein sicheres Array wird üblicherweise im Code über einen Zeiger auf seinen SAFEARRAY-Deskriptor (also SAFEARRAY*) verarbeitet. Es sind auch Windows-APIs für die C-Schnittstelle zum Manipulieren sicherer Arrays verfügbar, z. B. „SafeArrayCreate“ und „SafeArrayDestroy“ für die Erstellung und Zerstörung, sowie weitere Funktionen zum Sperren einer sicheren Arrayinstanz und für den sicheren Zugriff auf seine Daten. Weitere Details zur C-Datenstruktur SAFEARRAY sowie zu einigen ihrer nativen APIs der C-Schnittstelle finden Sie in der Onlinebegleitung zu diesem Artikel „Einführung in die SAFE­ARRAY-Datenstruktur“ (msdn.com/magazine/mt778923).

Für C++-Programmierer ist es jedoch bequemer, C++-Klassen einer höheren Ebene (z. B. „CComSafeArray“ der ATL (Active Template Library)) zu verwenden, anstatt auf der C-Schnittstellenebene zu arbeiten.

In diesem Artikel beschreibe ich C++-Codebeispiele mit zunehmender Komplexität, um sichere Arrays zu erstellen, die verschiedene Arten von Daten mit ATL-Hilfsklassen einschließlich „CComSafeArray“ speichern.

Sichere Arrays im Vergleich zum STL-Vektor

STL-Klassenvorlagen (Standard Template Library, Standardvorlagenbibliothek) wie „std::vector“ sind ausgezeichnete Container für C++-Code innerhalb von Modulgrenzen. Ich empfehle deren Verwendung in einem solchen Kontext. Es ist z. B. effizienter, Inhalt dynamisch hinzuzufügen und einen „std::vector“ zu generieren, als ein sicheres Array zu erstellen, und „std::vector“ ist außerdem gut in Algorithmen aus der STL- und anderen plattformübergreifenden C++-Bibliotheken von Drittanbietern (wie Boost) integriert. Außerdem können Sie mit „std::vector“ plattformübergreifenden C++-Standardcode erstelle. Sichere Arrays sind im Gegensatz dazu für die Windows-Plattform spezifisch.

An den Modulgrenzen sind hingegen sichere Arrays vorzuziehen. „std::vectors“ können Modulgrenzen nicht sicher überschreiten und können nicht von Clients genutzt werden, die in anderen Sprachen als C++ geschrieben wurden. Dies sind die eigentlichen Kontexte, in denen es sinnvoll ist, sichere Arrays zu verwenden. Ein gutes Codierungsmuster zeichnet sich dadurch aus, dass zum Ausführen der Kernverarbeitung Standard-C++ und STL-Container wie „std::vector“ verwendet werden. Wenn Sie solche Arraydaten dann über Modulgrenzen hinweg übertragen müssen, können Sie den Inhalt von „std::vector“ in ein sicheres Array projizieren, das ein ausgezeichneter Kandidat für das Überschreiten von Modulgrenzen und für die Nutzung durch Clients ist, die in anderen Sprachen als C++ geschrieben wurden.

Der ATL-Wrapper „CComSafeArray“

Die „native“ Programmierschnittstelle des sicheren Arrays verwendet APIs der Win32-C-Schnittstelle. Dies wird in der Onlinebegleitung zu diesem Artikel beschrieben. Zwar ist es möglich, diese C-Funktionen in C++-Code zu verwenden, sie führen jedoch häufig zu sperrigem und fehleranfälligem Code. Sie müssen z. B. darauf achten, dass die Aufhebungen der Zuordnung sicherer Arrays mit jeder Zuordnung sicherer Arrays ordnungsgemäß übereinstimmen, damit Sperren mit aufgehobenen Sperren ordnungsgemäß übereinstimmen usw.

Glücklicherweise ist es mit C++ möglich, diesen Code im C-Stil mithilfe praktischer Codierungsmuster wie RAII und Destruktoren erheblich zu vereinfachen. Sie können z. B. eine Klasse schreiben, die als Wrapper für SAFEARRAY-Rohdeskriptoren dient, und dann über den Konstruktor „SafeArrayLock“ und über den Destruktor die entsprechende SafeArrayUnlock-Funktion aufrufen. Auf diese Weise wird das sichere Array automatisch entsperrt, sobald das Wrapperobjekt den Gültigkeitsbereich verlässt. Es ist außerdem sinnvoll, diese Klasse als Vorlage zu konzipieren, um Typsicherheit für das sichere Array zu erzwingen. Während in C die Generizität des sicheren Arrays mithilfe eines void-Zeigers für das Feld „SAFEARRAY::pvData“ ausgedrückt wird, können Sie in C++ eine bessere Typüberprüfung zur Kompilierzeit mithilfe von Vorlagen ausführen.

Glücklicherweise müssen Sie eine solche Klassenvorlage nicht von Grund auf neu erstellen. ATL stellt nämlich eine geeignete C++-Klassenvorlage bereit, um die Programmierung sicherer Arrays zu vereinfachen: „CComSafeArray“, deklariert im <atlsafe.h>-Header. In „CComSafeArray<T>“ von ATL stellt der Vorlagenparameter „T“ den Datentyp dar, der im sicheren Array gespeichert wird. Für ein sicheres Array aus BYTEs würden Sie z. B. „CComSafeArray<BYTE>“ verwenden, für ein sicheres Array von Gleitkommawerten wird der Wrapper „CComSafeArray<float>“ verwendet usw. Beachten Sie, dass das intern mit einem Wrapper versehene sichere Array weiterhin ein polymorphes, auf einem void-Zeiger basierendes Array im C-Stil ist. Die von „CComSafeArray“ erstellte C++-Wrapperschicht bietet eine höhere und sicherere Ebene der Abstraktion, die automatische Verwaltung der Lebensdauer des sicheren Arrays und bessere Typsicherheit als „C void*“ bereitstellt.

„CComSafeArray<T>“ weist nur einen Datenmember („m_psa“) auf, der ein Zeiger auf einen Deskriptor des sicheren Arrays („SAFEARRAY*“) ist, das durch das CComSafeArray-Objekt in einen Wrapper eingeschlossen wird.

Erstellen von CComSafeArray-Instanzen

Der CComSafeArray-Standardkonstruktor erstellt ein Wrapperobjekt, das nur einen NULL-Zeiger auf den Deskriptor eines sicheren Arrays enthält. Im Prinzip wird nichts in diesen Wrapper eingeschlossen.

Außerdem sind mehrere Konstruktorüberladungen verfügbar, die ganz praktisch sein können. Sie können z. B. eine Elementanzahl übergeben, um ein sicheres Array zu erstellen, das aus einer angegebenen Anzahl von Elementen besteht:

// Create a SAFEARRAY containing 1KB of data
CComSafeArray<BYTE> data(1024);

Es ist auch möglich, eine niedrigere Grenze als null (Standardwert) anzugeben:

// Create a SAFEARRAY containing 1KB of data
// with index starting from 1 instead of 0
CComSafeArray<BYTE> data(1024, 1);

Wenn Sie Code für sichere Arrays schreiben, die durch C++- oder C#-/.NET-Clients verarbeitet werden sollen, halten Sie sich jedoch besser an die übliche Konvention, als Untergrenze null (anstatt eins) zu verwenden.

Beachten Sie, dass die CComSafeArray-Konstruktoren „SafeArrayLock“ automatisch für Sie aufrufen. Das in einen Wrapper eingeschlossene sichere Array ist daher bereits gesperrt und bereit für Lese- und Schreibvorgänge im Benutzercode.

Wenn ein Zeiger auf den Deskriptor eines vorhandenen sicheren Arrays vorhanden ist, können Sie dieses an eine andere CComSafeArray-Konstruktorüberladung übergeben:

// psa is a pointer to an existing safe array descriptor
// (SAFEARRAY* psa)
CComSafeArray<BYTE> sa(psa);

In diesem Fall versucht „CComSafeArray“, eine tiefe Kopie des ursprünglichen sicheren Arrays aus BYTEs zu erstellen, auf das „psa“ zeigt.

Automatische Ressourcenbereinigung mit „CComSafeArray“

Wenn ein CComSafeArray-Objekt den Geltungsbereich verlässt, bereinigt dessen Destruktor automatisch den Arbeitsspeicher sowie die Ressourcen, die vom in den Wrapper eingeschlossenen sicheren Array zugeordnet wurden. Das ist sehr praktisch und vereinfacht den C++-Code erheblich. Auf diese Weise könne sich C++-Programmierer auf den Kern ihres Codes konzentrieren und müssen sich nicht mit den Details der Bereinigung des Arbeitsspeichers des sicheren Arrays beschäftigen. Unerfreuliche Fehler durch Arbeitsspeicherverluste werden so vermieden.

Wenn Sie den ATL CComSafeArray-Code untersuchen, erkennen Sie, dass der CComSafeArray-Destruktor zuerst „SafeArrayUnlock“ und dann „SafeArrayDestroy“ aufruft. Es ist daher nicht erforderlich, dass Clientcode das sichere Array explizit entsperrt und zerstört. Es würde sich sogar um einen Fehler durch doppelte Zerstörung handeln.

Praktische Methoden für allgemeine SafeArray-Vorgänge

Zusätzlich zur automatischen Lebensdauerverwaltung des sicheren Arrays bietet die CComSafeArray-Klassenvorlage einige praktische Methoden, um Vorgänge für sichere Arrays zu vereinfachen. Sie können z. B. einfach die GetCount-Methode aufrufen, um die Elementanzahl des in einen Wrapper eingeschlossenen sicheren Arrays abzurufen. Zum Erhalten des Zugriffs auf die Elemente des sicheren Arrays können Sie die CComSafeArray::GetAt- und SetAt-Methoden aufrufen, indem Sie einfach den Index des Elements angeben.

Wenn Sie beispielsweise durch die Elemente in einem vorhandenen sicheren Array iterieren möchten, können Sie Code ähnlich dem folgenden verwenden:

// Assume sa is a CComSafeArray instance wrapping an existing safe array.
// Note that this code is generic enough to handle safe arrays
// having lower bounds different than the usual zero.
const LONG lb = sa.GetLowerBound();
const LONG ub = sa.GetUpperBound();
// Note that the upper bound is *included* (<=)
for (LONG i = lb; i <= ub; i++)
{
  // ... use sa.GetAt(i) to access the i-th item
}

Außerdem überlädt „CComSafeArray“ „operator[]“, um eine noch einfachere Syntax für den Zugriff auf die Elemente des sicheren Arrays zu bieten. Sie werden einige dieser Methoden in den nächsten Abschnitten dieses Artikels in Aktion erleben.

Es ist auch möglich, neue Elemente an ein vorhandenes sicheres Array anzufügen, indem die CComSafeArray::Add-Methode aufgerufen wird. Außerdem kann „CCom­SafeArray::Resize“ (wie der Name schon sagt) zum Ändern der Größe des in einen Wrapper eingeschlossenen sicheren Arrays verwendet werden. Im Allgemeinen empfehle ich jedoch, C++-Code zu schreiben, der „std::vector“ verwendet. Dieser ist für die Größenänderung schneller als sichere Arrays und gut in andere STL-Algorithmen und auch andere plattformübergreifende C++-Bibliotheken von Drittanbietern integriert. Sobald die Vorgänge für den Inhalt von „std::vector“ abgeschlossen wurden, können die Daten in ein sicheres Array kopiert werden, das Modulgrenzen sicher überschreiten (im Gegensatz zu „std::vector“) und auch von Clients genutzt werden kann, die in einer anderen Sprache als C++ geschrieben wurden.

Kopiervorgänge sicherer Arrays sind mithilfe der überladenen Kopierzuweisung „operator=“ oder durch Aufrufen von Methoden wie „CComSafeArray::CopyFrom“ möglich.

„Move-Semantik“ in „CComSafeArray“

Die CComSafeArray-Klasse implementiert keine „Move-Semantik“ im strengen Sinn von C++11. Diese ATL-Klasse ist älter als C++11, und mindestens bis Visual Studio 2015 wurden Verschiebevorgänge von C++11 (z. B. Verschieben des Konstruktors und Verschieben des Zuweisungsoperators) der Klasse nicht hinzugefügt. „CComSafeArray“ bietet jedoch eine andere Art von Move-Semantik, die auf zwei Methoden basiert: „Attach“ und „Detach“. Wenn ein Zeiger auf den Deskriptor eines sicheren Arrays („SAFEARRAY*“) vorhanden ist, können Sie es im Prinzip an die Attach-Methode übergeben. „CComSafeArray“ übernimmt dann den Besitz dieses sicheren Roharrays. Beachten Sie, dass alle ggf. zuvor vorhandenen Daten des sicheren Arrays, die vom CComSafeArray-Objekt umschlossen werden, ordnungsgemäß bereinigt werden.

Analog dazu können Sie die CComSafeArray::Detach-Methode aufrufen. Der CComSafeArray-Wrapper übergibt dann den Besitz des umschlossenen sicheren Arrays an den Aufrufer und gibt einen Zeiger auf den zuvor im Besitz befindlichen Deskriptor des sicheren Arrays zurück. Die Detach-Methode erweist sich als praktisch, wenn Sie ein sicheres Array in C++-Code mithilfe von „CComSafeArray“ erstellen und dieses dann als Ausgabezeigerparameter an einen Aufrufer (z. B. in einer COM-Schnittstellenmethode oder in einer DLL-Funktion der C-Schnittstelle) übergeben. Die CComSafeArray-Klasse von C++ kann keine COM- oder DLL-Grenzen der C-Schnittstelle überschreiten. Dies ist nur Zeigern des SAFEARRAY*-Deskriptors möglich.

C++-Ausnahmen können keine COM- und DLL-Grenzen von C überschreiten

Einige der CComSafeArray-Methoden wie „Create“, „CopyFrom“, „SetAt“, „Add“ und „Resize“ geben HRESULTs zurück, um (wie in der COM-Programmierung üblich) Erfolgs- oder Fehlerbedingungen zu melden. Andere Methoden, z. B. einige CComSafeArray-Konstruktorüberladungen oder „GetAt“ bzw. „operator[]“, lösen bei Fehlern Ausnahmen aus. ATL löst standardmäßig C++-Ausnahmen vom Typ „CAtlException“ aus. Dabei handelt es sich um einen kleinen Wrapper um ein HRESULT.

C++-Ausnahmen können jedoch die Grenzen von COM-Methoden oder DLL-Funktionen der C-Schnittstelle nicht überschreiten. Für COM-Methoden ist es zwingend erforderlich, dass HRESULTs zum Anzeigen von Fehlern zurückgegeben werden. Diese Option kann auch in DLLs der C-Schnittstelle verwendet werden. Beim Schreiben von C++-Code, der den CComSafeArray-Wrapper (oder auch eine andere auslösende Komponente) verwendet, ist es wichtig, diesen Code mithilfe eines try/catch-Blocks zu schützen. Innerhalb der Implementierung einer COM-Methode oder innerhalb einer DLL-Grenzfunktion, die ein HRESULT zurückgibt, können Sie Code schreiben, der dem folgenden ähnelt:

try
{
  // Do something that can potentially throw exceptions as CAtlException ...
}
catch (const CAtlException& e)
{
  // Exception object implicitly converted to HRESULT,
  // and returned as an error code to the caller
  return e;
}
// All right
return S_OK;

Generieren eines sicheren Arrays von Bytes

Nach dem vorherigen konzeptionellen Framework für sichere Arrays und dem praktischen C++ CComSafeArray-Wrapper von ATL möchte ich nun einige praktische Anwendungen der Programmierung sicherer Arrays behandeln und „CComSafeArray“ in Aktion zeigen. Ich beginne mit einem einfachen Beispiel: dem Generieren eines sicheren Arrays von Bytes aus C++-Code. Dieses sichere Array kann als ein Ausgabeparameter in einer COM-Schnittstellenmethode oder DLL-Funktion der C-Schnittstelle übergeben werden.

Auf ein sicheres Array wird normalerweise über einen Zeiger auf den Deskriptor des Arrays verwiesen. Im C++-Code erfolgt die Übersetzung in „SAFEARRAY*“. Wenn das sichere Array als Ausgabeparameter vorhanden ist, benötigen Sie einen anderen Grad der Dereferenzierung, damit ein sicheres Array, das als Ausgabeparameter übergeben wird, die SAFEARRAY**-Form (doppelter Zeiger auf den SAFEARRARY-Deskriptor) aufweist:

STDMETHODIMP CMyComComponent::DoSomething(
    /* [out] */ SAFEARRAY** ppsa)
{
  // ...
}

Vergessen Sie innerhalb der COM-Methode nicht, einen try/catch-Schutz zum Abfangen von Ausnahmen und Übersetzen der Ausnahmen in die entsprechenden HRESULTs vorzusehen, wie im vorherigen Abschnitt gezeigt wurde. (Beachten Sie, dass das STDMETHODIMP-Präprozessormakro implizit voraussetzt, dass die Methode ein HRESULT zurückgibt.)

Innerhalb des try-Blocks wird eine Instanz der CComSafeArray-Klassenvorlage erstellt, die „BYTE“ als Vorlagenparameter angibt:

// Create a safe array storing 'count' BYTEs
CComSafeArray<BYTE> sa(count);

Anschließend können Sie auf die Elemente im sicheren Array zugreifen, indem Sie einfach den überladenen „operator[]“ von „CComSafeArray“ verwenden, wie das folgende Beispiel zeigt:

for (LONG i = 0; i < count; i++)
{
  sa[i] = /* some value */;
}

Nachdem Sie die Daten vollständig in das sichere Array geschrieben haben (z. B. durch Kopieren aus einem „std::vector“), kann das sichere Array als ein Ausgabeparameter zurückgegeben werden und die Detach-Methode von „CComSafeArray“ aufrufen:

*ppsa = sa.Detach();

Beachten Sie, dass das CComSafeArray-Wrapperobjekt nach dem Aufruf von „Detach“ den Besitz des sicheren Arrays an den Aufrufer überträgt. Wenn das CComSafeArray-Objekt den Geltungsbereich verlässt, wird daher das mit dem vorherigen Code erstellte sichere Array nicht zerstört. Mithilfe von „Detach“ wurden die Daten des sicheren Arrays einfach aus dem vorherigen Code, mit dem das sichere Array ursprünglich erstellt wurde, an den Aufrufer übertragen bzw. in ihn „verschoben“. Es liegt nun in der Verantwortung des Aufrufers, das sichere Array zu zerstören, wenn es nicht mehr benötigt wird.

Sichere Arrays eignen sich auch gut für das Austauschen von Arraydaten über die Grenzen des DLL-Moduls hinweg. Nehmen Sie als Beispiel eine in C++ geschriebene DLL der C-Schnittstelle, die ein sicheres Array generiert, das von C#-Clientcode genutzt wird. Angenommen, die von der DLL exportierte Begrenzungsfunktion weist den folgenden Prototyp auf:

extern "C" HRESULT __stdcall ProduceByteArrayData(/* [out] */ SAFEARRAY** ppsa)

Die entsprechende PInvoke-Deklaration von C# lautet dann:

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void ProduceByteArrayData(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
  out byte[] result);

„UnmanagedType.SafeArray“ bedeutet, dass der tatsächliche native Arraytyp an der Grenze der Funktion ein „SAFEARRAY“ ist. Der „SafeArraySubType“ zugewiesene Var­Enum.VT_UI1-Wert gibt an, dass der Typ der im sicheren Array gespeicherten Daten „BYTE“ ist (also ein Integerwert ohne Vorzeichen mit der Größe von genau einem Byte). Für ein sicheres Array, in dem 4-Byte-Integerwerte mit Vorzeichen gespeichert werden, wäre auf der C++-Seite „CComSafe­Array<int>“ vorhanden, und der entsprechende PInvoke VarEnum-Typ wäre „VT_I4“ (also ein Integerwert mit Vorzeichnen mit einer Größe von 4 Byte). Das sichere Array wird einem byte[]-Array in C# zugeordnet, das als ein Ausgabeparameter übergeben wird.

Das Attribut „PreserveSig = false“ weist „PInvoke“ an, die von der nativen Funktion zurückgegebenen Fehler-HRESULTs in Ausnahmen in C# zu übersetzen.

Abbildung 1 zeigt einen vollständigen Beispielcodeausschnitt zum Generieren eines sicheren Arrays von Bytes in C++ mithilfe von „CComSafeArray“. Der Code ist Bestandteil einer hypothetischen COM-Methode. Der gleiche auf „CComSafeArray“ basierende Code kann jedoch auch für DLL-Begrenzungsfunktionen der C-Schnittstelle verwendet werden.

Abbildung 1: Generieren eines sicheren Arrays von Bytes mithilfe von „CComSafeArray“

STDMETHODIMP CMyComComponent::DoSomething(/* [out] */ SAFEARRAY** ppsa) noexcept
{
  try
  {
    // Create a safe array storing 'count' BYTEs
    const LONG count = /* some count value */;
    CComSafeArray<BYTE> sa(count);
    // Fill the safe array with some data
    for (LONG i = 0; i < count; i++)
    {
      sa[i] = /* some value */;
    }
    // Return ("move") the safe array to the caller
    // as an output parameter
    *ppsa = sa.Detach();
  }
  catch (const CAtlException& e)
  {
    // Convert ATL exceptions to HRESULTs
    return e;
  }
  // All right
  return S_OK;
}

Generieren eines sicheren Arrays von Zeichenfolgen

Sie wissen jetzt, wie ein sicheres Array von Bytes erstellt und die PInvoke-Deklarationssignatur in C# verwendet wird, wenn das sichere Array als ein Ausgabeparameter in einer DLL-Funktion der C-Schnittstelle übergeben wird. Dieses Codierungsmuster funktioniert gut für sichere Arrays, die andere skalare Typen wie „int“, „float“, „double“ usw. speichern. Diese Typen weisen die gleichen binären Darstellungen in C++ und C# auf und können auf einfache Weise zwischen diesen beiden Umgebungen und auch über die Grenzen der COM-Komponente hinweg gemarshallt werden.

Sichere Arrays, in denen Zeichenfolgen gespeichert werden, erfordern jedoch besondere Aufmerksamkeit. Zeichenfolgen verlangen besondere Sorgfalt, weil sie komplexer als einzelne Skalarwerte wie Bytes oder Integerwerte bzw. Gleitkommazahlen sind. Der BSTR-Typ eignet sich zum Darstellen von Zeichenfolgen, die Modulgrenzen sicher überschreiten können. Dieser Typ ist recht vielseitig. Sie können in aber als Unicode UTF-16-Zeichenfolgenzeiger mit Längenpräfix betrachten. Der Standardmarshaller weiß, wie BSTRs kopiert und über die Grenzen von COM- oder C-Schnittstellenfunktionen hinweg bereitgestellt werden können. Eine interessante und ausführliche Beschreibung der BSTR-Semantik finden Sie in einem Blogbeitrag von Eric Lippert unter bit.ly/2fLXTfY.

Wenn Sie ein sicheres Array zum Speichern von Zeichenfolgen in C++ erstellen möchten, kann die CComSafeArray-Klassenvorlage mithilfe des BSTR-Typs instanziiert werden:

// Creates a SAFEARRAY containing 'count' BSTR strings
CComSafeArray<BSTR> sa(count);

Der hier angegebene Typ des sicheren Arrays ist der BSTR C-Rohtyp. In C++-Code ist es jedoch wesentlich besser (also einfacher und sicherer) einen RAII-Wrapper um Roh-BSTRs zu verwenden. ATL bietet einen solchen praktischen Wrapper in Form der CComBSTR-Klasse. In Win32-/C++-Code, der mit Microsoft Visual C++ (MSVC) kompiliert wird, können Unicode UTF-16-Zeichenfolgen mithilfe der std::wstring-Klasse dargestellt werden. Es ist daher sinnvoll, eine praktische Hilfsfunktion zum Konvertieren aus „std::wstring“ in „ATL::CComBSTR“ zu erstellen:

// Convert from STL wstring to the ATL BSTR wrapper
inline CComBSTR ToBstr(const std::wstring& s)
{
  // Special case of empty string
  if (s.empty())
  {
    return CComBSTR();
  }
  return CComBSTR(static_cast<int>(s.size()), s.data());
}

Zum Auffüllen eines CComSafeArray<BSTR>-Objekts mit aus einem „vector<wstring>“ von STL kopierten Zeichenfolgen kann eine Iteration durch den Vektor erfolgen, und für jede „wstring“ im Vektor kann ein entsprechendes CComBSTR-Objekt durch Aufrufen der bereits erwähnten Hilfsfunktion erstellt werden:

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

Anschließend kann der zurückgegebene bstr-Wert in das sichere Arrayobjekt durch Aufrufen der CComSafeArray::SetAt-Methode kopiert werden:

hr = sa.SetAt(i, bstr);

Die SetAt-Methode gibt ein HRESULT zurück. Es hat sich daher als gute Programmierpraktik bewährt, den HRESULT-Wert zu überprüfen und im Fall von Fehlern eine Ausnahme auszulösen:

if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

Die Ausnahme wird an der Grenze der COM-Methode oder DLL-Funktion der C-Schnittstelle in ein HRESULT konvertiert. Als Alternative kann das Fehler-HRESULT direkt aus dem vorherigen Codeausschnitt zurückgegeben werden.

Der Hauptunterschied zwischen diesem sicheren BSTR-Array und dem vorherigen CComSafeArray<BYTE>-Beispiel besteht in der Erstellung eines CComBSTR-Wrapperzwischenobjekts um die BSTR-Zeichenfolgen. Für einfache skalare Typen wie Bytes, Integerwerte oder Gleitkommazahlen sind solche Wrapper nicht erforderlich. Für komplexere Typen wie BSTRs, für die ordnungsgemäße Verwaltung der Lebensdauer erforderlich ist, sind diese C++ RAII-Wrapper ausgesprochen hilfreich. Wrapper wie etwa „CComBSTR“ verbergen Funktionsaufrufe wie „SysAllocString“. Dieser Aufruf wird zum Erstellen eines neuen BSTR-Objekts aus einem vorhandenen Zeichenfolgenzeiger im C-Stil verwendet. Analog dazu ruft der CComBSTR-Destruktor automatisch „SysFreeString“ zum Freigeben des BSTR-Arbeitsspeichers auf. Diese Details der Verwaltung der BSTR-Lebensdauer werden praktischerweise alle in der Implementierung der CCom­BSTR-Klasse verborgen, damit sich C++-Programmierer auf die Programmlogik Ihres Codes auf höherer Ebene konzentrieren können.

„Move-Semantik“-Optimierung für sichere Arrays von BSTRs

Beachten Sie, dass der oben gezeigte CComSafeArray<BSTR>::SetAt-Methodenaufruf eine tiefe Kopie der Eingabe-BSTR in das sichere Array ausführt. Tatsächlich verfügt die SetAt-Methode über einen zusätzlichen dritten booleschen bCopy-Parameter, der standardmäßig den Wert TRUE aufweist. Dieser bCopy-Kennzeichnungsparameter ist für skalare Typen wie Bytes, Integerwerte oder Gleitkommazahlen nicht wichtig, weil diese alle mithilfe einer tiefen Kopie in das sichere Array übertragen werden. Für komplexere Typen (z. B. BSTRs), deren Verwaltung der Lebensdauer und Kopiersemantik nicht trivial ist, ist er jedoch wichtig. Wenn in diesem Fall von „CComSafeArray<BSTR>“ z. B. FALSE als dritter Parameter für SetAt angegeben wird, übernimmt das sichere Array einfach den Besitz des BSTR-Eingabeobjekts, anstatt eine tiefe Kopie des Objekts anzufertigen. Es handelt sich um eine Art von schnell optimiertem Verschiebevorgang und nicht um eine tiefe Kopie. Diese Optimierung erfordert außerdem, dass die Detach-Methode für den CComBSTR-Wrapper aufgerufen wird, um den BSTR-Besitz von „CComBSTR“ an das „CComSafeArray“ zu übertragen:

hr = sa.SetAt(i, bstr.Detach(), FALSE);

Wie bereits für das CComSafeArray<BYTE>-Beispiel gezeigt wurde, kann das sichere Array an den Aufrufer mit Code ähnlich dem folgenden übergeben werden, sobald die Erstellung von „CComSafeArray<BSTR>“ abgeschlossen wurde:

// Return ("move") the safe array to the caller
// as an output parameter (SAFEARRAY **ppsa)
*ppsa = sa.Detach();

Eine DLL-Funktion der C-Schnittstelle, die ein sicheres Array von BSTR-Zeichenfolgen erstellt und dieses an den Aufrufer übergibt, kann in C# wie folgt mithilfe von „PInvoke“ verwendet werden:

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void BuildStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)]
  out string[] result);

Beachten Sie die Verwendung von „VarEnum.VT_BSTR“ zum Angeben des Vorhandenseins eines sicheren Arrays, das BSTR-Zeichenfolgen speichert. Das „SAFEARRAY“ von BSTRs, das in nativem C++-Code erstellt wird, wird an C# mithilfe eines string[]-Arraytyps gemarshallt und als ein Ausgabeparameter übergeben.

Generieren eines sicheren Arrays von Varianten, die Zeichenfolgen enthalten

Steigern wir nun den Komplexitätsgrad mit einem weiteren Schritt. Ein sicheres Array kann nicht nur Elemente mit Typen wie Byte, Integerwert und BSTR-Zeichenfolgen speichern, sondern es ist auch möglich, ein sicheres Array eines „generischen“ Typs zu erstellen: des Typs „VARIANT“. Eine Variante ist ein polymorpher Typ, der Werte aus einer Vielzahl verschiedener Typen (von Integerwerten bis hin zu Gleitkommazahlen und BSTR-Zeichenfolgen usw.) speichern kann. Der C-Typ „VARIANT“ ist im Grunde eine gigantische Union. Eine Definition finden Sie unter bit.ly/2fMc4Bu. Wie für den C-Typ „BSTR“ bietet ATL einen praktischen C++-Wrapper um den C-Rohtyp „VARIANT“: die ATL::CComVariant-Klasse. In C++-Code ist es einfacher und sicherer, Varianten mithilfe des CComVariant-Wrappers zu verarbeiten, anstatt C-Funktionen direkt zum Zuordnen, Kopieren und Löschen von Varianten aufzurufen.

In C++ und C# geschriebene Clients verstehen sichere Arrays, die „direkte“ Typen speichern (wie in den Beispielen oben für „BYTE“ und „BSTR“ gezeigt). Es gibt aber auch einige Skriptingclients, die nur sichere Arrays verstehen, die Varianten speichern. Wenn Sie also Arraydaten in C++ erstellen möchten und diese Daten durch solche Skriptingclients nutzbar sein sollen, müssen sie in ein sicheres Array gepackt werden, das Varianten speichert. Auf diese Weise ergibt sich eine neue Ebene der Dereferenzierung (und auch zusätzlicher Mehraufwand). Jedes Variantenelement im sicheren Array speichert seinerseits ein „BYTE“, eine Gleitkommazahl, eine „BSTR“ oder einen anderen Wert, dessen Typ unterstützt wird.

Angenommen, Sie möchten den Code oben ändern, der ein sicheres Array aus BSTR-Zeichenfolgen erstellt, und stattdessen ein sicheres Array von Varianten verwenden. Die Varianten enthalten ihrerseits BSTRs. Generiert wird jedoch ein sicheres Array von Varianten und kein direktes sicheres Array von BSTR-Zeichenfolgen.

Zum Erstellen eines sicheren Arrays von Varianten kann die CComSafeArray-Klassenvorlage im ersten Schritt wie folgt instanziiert werden:

// Create a safe array storing VARIANTs
CComSafeArray<VARIANT> sa(count);

Anschließend können Sie durch eine Sammlung von Zeichenfolgen iterieren, die z. B. in einem „vector<wstring>“ von STL gespeichert sind. Für jedes wstring-Objekt kann ein CComBSTR-Objekt genau wie im vorherigen Codebeispiel erstellt werden:

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

Hier gibt es jedoch einen Unterschied zum vorherigen Szenario mit sicheren Arrays aus BSTRs. Da Sie dieses Mal ein sicheres Array aus VARIANTs (und kein direktes sicheres Array aus BSTRs) erstellen, können Sie das BSTR-Objekt nicht direkt im „CComSafeArray“ durch Aufrufen von „SetAt“ speichern. Stattdessen muss zuerst ein Variantenobjekt als Wrapper für dieses bstr-Objekt erstellt werden. Anschließend kann dieses Variantenobjekt in das sichere Array aus Varianten eingefügt werden:

// First create a variant from the CComBSTR
  CComVariant var(bstr);
  // Then add the variant to the safe array
  hr = sa.SetAt(i, var);
  if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

Beachten Sie, dass „CComVariant“ einen überladenen Konstruktor besitzt, der einen konstanten wchar_t*-Zeiger annimmt. Es wäre daher also möglich, eine „CComVariant“ direkt aus einer „std::wstring“ durch Aufrufen der c_str-Methode von „wstring“ zu erstellen. In diesem Fall speichert die Variante jedoch nur den anfänglichen Block des ursprünglichen wstring-Objekts bis zum ersten NULL-Terminator, während „wstring“ und „BSTR“ potenziell Zeichenfolgen mit eingebetteten NULL-Werten speichern können. Das Erstellen eines CComBSTR-Zwischenobjekts aus „std::wstring“ (wie zuvor in der benutzerdefinierten ToBstr-Hilfsfunktion erfolgt) deckt auch diesen generischeren Fall ab.

Wie gewöhnlich kann die CComSafeArray::Detach-Methode zum Zurückgeben des erstellten sicheren Arrays an den Aufrufer als ein SAFEARRAY**-Ausgabeparameter verwendet werden:

// Transfer ownership of the created safe array to the caller
*ppsa = sa.Detach();

Das sichere Array kann auch über eine Funktion der C-Schnittstelle wie folgt übergeben werden:

extern "C" HRESULT __stdcall BuildVariantStringArray(/* [out] */ SAFEARRAY** ppsa)

In diesem Fall kann die folgenden PInvoke-Deklaration von C# verwendet werden:

[DllImport("NativeDll.dll", PreserveSig = false)]
pubic static extern void BuildVariantStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
  out string[] result);

Beachten Sie die Verwendung von „VarEnum.VT_VARIANT“ für den „SafeArraySubType“, da dieses Mal das in C++ erstellte sichere Array Varianten enthält (die ihrerseits als Wrapper für BSTR-Zeichenfolgen dienen), jedoch nicht „BSTR“ direkt.

Im Allgemeinen empfehle ich, Daten mithilfe sicherer Arrays von direkten Typen und nicht mit sicheren Arrays zu exportieren, die Varianten speichern. Dies gilt nur dann nicht, wenn Ihre Daten für Skriptingclients bereitgestellt werden müssen, die nur das sichere Array von Varianten verarbeiten können.

Zusammenfassung

Die Datenstruktur „sicheres Array“ ist ein nützliches Tool zum Austauschen von Arraydaten über verschiedene Modul- und Sprachgrenzen hinweg. Das sichere Array ist eine vielseitige Datenstruktur. In ihr können einfache primitive Typen wie Bytes, Integerwerte oder Gleitkommazahlen gespeichert werden, aber auch komplexere Typen wie BSTR-Zeichenfolgen oder sogar generische VARIANT-Objekte. In diesem Artikel wurde anhand konkreter Beispiele gezeigt, wie die Programmierung solcher Datenstrukturen in C++ mithilfe von ATL-Hilfsklassen vereinfacht werden kann.


Giovanni Dicanio ist Programmierer mit den Spezialgebieten C++ und Windows-Betriebssysteme, Pluralsight-Autor (bit.ly/GioDPS) und Visual C++-MVP. Er liebt nicht nur das Programmieren und Verfassen von Kursen, sondern unterstützt auch gerne andere Benutzer in Foren und Communitys mit dem Schwerpunkt C++. Sie erreichen ihn unter giovanni.dicanio@gmail.com. Er bloggt außerdem unter msmvps.com/gdicanio.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: David Cravey und Marc Gregoire
David Cravey ist Unternehmensarchitekt bei GlobalSCAPE, führt verschiedene C++-Benutzergruppen und ist ein viermaliger Visual C++-MVP.
Marc Gregoire ist ein leitender Softwareentwickler aus Belgien, der Gründer der belgischen C++-Benutzergruppe, Autor von „Professional C++“ (Wiley), Koautor von „C++ Standard Library Quick Reference“ (Apress), technischer Redakteur bei einer Vielzahl von Büchern und hat seit 2007 die jährliche MVP-Auszeichnung für seine umfassenden VC++-Kenntnisse empfangen. Sie können Marc unter marc.gregoire@nuonsoft.com erreichen.