Share via



August 2015

Band 30, Nummer 8

Windows mit C++ – Windows-Runtime-Komponenten mit MIDL

Von Kenny Kerr | August 2015

Kenny KerrIn meinem Artikel vom Juli 2015 (msdn.microsoft.com/magazine/mt238401) habe ich das Konzept von Windows-Runtime-Komponenten (WinRT) als Weiterentwicklung des COM-Programmierungsmodells vorgestellt. Während COM von Win32 zur Seite geschoben wurde, steht COM bei der Windows-Runtime im Vordergrund und Mittelpunkt. Die Windows-Runtime ist die Nachfolgerin von Win32, wobei dies ein Sammelbegriff für die Windows-API ist, da sie viele verschiedene Technologien und Programmiermodelle umfasst. Die Windows-Runtime bietet ein konsistentes und einheitliches Programmiermodell. Damit es jedoch Erfolg hat, benötigen Entwickler innerhalb und außerhalb von Microsoft bessere Tools zum Entwickeln von WinRT-Komponenten und zum Verwenden dieser Komponenten in Apps.

Das wichtigste über das Windows SDK zur Verfügung gestellte Tool für diesen Zweck ist der MIDL-Compiler. In der Kolumne vom Juli habe ich gezeigt, wie mit dem MIDL-Compiler die Windows Runtime Metadata-Datei (WINMD) erzeugt wird, die die meisten Sprachprojektionen zum Nutzen von WinRT-Komponenten benötigen. Freilich weiß jeder erfahrene Entwickler auf der Windows-Plattform, dass der MIDL-Compiler auch Code erzeugt, den ein C-oder C++-Compiler direkt nutzen kann. In der Tat weiß MIDL selbst nichts über das WINMD-Dateiformat. Es geht hauptsächlich um das Analysieren von IDL-Dateien und Erzeugen von Code für C- und C++-Compiler zur Unterstützung der COM- und RPC-Entwicklung sowie um die Erzeugung von Proxy-DLLs. Der MIDL-Compiler ist "historisch" gesehen eine überaus wichtige Komponente, dass die Entwickler der Windows-Runtime nicht das Risiko seiner Aufgliederung eingegangen sind und stattdessen einen "Subcompiler" entwickelt haben, der ausschließlich für die Windows-Runtime zuständig ist. Entwickler sind sich in der Regel dieses Manövers nicht bewusst – und müssen es auch nicht – aber es hilft beim Erklären, wie der MIDL-Compiler in der Praxis funktioniert.

Lassen Sie uns IDL-Quellcode betrachten und prüfen, was wirklich beim MIDL-Compiler abläuft. Hier ist eine IDL-Quelldatei, die eine klassische COM-Schnittstelle definiert:

C:\Sample>type Sample.idl
import "unknwn.idl";
[uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
interface IHen : IUnknown
{
  HRESULT Cluck();
}

Klassisches COM ist nicht besonders an Namespaces interessiert, weshalb die "IHen"-Schnittstelle einfach auf Dateiebene definiert wird. Die Definition von "IUnknown" muss auch vor ihrer Verwendung importiert werden. Ich kann diese Datei anschließend durch den MIDL-Compiler laufen lassen, um verschiedene Artefakte zu erzeugen:

C:\Sample>midl Sample.idl
C:\Sample>dir /b
dlldata.c
Sample.h
Sample.idl
Sample_i.c
Sample_p.c

Die Quelldatei "dlldata.c" enthält einige Makros, die die erforderlichen Exporte für eine Proxy-DLL implementieren. Die Datei "Sample_i.c" enthält die GUID der "IHen"-Schnittstelle für den Fall, dass Sie einen 25 Jahre alten Compiler verwenden, der die "uuid __declspec" nicht unterstützt, die GUIDs an Typen anfügt. Dann ist da noch die Datei "Sample_p.c", die die Marshallinganweisungen für die Proxy-DLL enthält. Ich übergehe diese jetzt einmal und konzentriere mich stattdessen auf die Datei "Sample.h", die etwas recht praktisch enthält. Wenn Sie sich einen Überblick über all die schrecklichen Makros verschaffen, die C-Entwickler bei der Verwendung von COM helfen sollen (der reinste Horror!), finden Sie dies hier vor:

MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
IHen : public IUnknown
{
public:
  virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
};

Es handelt sich nicht um elegantes C++, aber nach dem Präprozessorlauf erhalten Sie eine C++-Klasse, die von "IUnknown" erbt und selbständig eine rein virtuelle Funktion hinzufügt. Dies ist praktisch, da es bedeutet, dass Sie diese nicht von Hand schreiben müssen, wobei Sie möglicherweise eine Diskrepanz zwischen der C++-Definition der Schnittstelle und der ursprünglichen IDL-Definition herbeiführen, die andere Tools und Sprachen ggf. nutzen. Das ist im Wesentlichen die Aufgabe des MIDL-Compilers für C++-Entwickler: das Erzeugen einer Übersetzung des IDL-Quellcodes dergestalt, dass ein C++-Compiler diese Typen direkt nutzen kann.

Lassen Sie uns nun zur Windows-Runtime zurückkehren. Ich aktualisiere den IDL-Quellcode nur geringfügig, um die strengeren Vorgaben für WinRT-Typen zu erfüllen:

C:\Sample>type Sample.idl
import "inspectable.idl";
namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
}

WinRT-Schnittstellen müssen direkt von "IInspectable" erben. Außerdem wird ein Namespace teilweise verwendet, um die Typen der implementierenden Komponente zuzuordnen. Wenn ich versuche, den Code wie zuvor zu kompilieren, tritt ein Problem auf:

.\Sample.idl(3) : error MIDL2025 : syntax error : expecting an interface name or DispatchInterfaceName or CoclassName or ModuleName or LibraryName or ContractName or a type specification near "namespace"

Der MIDL-Compiler erkennt das Schlüsselwort "namespace" nicht und gibt auf. Hierfür gibt es die Befehlszeilenoption "/winrt". Sie weist den MIDL-Compiler an, die Befehlszeile direkt an den MIDLRT-Compiler zu übergeben, um die IDL-Quelldatei vorzuverarbeiten. Es ist dieser zweite Compiler (MIDLRT), der die Befehlszeilenoption "/metadata_dir" erwartet, die ich in der Kolumne vom Juli erwähnt habe:

C:\Sample>midl /winrt Sample.idl /metadata_dir
  "C:\Program Files (x86)\Windows Kits ..."

Sehen Sie sich als weiteren Beweis dafür die MIDL-Compiler-Ausgabe näher an, und Sie werden sehen, was ich meine:

C:\Sample>midl /winrt Sample.idl /metadata_dir "..."
Microsoft (R) 32b/64b MIDLRT Compiler Engine Version 8.00.0168
Copyright (c) Microsoft Corporation. All rights reserved.
MIDLRT Processing .\Sample.idl
.
.
.
Microsoft (R) 32b/64b MIDL Compiler Version 8.00.0603
Copyright (c) Microsoft Corporation. All rights reserved.
Processing C:\Users\Kenny\AppData\Local\Temp\Sample.idl-34587aaa
.
.
.

Ich habe einige Schritte zur Verarbeitung von Abhängigkeiten entfernt, um die wichtigsten Punkte zu verdeutlichen. Durch Aufrufen der ausführbaren MIDL-Datei mit den "/winrt"-Optionen wird die Befehlszeile vor dem Beenden blind an die ausführbaren MIDLRT-Datei übergeben. MIDLRT analysiert der IDL-Datei zuerst, um die WINMD-Datei zu generieren, erstellt dabei aber auch eine andere weitere temporäre IDL-Datei. Diese temporäre IDL-Datei ist eine Übersetzung des Originals, bei der alle WinRT-spezifischen Schlüsselwörter, wie z. B. "namespaces", so ersetzt wurden, dass der ursprüngliche MIDL-Compiler sie akzeptiert. MIDLRT ruft dann die ausführbare MIDL-Datei erneut auf, nun aber ohne die Option "/winrt" und mit dem Speicherort der temporären IDL-Datei, sodass der ursprünglichen Satz von C- und C++-Headern und -Quelldateien wie zuvor erzeugt werden kann.

Der Namespace in der ursprünglichen IDL-Datei wird entfernt, und der Name der "IHen"-Schnittstelle wird in der temporären IDL-Datei wie folgt ergänzt:

interface __x_Sample_CIHen : IInspectable
.
.
.

Dies ist tatsächlich eine codierte Form des Typnamens, der vom MIDL-Compiler interpretiert wird, wenn beim Aufrufen von MIDL mit der vorverarbeiteten Ausgabe die von MIDLRT verwendete Befehlszeilenoption "/gen_namespace" angegeben wird. Der ursprüngliche MIDL-Compiler kann dann diesen Code ohne spezifische Kenntnisse der Windows-Runtime direkt verarbeiten. Dies ist nur ein Beispiel, doch nun können Sie nachvollziehen, wie die neuen Tools vorhandene Technologie optimal nutzen, um die Aufgabe zu erledigen. Wenn Sie wissen möchten, wie dies funktioniert, können Sie den temporären Ordner durchstöbern, den der MIDL-Compiler ausgibt, um dann festzustellen, dass diese Dateien ("Sample.idl-34587aaa" im vorherigen Beispiel) fehlen. Die ausführbare MIDLRT-Datei sorgt nach ihrer Ausführung selbst für die Bereinigung, doch wenn Sie die Befehlszeilenoption "/savePP" hinzufügen, löscht MIDL diese temporären Präprozessordateien nicht. Wenn noch etwas mehr Vorverarbeitung hinzukommt, enthält die resultierende Datei "Sample.h" nun etwas, das auch ein C++-Compiler als Namespace erkennt:

namespace Sample {
  MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
  IHen : public IInspectable
  {
  public:
    virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
  };
}

Ich kann dann diese Schnittstelle wie zuvor implementieren und sicher sein, dass der Compiler jedwede Diskrepanz zwischen meiner Implementierung und den Originaldefinitionen erkennt, die ich in IDL programmiert habe. Wenn Sie hingegen MIDL nur benötigen, um die WINMD-Datei zu erzeugen, und nicht alle Quelldateien für einen C- oder C++-Compiler brauchen, können Sie mit der Befehlszeilenoption "/nomidl" das Erstellen aller zusätzlichen Artefakte vermeiden. Diese Option wird, zusammen mit dem gesamten Rest, von der ausführbaren MIDL-Datei an die ausführbare MIDLRT-Datei übergeben. MIDLRT überspringt dann erneut den letzten Schritt beim Aufrufen von MIDL, nachdem die Erstellung der WINMD-Datei abgeschlossen ist. Bei Verwenden einer von MIDL erzeugten Windows-Runtime-ABI ist es auch üblich, die Befehlszeilenoption "/ns_prefix" hinzuzufügen, damit resultierende Typen und Namespaces wie folgt vom Namespace "ABI" umschlossen werden:

namespace ABI {
  namespace Sample {
    MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
    IHen : public IInspectable
    {
    public:
      virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
    };
  }
}

Abschließend sollte ich erwähnen, dass weder MIDL noch MIDLRT ausreicht, um eine eigenständige WINMD-Datei zu erzeugen, die die Typen einer Komponente ausreichend beschreibt. Wenn Sie versehentlich auf externe Typen verweisen, in der Regel andere vom Betriebssystem definierte Typen, muss die WINMD-Datei, die vom bisher beschriebenen Prozess erzeugt wird, immer noch mit der Hauptmetadatendatei für die Version von Windows zusammengeführt werden, für die Sie entwickeln. Lassen Sie mich das Problem veranschaulichen.

Ich beginne mit einem IDL-Namespace, der eine "IHen"-Schnittstelle und eine aktivierbare "Hen"-Klasse beschreibt, die diese Schnittstelle implementiert (sieheAbbildung 1).

Abbildung 1: Die "Hen"-Klasse in IDL

namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
  [version(1)]
  [activatable(1)]
  runtimeclass Hen
  {
    [default] interface IHen;
  }
}

Ich implementieren Sie anschließend mit derselben Technik, die ich der Kolumne vom Juli beschrieben habe. Der Unterschied ist, dass ich nun mit der Definition von "IHen" arbeiten kann, die vom MIDL-Compiler bereitgestellt wird. Innerhalb einer WinRT-App kann ich nun einfach ein "Hen"-Objekt erstellen und die "Cluck"-Methode aufrufen. Ich verwende C#, um die App-Seite der Gleichung zu veranschaulichen:

public void SetWindow(CoreWindow window)   
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck();
}

Die "SetWindow"-Methode ist Teil der "IFrameworkView"-Implementierung, die von der C#-App bereitgestellt wird. ("IFrameworkView" habe ich in meiner Kolumne vom August 2013 beschrieben, die Sie unter msdn.microsoft.com/magazine/jj883951 finden.) Und der Aufruf funktioniert fehlerfrei. C# ist von den WINMD-Metadaten, die die Komponente beschreiben, komplett abhängig. Auf der anderen Seite ist das Freigeben von systemeigenem C++-Code für C#-Clients dadurch ein Kinderspiel. In den meisten Fällen jedenfalls. Auf ein Problem, das entsteht, ist, wenn Sie auf externe Typen verweisen, habe ich vorhin hingewiesen. Lassen Sie uns die "Cluck"-Methode so aktualisieren, dass sie ein "CoreWindow" als Argument anfordert. "CoreWindow" wird vom Betriebssystem definiert, weshalb nicht so einfach eine Definition in meiner IDL-Quelldatei möglich ist.

Zunächst aktualisiere ich die IDL-Datei so, dass eine Abhängigkeit von der "ICoreWindow"-Schnittstelle eingerichtet wird. Ich importiere einfach die Definition wie folgt:

import "windows.ui.core.idl";

Dann füge ich einen "ICoreWindow"-Parameter an die "Cluck"-Methode an:

HRESULT Cluck([in] Windows.UI.Core.ICoreWindow * window);

Der MIDL-Compiler verwandelt diesen "Import" in ein "#include" von "windows.ui.core.h" im Header um, der generiert wird, sodass ich lediglich meine Implementierung der "Hen"-Klasse aktualisieren muss:

virtual HRESULT __stdcall Cluck(ABI::Windows::UI::Core::ICoreWindow *) 
  noexcept override
{
  return S_OK;
}

Ich kann jetzt die Komponente wie zuvor kompilieren und sie an den App-Entwickler weiterleiten. Der Entwickler der C#-App aktualisiert pflichtbewusst den Aufruf der "Cluck"-Methode wie folgt mit einem Verweis auf das "CoreWindow" der App:

public void SetWindow(CoreWindow window)
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck(window);
}

Leider meldet sich jetzt der C#-Compiler:

error CS0012: The type 'ICoreWindow' is defined in an assembly
  that is not referenced.

Sie sehen, dass der C#-Compiler die Schnittstellen als nicht identisch erkennt. Der C#-Compiler ist mit nur einem übereinstimmenden Typnamen nicht zufrieden und kann nicht die Verbindung mit dem Windows-Typ mit demselben Namen herstellen. Im Gegensatz zu C++ ist C# sehr viel abhängiger von binären Typinformationen, um alle Einzelteile miteinander zu verbinden. Um dieses Problem zu beheben, kann ich ein weiteres Tool nutzen, das im Windows SDK zur Verfügung gestellt wird. Es dient zum Erstellen oder Zusammenführen der Metadaten des Windows-Betriebssystems mit den Metadaten der Komponente, wobei "ICoreWindow" ordnungsgemäß in die Hauptmetadatendatei für das Betriebssystem aufgelöst wird. Dieses Tool heißt MDMERGE:

c:\Sample>mdmerge /i . /o output /partial /metadata_dir "..."

Die ausführbaren MIDLRT- und MDMERGE-Dateien sind hinsichtlich ihrer Befehlszeilenargumente besonders. Sie müssen sie unbedingt ordnungsgemäß verwenden, damit sie funktionieren. In diesem Fall kann ich nicht einfach "Sample.winmd" direkt aktualisieren, indem ich die Optionen "/i" (Eingabe) und "/o" (Ausgabe) auf den gleichen Ordner verweise, da MDMERGE nach dem Abschluss die WINMD-Eingabedatei tatsächlich löscht. Die Option "/partial" weist MDMERGE an, die nicht aufgelöste "ICoreWindow"-Schnittstelle in den Metadaten zu suchen, die von der Option "/metadata_dir" bereitgestellt werden. Diese werden als Verweismetadaten bezeichnet. MDMERGE kann demnach verwendet werden, um mehrere WINMD-Dateien zusammenzuführen, aber in diesem Fall verwende ich es nur, um Verweise für Betriebssystemtypen aufzulösen.

An diesem Punkt verweist die resultierende Datei "Sample.winmd" ordnungsgemäß auf die Metadaten aus dem Windows-Betriebssystem, wenn auf die "ICoreWindow"-Schnittstelle verwiesen wird. Der C#-Compiler ist zufrieden und kompiliert die App, wie sie geschrieben wurde. Seien Sie auch nächsten Monat dabei, wenn ich die Windows-Runtime in C++ weiter untersuche.


Kenny Kerr ist Programmierer aus Kanada, Autor für 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