WebIDL Binder

Der WebIDL Binder bietet einen einfachen und leichtgewichtigen Ansatz zur Bindung von C++, sodass kompilierter Code von JavaScript aus aufgerufen werden kann, als wäre er eine normale JavaScript-Bibliothek.

Der WebIDL Binder verwendet WebIDL zur Definition der Bindungen, eine Schnittstellensprache, die speziell für die Verknüpfung von C++ und JavaScript entwickelt wurde. Dies ist nicht nur eine natürliche Wahl für die Bindungen, sondern da es sich um eine Low-Level-Sprache handelt, ist sie relativ einfach zu optimieren.

Der Binder unterstützt die Untermenge der C++-Typen, die in WebIDL ausgedrückt werden können. Diese Untermenge ist für die meisten Anwendungsfälle mehr als ausreichend – Beispiele für Projekte, die mit dem Binder portiert wurden, sind die Physik-Engines Box2D und Bullet.

Dieses Thema zeigt, wie C++-Klassen, -Funktionen und andere Typen mit IDL gebunden und verwendet werden.

Hinweis

Eine Alternative zum WebIDL Binder ist die Verwendung von Embind. Weitere Informationen finden Sie unter Binding C++ und JavaScript — WebIDL Binder und Embind.

Ein kurzes Beispiel

Das Binden mit dem WebIDL Binder ist ein dreistufiger Prozess:

  • Erstellen Sie eine WebIDL-Datei, die die C++-Schnittstelle beschreibt.

  • Verwenden Sie den Binder, um C++- und JavaScript-„Klebecode“ zu generieren.

  • Kompilieren Sie diesen Klebecode mit dem Emscripten-Projekt.

Definieren der WebIDL-Datei

Der erste Schritt ist die Erstellung einer WebIDL-Datei, die die C++-Typen beschreibt, die Sie binden möchten. Diese Datei wird einige Informationen in der C++-Header-Datei duplizieren, in einem Format, das explizit sowohl für einfaches Parsen als auch für die Darstellung von Code-Elementen konzipiert ist.

Betrachten Sie zum Beispiel die folgenden C++-Klassen:

class Foo {
public:
  int getVal();
  void setVal(int v);
};

class Bar {
public:
  Bar(long val);
  void doSomething();
};

Die folgende IDL-Datei kann verwendet werden, um diese zu beschreiben:

interface Foo {
  void Foo();
  long getVal();
  void setVal(long v);
};

interface Bar {
  void Bar(long val);
  void doSomething();
};

Die Zuordnung zwischen der IDL-Definition und C++ ist ziemlich offensichtlich. Die wichtigsten Punkte sind:

  • Die IDL-Klassendefinitionen enthalten eine Funktion, die void zurückgibt und denselben Namen wie die Schnittstelle hat. Dieser Konstruktor ermöglicht es Ihnen, das Objekt aus JavaScript zu erstellen und muss in IDL definiert werden, auch wenn C++ den Standardkonstruktor verwendet (siehe Foo oben).

  • Die Typnamen in WebIDL sind nicht identisch mit denen in C++ (zum Beispiel wird int oben auf long abgebildet). Weitere Informationen zu den Abbildungen finden Sie unter WebIDL-Typen.

Hinweis

structs werden auf dieselbe Weise wie die obigen Klassen definiert — unter Verwendung des Schlüsselworts interface.

Generieren des Bindungs-Klebecodes

Der Bindings-Generator (tools/webidl_binder.py) nimmt einen Web IDL-Dateinamen und einen Ausgabedateinamen als Eingaben und erstellt C++- und JavaScript-Klebecodedateien.

Um zum Beispiel die Klebecodedateien glue.cpp und glue.js für die IDL-Datei my_classes.idl zu erstellen, würden Sie den folgenden Befehl verwenden:

tools/webidl_binder my_classes.idl glue

Kompilieren des Projekts (unter Verwendung des Bindungs-Klebecodes)

Um die Klebecodedateien (glue.cpp und glue.js) in einem Projekt zu verwenden:

  1. Fügen Sie --post-js glue.js in Ihren endgültigen emcc-Befehl ein. Die Option post-js fügt den Klebecode am Ende der kompilierten Ausgabe hinzu.

  2. Erstellen Sie eine Datei mit einem Namen wie my_glue_wrapper.cpp, um die Header der Klassen, die Sie binden, und glue.cpp zu #include einzubinden. Dies könnte den folgenden Inhalt haben:

#include <...> // Where "..." represents the headers for the classes we are binding.
#include <glue.cpp>

Hinweis

Der vom Bindungsgenerator ausgegebene C++-Klebecode enthält die Header für die von ihm gebundenen Klassen nicht, da sie in der Web IDL-Datei nicht vorhanden sind. Der obige Schritt macht diese für den Klebecode verfügbar. Eine andere Alternative wäre, die Header oben in glue.cpp einzuschließen, aber dann würden sie bei jeder Neukompilierung der IDL-Datei überschrieben.

  1. Fügen Sie my_glue_wrapper.cpp zum endgültigen emcc-Befehl hinzu.

Der endgültige emcc-Befehl enthält sowohl den C++- als auch den JavaScript-Klebecode, die so erstellt wurden, dass sie zusammenarbeiten:

emcc my_classes.cpp my_glue_wrapper.cpp --post-js glue.js -o output.js

Die Ausgabe enthält nun alles, was zum Verwenden der C++-Klassen über JavaScript benötigt wird.

Modulare Ausgabe

Bei der Verwendung des WebIDL-Binders erstellen Sie oft eine Bibliothek. In diesem Fall ist die Option MODULARIZE sinnvoll. Sie umschließt die gesamte JavaScript-Ausgabe in einer Funktion und gibt ein Promise zurück, das sich auf die initialisierte Modulinstanz auflöst.

var instance;
Module().then(module => {
  instance = module;
});

Das Promise wird aufgelöst, wenn es sicher ist, kompilierten Code auszuführen, d.h. nachdem er heruntergeladen und instanziiert wurde. Das Promise wird gleichzeitig mit dem Aufruf des onRuntimeInitialized-Callbacks aufgelöst, sodass bei der Verwendung von MODULARIZE keine Notwendigkeit besteht, onRuntimeInitialized zu verwenden.

Sie können die Option EXPORT_NAME verwenden, um Module in etwas anderes zu ändern. Dies ist eine gute Praxis für Bibliotheken, da sie dann keine unnötigen Dinge im globalen Gültigkeitsbereich enthalten, und in einigen Fällen möchten Sie mehr als eine erstellen.

Verwenden von C++-Klassen in JavaScript

Nachdem die Bindung abgeschlossen ist, können C++-Objekte in JavaScript erstellt und verwendet werden, als wären sie normale JavaScript-Objekte. Zum Beispiel können Sie, das obige Beispiel fortsetzend, die Objekte Foo und Bar erstellen und Methoden darauf aufrufen.

var f = new Module.Foo();
f.setVal(200);
alert(f.getVal());

var b = new Module.Bar(123);
b.doSomething();

Wichtig

Greifen Sie Objekte immer über das Module object zu, wie oben gezeigt.

Obwohl die Objekte standardmäßig auch im globalen Namensraum verfügbar sind, gibt es Fälle, in denen dies nicht der Fall sein wird (zum Beispiel, wenn Sie den Closure Compiler verwenden, um Code zu minifizieren oder kompilierten Code in eine Funktion zu verpacken, um eine Verschmutzung des globalen Namensraums zu vermeiden). Sie können natürlich jeden Namen verwenden, den Sie für das Modul wünschen, indem Sie es einer neuen Variablen zuweisen: var MyModuleName = Module;.

Wichtig

Sie können diesen Code nur verwenden, wenn es sicher ist, kompilierten Code aufzurufen; weitere Details finden Sie in diesem FAQ-Eintrag.

JavaScript wird automatisch alle verpackten C++-Objekte per Garbage Collection entsorgen, wenn keine Referenzen mehr vorhanden sind. Wenn das C++-Objekt keine spezielle Bereinigung erfordert (d.h. es hat keinen Destruktor), dann ist keine weitere Aktion erforderlich.

Wenn ein C++-Objekt bereinigt werden muss, müssen Sie explizit Module.destroy(obj) aufrufen, um dessen Destruktor aufzurufen – und dann alle Referenzen auf das Objekt löschen, damit es von der Garbage Collection erfasst werden kann. Zum Beispiel, wenn Bar Speicher alloziieren würde, der bereinigt werden muss:

var b = new Module.Bar(123);
b.doSomething();
Module.destroy(b); // If the C++ object requires clean up

Hinweis

Der C++-Konstruktor wird transparent aufgerufen, wenn ein C++-Objekt in JavaScript erstellt wird. Es gibt jedoch keine Möglichkeit zu erkennen, ob ein JavaScript-Objekt kurz vor der Garbage Collection steht, daher kann der Binder-Klebecode den Destruktor nicht automatisch aufrufen.

Sie müssen die Objekte, die Sie erstellen, normalerweise zerstören, aber dies hängt von der zu portierenden Bibliothek ab.

Attribute

Objektattribute werden in IDL mit dem Schlüsselwort attribute definiert. Diese können dann in JavaScript entweder über get_foo()/set_foo() Zugriffs-Methoden oder direkt als Eigenschaft des Objekts zugegriffen werden.

// C++
int attr;
// WebIDL
attribute long attr;
// JavaScript
var f = new Module.Foo();
f.attr = 7;
// Equivalent to:
f.set_attr(7);

console.log(f.attr);
console.log(f.get_attr());

Für schreibgeschützte Attribute siehe Const.

Zeiger, Referenzen, Werttypen (Ref und Value)

C++-Argumente und Rückgabetypen können Zeiger, Referenzen oder Werttypen (auf dem Stack alloziert) sein. Die IDL-Datei verwendet unterschiedliche Dekorationen, um jeden dieser Fälle darzustellen.

Undekorierte Argument- und Rückgabewerte eines benutzerdefinierten Typs in der IDL werden in C++ als Zeiger angenommen.

// C++
MyClass* process(MyClass* input);
// WebIDL
MyClass process(MyClass input);

Diese Annahme trifft nicht auf Basistypen wie void, int, bool, DOMString usw. zu.

Referenzen sollten mit [Ref] dekoriert werden.

// C++
MyClass& process(MyClass& input);
// WebIDL
[Ref] MyClass process([Ref] MyClass input);

Hinweis

Wenn [Ref] bei einer Referenz weggelassen wird, kompiliert der generierte Klebe-C++-Code nicht (er schlägt fehl, wenn er versucht, die Referenz – die er für einen Zeiger hält – in ein Objekt zu konvertieren).

Wenn der C++-Code ein Objekt (anstatt einer Referenz oder eines Zeigers) zurückgibt, sollte der Rückgabetyp mit [Value] dekoriert werden. Dies wird eine statische (Singleton-)Instanz dieser Klasse allozieren und zurückgeben. Sie sollten sie sofort verwenden und alle Referenzen darauf nach Gebrauch löschen.

// C++
MyClass process(MyClass& input);
// WebIDL
[Value] MyClass process([Ref] MyClass input);

Konst

C++-Argumente oder Rückgabetypen, die const verwenden, können in IDL mit [Const] angegeben werden.

Zum Beispiel zeigen die folgenden Codefragmente den C++- und IDL-Code für eine Funktion, die ein konstantes Zeigerobjekt zurückgibt.

//C++
const myObject* getAsConst();
// WebIDL
[Const] myObject getAsConst();

Attribute, die konstanten Datenmembern entsprechen, müssen mit dem Schlüsselwort readonly angegeben werden, nicht mit [Const]. Zum Beispiel:

//C++
const int numericalConstant;
// WebIDL
readonly attribute long numericalConstant;

Dies erzeugt eine Methode get_numericalConstant() in den Bindungen, aber keinen entsprechenden Setter. Das Attribut wird auch in JavaScript als schreibgeschützt definiert, was bedeutet, dass der Versuch, es zu setzen, keine Auswirkung auf den Wert hat und im Strikt-Modus einen Fehler auslöst.

Tipp

Es ist möglich, dass ein Rückgabetyp mehrere Spezifizierer hat. Zum Beispiel würde eine Methode, die eine konstante Referenz zurückgibt, in der IDL mit [Ref, Const] markiert werden.

Nicht löschbare Klassen (NoDelete)

Wenn eine Klasse nicht gelöscht werden kann (weil der Destruktor privat ist), geben Sie [NoDelete] in der IDL-Datei an.

[NoDelete]
interface Foo {
...
};

Definieren von inneren Klassen und Klassen innerhalb von Namensräumen (Prefix)

C++-Klassen, die innerhalb eines Namensraums (oder einer anderen Klasse) deklariert sind, müssen das Schlüsselwort Prefix der IDL-Datei verwenden, um den Gültigkeitsbereich anzugeben. Das Präfix wird dann immer verwendet, wenn die Klasse im C++-Klebecode referenziert wird.

Zum Beispiel stellt die folgende IDL-Definition sicher, dass die Klasse Inner als MyNameSpace::Inner bezeichnet wird:

[Prefix="MyNameSpace::"]
interface Inner {
..
};

Operatoren

Sie können sich mit C++-Operatoren über [Operator=] verbinden.

[Operator="+="] TYPE1 add(TYPE2 x);

Hinweis

  • Der Operatorname kann beliebig sein (add ist nur ein Beispiel).

  • Die Unterstützung ist derzeit auf die folgenden binären Operatoren beschränkt: +, -, *, /, %, ^, &, |, =, <, >, +=, -=, *=, /=, %=, ^=, &=, |=, <<, >>, >>=, <<=, ==, !=, <=, >=, <=>, &&, ||, und auf den Array-Indizierungsoperator [].

Enums

Enums werden in C++ und IDL sehr ähnlich deklariert:

// C++
enum AnEnum {
  enum_value1,
  enum_value2
};

// WebIDL
enum AnEnum {
  "enum_value1",
  "enum_value2"
};

Die Syntax ist für Enums, die innerhalb eines Namensraums deklariert sind, etwas komplizierter:

// C++
namespace EnumNamespace {
  enum EnumInNamespace {
  e_namespace_val = 78
  };
};

// WebIDL
enum EnumNamespace_EnumInNamespace {
  "EnumNamespace::e_namespace_val"
};

Wenn das Enum innerhalb einer Klasse definiert ist, sind die IDL-Definitionen für das Enum und die Klassenschnittstelle getrennt:

// C++
class EnumClass {
 public:
  enum EnumWithinClass {
  e_val = 34
  };
  EnumWithinClass GetEnum() { return e_val; }

  EnumNamespace::EnumInNamespace GetEnumFromNameSpace() { return EnumNamespace::e_namespace_val; }
};



// WebIDL
enum EnumClass_EnumWithinClass {
  "EnumClass::e_val"
};

interface EnumClass {
  void EnumClass();

  EnumClass_EnumWithinClass GetEnum();

  EnumNamespace_EnumInNamespace GetEnumFromNameSpace();
};

Unterklassifizierung von C++-Basisklassen in JavaScript (JSImplementation)

Der WebIDL Binder ermöglicht es, C++-Basisklassen in JavaScript zu unterklassifizieren. Im folgenden IDL-Fragment bedeutet JSImplementation="Base", dass die zugehörige Schnittstelle (ImplJS) eine JavaScript-Implementierung der C++-Klasse Base sein wird.

[JSImplementation="Base"]
interface ImplJS {
  void ImplJS();
  void virtualFunc();
  void virtualFunc2();
};

Nach dem Ausführen des Bindungsgenerators und dem Kompilieren können Sie die Schnittstelle wie gezeigt in JavaScript implementieren:

var c = new ImplJS();
c.virtualFunc = function() { .. };

Wenn C++-Code einen Zeiger auf eine Base-Instanz hat und virtualFunc() aufruft, erreicht dieser Aufruf den oben definierten JavaScript-Code.

Hinweis

  • Sie müssen alle Methoden implementieren, die Sie in der IDL der JSImplementation-Klasse (ImplJS) erwähnt haben, da sonst die Kompilierung mit einem Fehler fehlschlägt.

  • Sie müssen auch eine Schnittstellendefinition für die Klasse Base in der IDL-Datei bereitstellen.

Zeiger und Vergleiche

Alle Bindungsfunktionen erwarten Wrapper-Objekte (die einen Rohzeiger enthalten) und keine Rohzeiger. Normalerweise sollten Sie sich nicht mit Rohzeigern befassen müssen (dies sind einfach Speicheradressen/Ganzzahlen). Wenn doch, können die folgenden Funktionen im kompilierten Code nützlich sein:

  • wrapPointer(ptr, Class) – Gibt ein gewrapptes Objekt für einen Rohzeiger (eine Ganzzahl) zurück.

    Hinweis

    Wenn Sie die Class nicht übergeben, wird angenommen, dass es sich um die Root-Klasse handelt – das ist wahrscheinlich nicht das, was Sie wollen!

  • getPointer(object) – Gibt einen Rohzeiger zurück.

  • castObject(object, Class) – Gibt eine Umhüllung desselben Zeigers, aber für eine andere Klasse zurück.

  • compare(object1, object2) – Vergleicht die Zeiger zweier Objekte.

Hinweis

Es gibt immer ein einziges umhülltes Objekt für einen bestimmten Zeiger auf eine bestimmte Klasse. Dadurch können Sie Daten zu diesem Objekt hinzufügen und es an anderer Stelle mit normaler JavaScript-Syntax verwenden (object.attribute = someData usw.).

compare() sollte anstelle eines direkten Zeigervergleichs verwendet werden, da es möglich ist, verschiedene umhüllte Objekte mit demselben Zeiger zu haben, wenn eine Klasse eine Unterklasse der anderen ist.

NULL

Alle Bindungsfunktionen, die Zeiger, Referenzen oder Objekte zurückgeben, geben gewrappte Zeiger zurück. Der Grund dafür ist, dass durch die ständige Rückgabe eines Wrappers die Ausgabe an eine andere Bindungsfunktion übergeben werden kann, ohne dass diese Funktion den Typ des Arguments überprüfen muss.

Ein Fall, der hier verwirrend sein kann, ist die Rückgabe eines NULL-Zeigers. Bei der Verwendung von Bindungen wird der zurückgegebene Zeiger NULL (ein globales Singleton mit einem gewrappten Zeiger von 0) anstelle von null (dem integrierten JavaScript-Objekt) oder 0 sein.

void*

Der Typ void* wird durch einen Typ VoidPtr unterstützt, den Sie in IDL-Dateien verwenden können. Sie können auch den Typ any verwenden.

Der Unterschied zwischen ihnen besteht darin, dass VoidPtr sich wie ein Zeigertyp verhält, indem Sie ein Wrapper-Objekt erhalten, während any sich wie eine 32-Bit-Ganzzahl verhält (was Rohzeiger in Emscripten-kompiliertem Code sind).

WebIDL-Typen

Die Typnamen in WebIDL sind nicht identisch mit denen in C++. Dieser Abschnitt zeigt die Zuordnung für die häufigsten Typen, denen Sie begegnen werden.

C++

IDL

bool

boolean

float

float

double

double

char

byte

char*

DOMString (repräsentiert einen JavaScript-String)

unsigned char

octet

int

long

long

long

unsigned short

unsigned short

unsigned long

unsigned long

long long

long long

void

void

void*

any oder VoidPtr (siehe void*)

Hinweis

Die WebIDL-Typen sind vollständig in dieser W3C-Spezifikation dokumentiert.

Test- und Beispielcode

Für ein vollständiges funktionierendes Beispiel siehe test_webidl in der Testsuite. Der Code der Testsuite funktioniert garantiert und behandelt mehr Fälle als dieser Artikel allein.

Ein weiteres gutes Beispiel ist ammo.js, das den WebIDL Binder verwendet, um die Bullet Physics Engine ins Web zu portieren.