Juli 2015

Band 30, Nummer 7

Windows mit C++ – Komponenten für Windows-Runtime

Von Kenny Kerr | Juli 2015

Kenny KerrIn den nächsten Monaten werde ich die Grundlagen von Windows-Runtime untersuchen. Meine Absicht besteht darin, die Abstraktionen der höheren Ebenen aufzuschlüsseln, die Entwickler in den verschiedenen Sprachprojektionen und Toolketten verwenden, um zu untersuchen, wie Windows-Runtime in der ABI (Application Binary Interface) funktioniert – der Grenze zwischen Anwendungen und den Binärkomponenten, die erforderlich sind, um auf Betriebssystemdienste zuzugreifen.

In mancher Hinsicht stellt Windows-Runtime nur eine Weiterentwicklung von COM dar, das effektiv einen Binärstandard für die Wiederverwendung von Code bereitstellt und auch weiterhin ein beliebtes Verfahren zum Erstellen von komplexen Anwendungen und Betriebssystemkomponenten ist. Im Gegensatz zu COM ist Windows-Runtime jedoch zielgerichteter und wird in erster Linie als Grundlage für die Windows-API verwendet. Anwendungsentwickler werden Windows-Runtime eher als ein Consumer von Betriebssystemkomponenten verwenden, und es ist weniger wahrscheinlich, dass sie selbst Komponenten schreiben. Dennoch kann ein gutes Verständnis der Implementierung der verschiedenen eleganten Abstraktionen und ihrer Projektion in verschiedene Programmiersprachen Sie beim Schreiben effizienterer Anwendungen und bei der besseren Diagnose von Interoperabilitäts- und Leistungsproblemen unterstützen.

Einer der Gründe (neben der eher spärlichen Dokumentation), warum so wenige Entwickler verstehen, wie Windows-Runtime funktioniert, besteht darin, dass die Tools und Sprachprojektionen die zugrunde liegenden Plattform tatsächlich verdecken. Dies erscheint einem C#-Entwickler möglicherweise natürlich, aber es erleichtert einem C++-Entwickler gewiss nicht das Leben, der natürlich wissen möchte, was hinter den Kulissen geschieht. Beginnen wir also damit, eine einfache Komponente für Windows-Runtime mit Standard C++ mithilfe der Visual Studio 2015-Entwicklereingabeaufforderung zu schreiben.

Ich beginne mit einer einfachen, traditionellen DLL, die eine Reihe von Funktionen exportiert. Wenn Sie das Beispiel nachvollziehen möchten, erstellen Sie einen Ordner und darin einige Quelldateien. Beginnen Sie dabei mit "Sample.cpp":

C:\Sample>notepad Sample.cpp

Zunächst kümmere ich mich darum, dass die DLL entladen wird, die ab jetzt als "Komponente" bezeichnet wird. Die Komponente sollte Entladeabfragen über einen exportierten Funktionsaufruf ("DllCanUnloadNow") unterstützen, und die Anwendung steuert den Entladevorgang mithilfe der Funktion "CoFreeUnusedLibraries". Ich werde darauf nicht weiter eingehen, weil dies auf die gleiche Weise wie das Entladen von Komponenten in klassischem COM geschieht. Da die Komponente nicht statisch mit der Anwendung verknüpft ist – z. B. durch eine LIB-Datei –, sondern dynamisch über die Funktion "LoadLibrary" geladen wird, muss es eine Möglichkeit geben, die Komponente bei Bedarf zu entladen. Nur die Komponente weiß wirklich, wie viele ausstehende Verweise vorhanden sind. Die COM-Laufzeit kann daher ihre Funktion "DllCanUnloadNow" aufrufen, um zu ermitteln, ob der Entladevorgang sicher ist. Anwendungen können diese Wartungsaufgaben auch selbst mithilfe der Funktionen "CoFreeUnusedLibraries" oder "CoFreeUnusedLibrariesEx" ausführen. Die Implementierung in der Komponente ist einfach. Ich benötige eine Sperre, die nachverfolgt, wie viele Objekte aktiv sind:

static long s_lock;

Jedes Objekt kann diese Sperre dann einfach in seinem Konstruktor inkrementieren und in seinem Destruktor dekrementieren. Damit dieser Vorgang einfach bleibt, schreibe ich eine einfache ComponentLock-Klasse:

struct ComponentLock
{
  ComponentLock() noexcept
  {
    InterlockedIncrement(&s_lock);
  }
  ~ComponentLock() noexcept
  {
    InterlockedDecrement(&s_lock);
  }
};

Alle Objekte, die verhindern sollen, dass die Komponente entladen wird, können dann einfach ein ComponentLock-Element als Membervariable einbetten. Die Funktion "DllCanUnloadNow" kann nun recht einfach implementiert werden:

HRESULT __stdcall DllCanUnloadNow()
{
  return s_lock ? S_FALSE : S_OK;
}

Es gibt zwei Arten von Objekten, die Sie innerhalb einer Komponente erstellen können – Aktivierungsfactorys, die in klassischem COM als Klassenfactorys bezeichnet werden, und die tatsächlichen Instanzen einer bestimmten Klasse. Ich werde eine einfache Klasse "Hen" implementieren und zunächst eine IHen-Schnittstelle definieren, damit die Henne gackern kann:

struct __declspec(uuid("28a414b9-7553-433f-aae6-a072afe5cebd")) __declspec(novtable)
IHen : IInspectable
{
  virtual HRESULT __stdcall Cluck() = 0;
};

Dies ist eine reguläre COM-Schnittstelle, die nur von "IInspectable" anstatt direkt von "IUnknown" abgeleitet ist. Ich kann dann die Klassenvorlage "Implements" verwenden, die ich im Artikel aus Dezember 2014 (msdn.com/magazine/dn879357) beschrieben habe, und die tatsächliche Implementierung der Klasse "Hen" in der Komponente bereitstellen:

struct Hen : Implements<IHen>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall Cluck() noexcept override
  {
    return S_OK;
  }
};

Eine Aktivierungsfactory ist einfach eine C++-Klasse, die die IActivationFactory-Schnittstelle implementiert. Diese IActivationFactory-Schnittstelle stellt die einzelne Methode "ActivateInstance" bereit, die der klassischen COM-Schnittstelle "IClassFactory"und ihrer Methode "CreateInstance" entspricht. Die klassische COM-Schnittstelle ist tatsächlich geringfügig überlegen, weil die Möglichkeit besteht, dass der Aufrufer eine bestimmte Schnittstelle direkt anfordert, während "IActivationFactory" von Windows-Runtime einfach einen IInspectable-Schnittstellenzeiger zurückgibt. Die Anwendung ist dann dafür verantwortlich, die Methode "QueryInterface" von "IUnknown" aufzurufen, um eine sinnvollere Schnittstelle für das Objekt abzurufen. Dennoch ist die Methode "ActivateInstance" recht einfach zu implementieren:

struct HenFactory : Implements<IActivationFactory>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance)
    noexcept override
  {
    *instance = new (std::nothrow) Hen;
    return *instance ? S_OK : E_OUTOFMEMORY;
  }
};

Die Komponente ermöglicht Anwendungen das Abrufen einer bestimmten Aktivierungsfactory, indem eine weitere Funktion mit dem Namen "DllGetActivationFactory" exportiert wird. Dies entspricht wiederum der exportierten Funktion "DllGetClassObject", die das COM-Aktivierungsmodell unterstützt. Der Hauptunterschied besteht darin, dass die gewünschte Klasse mit einer Zeichenfolge anstelle einer GUID angegeben wird:

HRESULT __stdcall DllGetActivationFactory(HSTRING classId,
   IActivationFactory ** factory) noexcept
{
}

Ein HSTRING ist ein Handle, das einen unveränderlichen Zeichenfolgenwert darstellt. Dies ist der Klassenbezeichner, z. B. "Sample.Hen". Er gibt an, welche Aktivierungsfactory zurückgegeben werden soll. An diesem Punkt können Aufrufe von "DllGetActivationFactory" aus verschiedenen Gründen zu einem Fehler führen. Ich beginne also damit, die Factoryvariable mit einem "nullptr" zu löschen:

*factory = nullptr;

Nun muss ich den Sicherungspuffer für den HSTRING-Klassenbezeichner abrufen:

wchar_t const * const expected = WindowsGetStringRawBuffer(classId, nullptr);

Anschließend kann ich diesen Wert mit allen Klassen vergleichen, die meine Komponente implementiert. Zurzeit ist nur eine Klasse vorhanden:

if (0 == wcscmp(expected, L"Sample.Hen"))
{
  *factory = new (std::nothrow) HenFactory;
  return *factory ? S_OK : E_OUTOFMEMORY;
}

Andernfalls gebe ich einen HRESULT zurück. Dieser Wert gibt an, dass die angeforderte Klasse nicht verfügbar ist:

return CLASS_E_CLASSNOTAVAILABLE;

Dies ist der gesamte C++-Code, der benötigt wird, um diese einfache Komponente einzurichten und auszuführen. Es stehen jedoch noch einige Aufgaben an, um eine DLL für diese Komponente tatsächlich zu erstellen und diese dann für die lästigen C#-Compiler zu beschreiben, die keine Headerdateien analysieren können. Zum Erstellen einer DLL muss ich den Linker integrieren – insbesondere seine Möglichkeit zum Definieren der aus der DLL exportierten Funktionen. Ich könnte den compilerspezifischen Microsoft-Spezifizierer "dllexport __declspec" verwenden. Dies ist jedoch einer der seltenen Fälle, in denen ich es vorziehe, den Linker direkt anzusprechen und stattdessen eine Moduldefinitionsdatei mit der Liste der Exporte bereitzustellen. Ich halte diese Vorgehensweise für weniger fehleranfällig. Es geht also zurück an die Konsole, um die zweite Quelldatei zu erstellen:

C:\Sample>notepad Sample.def

Diese DEF-Datei benötigt lediglich einen Abschnitt namens "EXPORTS", in dem die zu exportierenden Funktionen aufgelistet werden:

EXPORTS
DllCanUnloadNow         PRIVATE
DllGetActivationFactory PRIVATE

Jetzt kann ich die C++-Quelldatei zusammen mit der Moduldefinitionsdatei für den Compiler und den Linker bereitstellen, um die DLL zu generieren. Anschließend kann ich eine einfache Batchdatei aus Gründen der Bequemlichkeit verwenden, um die Komponente und alle Buildartefakte in einem Unterordner zu erstellen:

C:\Sample>type Build.bat
@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def

Ich übergehe die besondere "Magie", die die Batchdatei-Skriptsprache umgibt, und konzentriere mich auf die Visual C++-Compileroptionen. Die Option "/nologo" unterdrückt die Anzeige des Copyrightbanners. Die Option wird auch an den Linker weitergeleitet. Die unverzichtbare Option "/W4" weist den Compiler an, weitere Warnungen für gängige Codierungsfehler anzuzeigen. Es ist keine Option "/FoBuild" vorhanden. Der Compiler weist diese schwer zu lesende Konvention auf, nach der Ausgabepfade auf die Option folgen (in diesem Fall "/Fo"), ohne dass ein Leerzeichen zwischengeschaltet wird. Die Option "/Fo" wird jedenfalls verwendet, um den Compiler zu zwingen, die Objektdatei im Unterordner "Build" zu speichern. Dies ist die einzige Buildausgabe, die nicht standardmäßig im gleichen Ausgabeordner wie die ausführbare Datei gespeichert wird, die mit der Option "/Fe" definiert wird. Die Option "/link" weist den Compiler an, dass die nachfolgenden Argumente vom Linker interpretiert werden sollen. Auf diese Weise wird vermieden, dass der Linker als zweiter Schritt aufgerufen werden muss. Im Gegensatz zum Compiler wird für die Optionen des Linkers nicht zwischen Groß- und Kleinschreibung unterschieden, und es wird ein Trennzeichen zwischen dem Namen einer Option und einem Wert verwendet – wie bei der Option "/def", die die zu verwendende Moduldefinitionsdatei angibt.

Nun kann ich meine Komponente ganz einfach erstellen, und der sich ergebende Unterordner "Build" enthält eine Reihe von Dateien, von denen nur eine wichtig ist. Dies ist natürlich die ausführbare Datei "Sample.dll", die in den Adressraum der Anwendung geladen werden kann. Das ist jedoch nicht ausreichend. Ein Anwendungsentwickler muss wissen, was die Komponente enthält. Ein C++-Entwickler würde wahrscheinlich mit einer Headerdatei zufrieden sein, die die IHen-Schnittstelle enthält, aber selbst das ist nicht sehr praktisch. Windows-Runtime wendet das Konzept der Sprachprojektionen an, bei dem eine Komponente so beschrieben wird, dass verschiedene Sprachen ihre Typen ermitteln und in ihre Programmiermodelle projizieren können. Ich werde die Sprachprojektion in den nächsten Monaten untersuchen. Zum jetzigen Zeitpunkt reicht es aus, dass dieses Beispiel aus einer C#-Anwendung funktioniert, weil dies am meisten überzeugt. Wie bereits zuvor erwähnt, kann der C#-Compiler keine C++-Headerdateien analysieren. Daher muss ich einige Metadaten bereitstellen, die für den C#-Compiler ausreichend sind. Ich muss eine WINMD-Datei generieren, die die CLR-Metadaten enthält, die meine Komponente beschreiben. Dies ist keine einfache Aufgabe, weil die systemeigenen Typen, die ich ggf. für die ABI der Komponente verwenden kann, sich häufig bei der Projektion in C# sehr abweichend darstellen können. Glücklicherweise wurde der Microsoft IDL-Compiler so überarbeitet, dass er auf der Grundlage einer IDL-Datei, die einige neue Schlüsselwörter verwendet, eine WINMD-Datei generiert. Es geht also zurück an die Konsole, um die dritte Quelldatei zu erstellen:

C:\Sample>notepad Sample.idl

Zuerst muss ich die Definition der erforderlichen IInspectable-Schnittstelle importieren:

import "inspectable.idl";

Dann kann ich einen Namespace für die Typen der Komponente definieren. Dieser muss mit dem Namen der Komponente selbst übereinstimmen:

namespace Sample
{
}

Nun muss ich die IHen-Schnittstelle definieren, die ich zuvor in C++ definiert habe – dieses Mal jedoch als eine IDL-Schnittstelle:

[version(1)]
[uuid(28a414b9-7553-433f-aae6-a072afe5cebd)]
interface IHen : IInspectable
{
  HRESULT Cluck();
}

Dies ist traditionelle IDL-Syntax, und wenn Sie IDL in der Vergangenheit verwendet haben, um COM-Komponenten zu definieren, sollten Sie die hier beschriebenen Ausführungen nicht überraschen. Alle Typen von Windows-Runtime müssen jedoch ein Versionsattribut definieren. Diese Definition war einmal optional. Alle Schnittstellen müssen außerdem direkt von "IInspectable" abgeleitet sein. Es gibt praktisch keine Schnittstellenvererbung in Windows-Runtime. Dies hat einige negative Folgen, die ich in den kommenden Monaten behandeln werde.

Und schließlich muss ich die Klasse "Hen" selbst mithilfe des neuen Schlüsselworts "runtimeclass" definieren:

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Das Versionsattribut ist erneut erforderlich. Das aktivierbare Attribut, das jedoch nicht erforderlich ist, gibt an, dass diese Klasse aktiviert werden kann. In diesem Fall gibt es an, dass Standardaktivierung über die Methode "ActivateInstance" von "IActivationFactory" unterstützt wird. Eine Sprachprojektion sollte dies als einen C++- oder C#-Standardkonstruktor oder ein anderes sinnvolles Element einer bestimmten Sprache darstellen. Schließlich gibt das Standardattribut vor dem Schlüsselwort "interface" an, dass die IHen-Schnittstelle die Standardschnittstelle für die Klasse "Hen" ist. Die Standardschnittstelle ist die Schnittstelle, die die Funktion von Parametern und Rückgabetypen ausübt, wenn diese Typen die Klasse selbst angeben. Da die ABI nur für COM-Schnittstellen gilt und die Klasse "Hen" selbst keine Schnittstelle ist, ist die Standardschnittstelle deren Vertreter auf der ABI-Ebene.

Hier gibt es noch viel mehr zu entdecken, aber das Gesagte ist für den Moment ausreichend. Ich kann nun meine Batchdatei so aktualisieren, dass eine WINMD-Datei generiert wird, die meine Komponente beschreibt:

@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def
"C:\Program Files (x86)\Windows Kits\10\bin\x86\midl.exe" /nologo /winrt /out %~dp0Build /metadata_dir "c:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0" Sample.idl

Die "Magie" in der Batchdatei werde ich erneut überspringen und stattdessen die Neuerungen in den MIDL-Compileroptionen erläutern. Die Option "/winrt" ist der Schlüssel. Sie gibt an, dass die IDL-Datei Windows-Runtime-Typen anstelle herkömmlicher Schnittstellendefinitionen im COM- oder RPC-Stil enthält. Die Option "/out" stellt nur sicher, dass die WINMD-Datei im gleichen Ordner wie die DLL-Datei gespeichert wird, weil dies für die C#-Toolkette erforderlich ist. Die Option "/metadata_dir" informiert den Compiler, wo die Metadaten gespeichert sind, die zum Erstellen des Betriebssystems verwendet wurden. Während ich dies schreibe, befindet sich das Windows SDK für Windows 10 noch in der Stabilisierungsphase. Ich muss darauf achten, dass ich den MIDL-Compiler aufrufe, der im Lieferumfang des Windows SDK enthalten ist, und nicht den Compiler, der vom Pfad in der Eingabeaufforderung der Visual Studio-Tools bereitgestellt wird.

Durch das Ausführen der Batchdatei werden jetzt die Dateien "Sample.dll" und "Sample.winmd" generiert, auf die ich anschließend aus einer universellen Windows-C#-App verweisen kann. Damit kann ich die Klasse "Hen" verwenden, als handele es sich einfach um ein weiteres CLR-Bibliothekprojekt:

Sample.Hen h = new Sample.Hen();
h.Cluck();

Windows-Runtime basiert auf der Grundlage von COM und Standard-C++. Zugeständnisse wurden vorgenommen, um die CLR zu unterstützen und C#-Entwicklern die Verwendung der neuen Windows-API erheblich zu vereinfachen, ohne dass Interopkomponenten erforderlich sind. Windows-Runtime kann als die Zukunft der Windows-API angesehen werden.

Ich habe insbesondere die Entwicklung einer Komponente für Windows-Runtime aus der Perspektive des klassischen COM und dessen Ursprung im C++-Compiler dargestellt, damit Sie verstehen können, woher diese Technologie stammt. Dieser Ansatz wird jedoch schnell recht unpraktisch. Der MIDL-Compiler bietet in der Tat weit mehr als nur die WINMD-Datei. Er kann tatsächlich unter anderem dazu verwendet werden, die kanonische Version der IHen-Schnittstelle in C++ zu erstellen. Ich hoffe, Sie sind im nächsten Monat dabei, wenn ein zuverlässigerer Workflow für das Erstellen von Komponenten für Windows-Runtime untersucht wird und dabei auch einige Interopprobleme gelöst werden.


Kenny Kerrist Programmierer aus Kanada sowie Autor bei Pluralsight und Microsoft MVP. Er veröffentlicht Blogs unter kennykerr.ca, und Sie können ihm auf Twitter unter twitter.com/kennykerr folgen.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Larry Osterman