Embind wird verwendet, um C++-Funktionen und -Klassen an JavaScript zu binden, sodass der kompilierte Code auf natürliche Weise von „normalem“ JavaScript verwendet werden kann. Embind unterstützt auch das Aufrufen von JavaScript-Klassen aus C++.
Embind unterstützt die Bindung der meisten C++-Konstrukte, einschließlich der in C++11 und C++14 eingeführten. Die einzige signifikante Einschränkung besteht darin, dass derzeit keine Roh-Pointer (Raw Pointers) mit komplizierter Lebensdauer-Semantik unterstützt werden.
Dieser Artikel zeigt, wie man EMSCRIPTEN_BINDINGS()-Blöcke verwendet, um Bindungen für Funktionen, Klassen, Werttypen, Pointer (sowohl Roh- als auch Smart-Pointer), Enums und Konstanten zu erstellen, und wie man Bindungen für abstrakte Klassen erstellt, die in JavaScript überschrieben werden können. Er erklärt außerdem kurz, wie der Speicher von C++-Objekt-Handles verwaltet wird, die an JavaScript übergeben wurden.
Tipp
Zusätzlich zum Code in diesem Artikel
Gibt es viele weitere Beispiele zur Verwendung von Embind in der Test Suite.
Connecting C++ and JavaScript on the Web with Embind (Folien von der CppCon 2014) enthält weitere Beispiele und Informationen über die Designphilosophie und Implementierung von Embind.
Hinweis
Embind wurde von Boost.Python inspiriert und verwendet einen sehr ähnlichen Ansatz zur Definition von Bindungen.
Der folgende Code verwendet einen EMSCRIPTEN_BINDINGS()-Block, um die einfache C++-Funktion lerp() via function() für JavaScript verfügbar zu machen.
// quick_example.cpp
#include <emscripten/bind.h>
using namespace emscripten;
float lerp(float a, float b, float t) {
return (1 - t) * a + t * b;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("lerp", &lerp);
}
Um das obige Beispiel mit embind zu kompilieren, rufen wir emcc mit der Option bind auf.
emcc -lembind -o quick_example.js quick_example.cpp
Die resultierende Datei quick_example.js kann als Node-Modul oder über ein <script>-Tag geladen werden.
<!doctype html>
<html>
<script>
var Module = {
onRuntimeInitialized: function() {
console.log('lerp result: ' + Module.lerp(1, 2, 0.5));
}
};
</script>
<script src="quick_example.js"></script>
</html>
Hinweis
Wir verwenden den onRuntimeInitialized-Callback, um Code auszuführen, wenn die Runtime bereit ist, was eine asynchrone Operation ist (um WebAssembly zu kompilieren).
Hinweis
Öffnen Sie die Konsole der Entwicklertools, um die Ausgabe von console.log zu sehen.
Der Code in einem EMSCRIPTEN_BINDINGS()-Block wird ausgeführt, wenn die JavaScript-Datei initial geladen wird (zeitgleich mit den globalen Konstruktoren). Die Parametertypen und der Rückgabetyp der Funktion lerp() werden automatisch von embind abgeleitet.
Alle Symbole, die durch embind exportiert werden, sind auf dem Emscripten-Module-Objekt verfügbar.
Wichtig
Greifen Sie immer über das Module-Objekt auf Objekte 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 ist (z. B. wenn Sie den Closure Compiler zur Minimierung verwenden oder den kompilierten Code in eine Funktion einhüllen, um den globalen Namensraum nicht zu verschmutzen). Sie können natürlich einen beliebigen Namen für das Modul verwenden, indem Sie es einer neuen Variablen zuweisen: var MeinModulName = Module;.
Bindungs-Code wird als statischer Konstruktor ausgeführt. Statische Konstruktoren werden nur ausgeführt, wenn die Objektdatei in den Link-Vorgang einbezogen wird. Daher muss der Compiler beim Generieren von Bindungen für Bibliotheksdateien explizit angewiesen werden, die Objektdatei einzuschließen.
Um beispielsweise Bindungen für eine hypothetische, mit Emscripten kompilierte library.a zu generieren, führen Sie emcc mit dem Compiler-Flag --whole-archive aus.
emcc -lembind -o library.js -Wl,--whole-archive library.a -Wl,--no-whole-archive
Das Exponieren von Klassen an JavaScript erfordert eine komplexere Bindungs-Anweisung. Zum Beispiel:
class MyClass {
public:
MyClass(int x, std::string y)
: x(x)
, y(y)
{}
void incrementX() {
++x;
}
int getX() const { return x; }
void setX(int x_) { x = x_; }
static std::string getStringFromInstance(const MyClass& instance) {
return instance.y;
}
private:
int x;
std::string y;
};
// Binding code
EMSCRIPTEN_BINDINGS(my_class_example) {
class_<MyClass>("MyClass")
.constructor<int, std::string>()
.function("incrementX", &MyClass::incrementX)
.property("x", &MyClass::getX, &MyClass::setX)
.property("x_readonly", &MyClass::getX)
.class_function("getStringFromInstance", &MyClass::getStringFromInstance)
;
}
Der Bindungs-Block definiert eine Kette von Elementfunktionsaufrufen auf dem temporären class_-Objekt (dieser Stil wird auch in Boost.Python verwendet). Die Funktionen registrieren die Klasse, ihren constructor(), die Elementfunktion function(), die (statische) class_function() und die property().
Hinweis
Dieser Bindungs-Block bindet die Klasse und alle ihre Methoden. In der Regel sollten Sie nur die Elemente binden, die tatsächlich benötigt werden, da jede Bindung die Codegröße erhöht. Beispielsweise wäre es selten sinnvoll, private oder interne Methoden zu binden.
Eine Instanz von MyClass kann dann in JavaScript erstellt und verwendet werden, wie unten gezeigt:
var instance = new Module.MyClass(10, "hello");
instance.incrementX();
instance.x; // 11
instance.x = 20; // 20
Module.MyClass.getStringFromInstance(instance); // "hello"
instance.delete();
Hinweis
Der Closure Compiler erkennt die Namen der Symbole nicht, die über Embind an JavaScript exponiert werden. Um zu verhindern, dass solche Symbole durch den Closure Compiler in Ihrem eigenen Code (der beispielsweise über die Flags --pre-js oder --post-js bereitgestellt wird) umbenannt werden, ist es notwendig, den Code entsprechend zu annotieren. Ohne solche Annotationen stimmt der resultierende JavaScript-Code nicht mehr mit den in Embind verwendeten Symbolnamen überein, was zu Laufzeitfehlern führt.
Um zu verhindern, dass der Closure Compiler die Symbole im obigen Beispielcode umbenennt, muss dieser wie folgt umgeschrieben werden:
var instance = new Module["MyClass"](10, "hello");
instance["incrementX"]();
instance["x"]; // 11
instance["x"] = 20; // 20
Module["MyClass"]["getStringFromInstance"](instance); // "hello"
instance.delete();
Beachten Sie, dass dies nur für Code erforderlich ist, den der Optimizer sieht, wie etwa in --pre-js, --post-js, EM_ASM oder EM_JS. Für anderen Code, der nicht vom Closure Compiler optimiert wird, müssen Sie diese Änderungen nicht vornehmen. Sie benötigen dies auch nicht, wenn Sie ohne --closure 1 bauen.
Die JavaScript-Methode delete() wird bereitgestellt, um manuell zu signalisieren, dass ein C++-Objekt nicht mehr benötigt wird und gelöscht werden kann:
var x = new Module.MyClass;
x.method();
x.delete();
var y = Module.myFunctionThatReturnsClassInstance();
y.method();
y.delete();
Hinweis
Sowohl C++-Objekte, die auf der JavaScript-Seite konstruiert wurden, als auch solche, die von C++-Methoden zurückgegeben wurden, müssen explizit gelöscht werden, es sei denn, es wird eine reference-Rückgabewert-Policy verwendet (siehe unten).
Tipp
Das JavaScript-Konstrukt try … finally kann verwendet werden, um sicherzustellen, dass C++-Objekt-Handles in allen Codepfaden gelöscht werden, unabhängig von vorzeitigen Rückgaben oder geworfenen Fehlern.
function myFunction() {
const x = new Module.MyClass;
try {
if (someCondition) {
return; // !
}
someFunctionThatMightThrow(); // oops
x.method();
} finally {
x.delete(); // will be called no matter what
}
}
JavaScript erhielt Unterstützung für Finalizer erst in ECMAScript 2021 (ECMA-262 Edition 12). Die neue API heißt FinalizationRegistry und bietet immer noch keine Garantie, dass der bereitgestellte Finalisierungs-Callback tatsächlich aufgerufen wird. Embind verwendet dies zur Bereinigung, falls verfügbar, aber nur für Smart-Pointer und nur als letzten Ausweg.
Warnung
Es wird dringend empfohlen, dass JavaScript-Code alle empfangenen C++-Objekt-Handles explizit löscht.
Es gibt Situationen, in denen mehrere langlebige Teile der JavaScript-Codebasis dasselbe C++-Objekt unterschiedlich lange halten müssen.
Um diesen Anwendungsfall abzudecken, bietet Emscripten einen Mechanismus zur Referenzzählung (Reference Counting) an, bei dem mehrere Handles für dasselbe zugrunde liegende C++-Objekt erzeugt werden können. Erst wenn alle Handles gelöscht wurden, wird das Objekt zerstört.
Die JavaScript-Methode clone() gibt ein neues Handle zurück. Dieses muss schließlich ebenfalls mit delete() freigegeben werden.
async function myLongRunningProcess(x, milliseconds) {
// sleep for the specified number of milliseconds
await new Promise(resolve => setTimeout(resolve, milliseconds));
x.method();
x.delete();
}
const y = new Module.MyClass; // refCount = 1
myLongRunningProcess(y.clone(), 5000); // refCount = 2
myLongRunningProcess(y.clone(), 3000); // refCount = 3
y.delete(); // refCount = 2
// (after 3000ms) refCount = 1
// (after 5000ms) refCount = 0 -> object is deleted
Die manuelle Speicherverwaltung für Basistypen ist mühsam, daher bietet embind Unterstützung für Werttypen. Value arrays werden in JavaScript-Arrays umgewandelt (und umgekehrt), und Value objects in JavaScript-Objekte.
Betrachten Sie das folgende Beispiel:
struct Point2f {
float x;
float y;
};
struct PersonRecord {
std::string name;
int age;
};
// Array fields are treated as if they were std::array<type,size>
struct ArrayInStruct {
int field[2];
};
PersonRecord findPersonAtLocation(Point2f);
EMSCRIPTEN_BINDINGS(my_value_example) {
value_array<Point2f>("Point2f")
.element(&Point2f::x)
.element(&Point2f::y)
;
value_object<PersonRecord>("PersonRecord")
.field("name", &PersonRecord::name)
.field("age", &PersonRecord::age)
;
value_object<ArrayInStruct>("ArrayInStruct")
.field("field", &ArrayInStruct::field) // Need to register the array type
;
// Register std::array<int, 2> because ArrayInStruct::field is interpreted as such
value_array<std::array<int, 2>>("array_int_2")
.element(index<0>())
.element(index<1>())
;
function("findPersonAtLocation", &findPersonAtLocation);
}
Der JavaScript-Code muss sich nicht um die Lebensdauerverwaltung kümmern.
var person = Module.findPersonAtLocation([10.2, 156.5]);
console.log('Found someone! Their name is ' + person.name + ' and they are ' + person.age + ' years old');
JavaScript und C++ haben sehr unterschiedliche Speichermodelle. Dies kann dazu führen, dass unklar ist, welche Sprache ein Objekt besitzt und für dessen Löschung verantwortlich ist, wenn es zwischen den Sprachen wechselt. Um den Objektbesitz expliziter zu machen, unterstützt embind Smart-Pointer und Rückgabetyp-Policies (Return Value Policies). Diese Policies legen fest, was mit einem C++-Objekt passiert, wenn es an JavaScript zurückgegeben wird.
Um eine Rückgabetyp-Policy zu verwenden, übergeben Sie die gewünschte Policy an die Bindungen für Funktionen, Methoden oder Properties. Zum Beispiel:
EMSCRIPTEN_BINDINGS(module) {
function("createData", &createData, return_value_policy::take_ownership());
}
Embind unterstützt drei Rückgabetyp-Policies, die sich je nach Rückgabetyp der Funktion unterschiedlich verhalten:
default (kein Argument) - Bei Rückgabe als Wert oder Referenz wird ein neues Objekt mit dem Kopierkonstruktor des Objekts allokiert. JS besitzt dann das Objekt und ist für dessen Löschung verantwortlich. Die Rückgabe eines Pointers ist standardmäßig nicht erlaubt (verwenden Sie eine explizite Policy unten).
return_value_policy::take_ownership - Der Besitz wird auf JS übertragen.
return_value_policy::reference - Referenziert ein bestehendes Objekt, übernimmt aber keinen Besitz. Es muss darauf geachtet werden, das Objekt nicht zu löschen, während es in JS noch in Verwendung ist.
Weitere Details unten:
Rückgabetyp |
Konstruktor |
Bereinigung |
|---|---|---|
default |
||
Wert ( |
copy |
JS muss das kopierte Objekt löschen. |
Referenz ( |
copy |
JS muss das kopierte Objekt löschen. |
Pointer ( |
n.v. |
Pointer müssen explizit eine Return-Policy verwenden. |
take_ownership |
||
Wert ( |
move |
JS muss das verschobene Objekt löschen. |
Referenz ( |
move |
JS muss das verschobene Objekt löschen. |
Pointer ( |
keiner |
JS muss das Objekt löschen. |
reference |
||
Wert ( |
n.v. |
Referenz auf einen Wert ist nicht erlaubt. |
Referenz ( |
keiner |
C++ muss das Objekt löschen. |
Pointer ( |
keiner |
C++ muss das Objekt löschen. |
Da Roh-Pointer eine unklare Lebensdauer-Semantik haben, verlangt embind, dass deren Verwendung entweder mit allow_raw_pointers oder einer return_value_policy markiert wird. Wenn die Funktion einen Pointer zurückgibt, wird empfohlen, eine return_value_policy anstelle des allgemeinen allow_raw_pointers zu verwenden.
Zum Beispiel:
class C {};
C* passThrough(C* ptr) { return ptr; }
C* createC() { return new C(); }
EMSCRIPTEN_BINDINGS(raw_pointers) {
class_<C>("C");
function("passThrough", &passThrough, allow_raw_pointers());
function("createC", &createC, return_value_policy::take_ownership());
}
Hinweis
Derzeit dient allow_raw_pointers für Pointer-Argumente nur dazu, die Verwendung von Roh-Pointern zu erlauben und zu zeigen, dass Sie über deren Einsatz nachgedacht haben. Wir hoffen, in Zukunft Boost.Python-ähnliche Raw-Pointer-Policies für die Verwaltung des Objektbesitzes von Argumenten zu implementieren.
Es gibt zwei Möglichkeiten, Konstruktoren für eine Klasse anzugeben.
Die Zero-Argument-Template-Form ruft den natürlichen Konstruktor mit den im Template angegebenen Argumenten auf. Zum Beispiel:
class MyClass {
public:
MyClass(int, float);
void someFunction();
};
EMSCRIPTEN_BINDINGS(external_constructors) {
class_<MyClass>("MyClass")
.constructor<int, float>()
.function("someFunction", &MyClass::someFunction)
;
}
Die zweite Form des Konstruktors nimmt ein Funktionspointer-Argument an und wird für Klassen verwendet, die sich selbst über eine Factory-Funktion konstruieren. Zum Beispiel:
class MyClass {
virtual void someFunction() = 0;
};
MyClass* makeMyClass(int, float); //Factory function.
EMSCRIPTEN_BINDINGS(external_constructors) {
class_<MyClass>("MyClass")
.constructor(&makeMyClass, allow_raw_pointers())
.function("someFunction", &MyClass::someFunction)
;
}
Die beiden Konstruktoren bieten genau dasselbe Interface zur Konstruktion des Objekts in JavaScript. Fortführung des obigen Beispiels:
var instance = new MyClass(10, 15.5);
// instance is backed by a raw pointer to a MyClass in the Emscripten heap
Um die Objektlebensdauer mit Smart-Pointern zu verwalten, muss embind über den Smart-Pointer-Typ informiert werden.
Betrachten Sie beispielsweise die Verwaltung der Lebensdauer einer Klasse C mit std::shared_ptr<C>. Am besten registriert man den Smart-Pointer-Typ mit smart_ptr_constructor():
EMSCRIPTEN_BINDINGS(better_smart_pointers) {
class_<C>("C")
.smart_ptr_constructor("C", &std::make_shared<C>)
;
}
Wenn ein Objekt dieses Typs konstruiert wird (z. B. mit new Module.C()), gibt es ein std::shared_ptr<C> zurück.
Eine Alternative ist die Verwendung von smart_ptr() im EMSCRIPTEN_BINDINGS()-Block:
EMSCRIPTEN_BINDINGS(smart_pointers) {
class_<C>("C")
.constructor<>()
.smart_ptr<std::shared_ptr<C>>("C")
;
}
Mit dieser Definition können Funktionen std::shared_ptr<C> zurückgeben oder als Argumente annehmen, aber new Module.C() würde immer noch einen Roh-Pointer zurückgeben.
embind hat integrierte Unterstützung für Rückgabewerte vom Typ std::unique_ptr.
Um embind über eigene Smart-Pointer-Templates zu informieren, müssen Sie das smart_ptr_trait-Template spezialisieren.
Methoden auf dem JavaScript-Klassenprototyp können Nicht-Elementfunktionen sein, solange das Instanz-Handle in das erste Argument der Nicht-Elementfunktion konvertiert werden kann. Das klassische Beispiel ist, wenn die für JavaScript exponierte Funktion nicht exakt dem Verhalten einer C++-Methode entspricht.
struct Array10 {
int& get(size_t index) {
return data[index];
}
int data[10];
};
val Array10_get(Array10& arr, size_t index) {
if (index < 10) {
return val(arr.get(index));
} else {
return val::undefined();
}
}
EMSCRIPTEN_BINDINGS(non_member_functions) {
class_<Array10>("Array10")
.function("get", &Array10_get)
;
}
Wenn JavaScript Array10.prototype.get mit einem ungültigen Index aufruft, gibt es undefined zurück.
Wenn C++-Klassen virtuelle oder abstrakte Elementfunktionen haben, ist es möglich, diese in JavaScript zu überschreiben. Da JavaScript keine Kenntnis von der C++-Vtable hat, benötigt embind etwas Glue-Code, um virtuelle C++-Funktionsaufrufe in JavaScript-Aufrufe zu konvertieren.
Beginnen wir mit einem einfachen Fall: rein virtuelle Funktionen, die in JavaScript implementiert werden müssen.
struct Interface {
virtual ~Interface() {}
virtual void invoke(const std::string& str) = 0;
};
struct InterfaceWrapper : public wrapper<Interface> {
EMSCRIPTEN_WRAPPER(InterfaceWrapper);
void invoke(const std::string& str) {
return call<void>("invoke", str);
}
};
EMSCRIPTEN_BINDINGS(interface) {
class_<Interface>("Interface")
.function("invoke", &Interface::invoke, pure_virtual())
.allow_subclass<InterfaceWrapper>("InterfaceWrapper")
;
}
allow_subclass() fügt der Interface-Bindung zwei spezielle Methoden hinzu: extend und implement. extend ermöglicht es JavaScript, Unterklassen in einem Stil zu erstellen, wie er etwa durch Backbone.js beispielhaft ist. implement wird verwendet, wenn Sie ein JavaScript-Objekt haben (vielleicht vom Browser oder einer anderen Bibliothek bereitgestellt) und dieses zur Implementierung eines C++-Interfaces nutzen möchten.
Hinweis
Die pure_virtual-Annotation an der Funktionsbindung erlaubt es JavaScript, einen hilfreichen Fehler zu werfen, falls die JavaScript-Klasse invoke() nicht überschreibt. Andernfalls könnten verwirrende Fehler auftreten.
extend Beispiel¶var DerivedClass = Module.Interface.extend("Interface", {
// __construct and __destruct are optional. They are included
// in this example for illustration purposes.
// If you override __construct or __destruct, don't forget to
// call the parent implementation!
__construct: function() {
this.__parent.__construct.call(this);
},
__destruct: function() {
this.__parent.__destruct.call(this);
},
invoke: function() {
// your code goes here
},
});
var instance = new DerivedClass;
implement Beispiel¶var x = {
invoke: function(str) {
console.log('invoking with: ' + str);
}
};
var interfaceObject = Module.Interface.implement(x);
Nun kann interfaceObject an jede Funktion übergeben werden, die einen Interface-Pointer oder eine Referenz erwartet.
Wenn eine C++-Klasse eine nicht-rein virtuelle Funktion hat, kann diese überschrieben werden – muss aber nicht. Dies erfordert eine etwas andere Wrapper-Implementierung:
struct Base {
virtual void invoke(const std::string& str) {
// default implementation
}
};
struct BaseWrapper : public wrapper<Base> {
EMSCRIPTEN_WRAPPER(BaseWrapper);
void invoke(const std::string& str) {
return call<void>("invoke", str);
}
};
EMSCRIPTEN_BINDINGS(interface) {
class_<Base>("Base")
.allow_subclass<BaseWrapper>("BaseWrapper")
.function("invoke", optional_override([](Base& self, const std::string& str) {
return self.Base::invoke(str);
}))
;
}
Bei der Implementierung von Base mit einem JavaScript-Objekt ist das Überschreiben von invoke optional. Die spezielle Lambda-Bindung für invoke ist notwendig, um unendliche gegenseitige Rekursion zwischen dem Wrapper und JavaScript zu vermeiden.
Basisklassen-Bindungen werden wie folgt definiert:
EMSCRIPTEN_BINDINGS(base_example) {
class_<BaseClass>("BaseClass");
class_<DerivedClass, base<BaseClass>>("DerivedClass");
}
Alle auf BaseClass definierten Elementfunktionen sind dann für Instanzen von DerivedClass zugänglich. Darüber hinaus kann jeder Funktion, die eine Instanz von BaseClass akzeptiert, eine Instanz von DerivedClass übergeben werden.
Wenn eine C++-Klasse polymorph ist (d. h. sie hat eine virtuelle Methode), unterstützt embind das automatische Downcasting von Funktionsrückgabewerten.
class Base { virtual ~Base() {} }; // the virtual makes Base and Derived polymorphic
class Derived : public Base {};
Base* getDerivedInstance() {
return new Derived;
}
EMSCRIPTEN_BINDINGS(automatic_downcasting) {
class_<Base>("Base");
class_<Derived, base<Base>>("Derived");
function("getDerivedInstance", &getDerivedInstance, allow_raw_pointers());
}
Der Aufruf von Module.getDerivedInstance aus JavaScript gibt ein Derived-Instanz-Handle zurück, von dem aus alle Methoden von Derived verfügbar sind.
Hinweis
Damit das automatische Downcasting funktioniert, muss Embind den am weitesten abgeleiteten Typ (fully-derived type) kennen.
Hinweis
Embind unterstützt dies nur, wenn RTTI aktiviert ist.
Konstruktoren und Funktionen können basierend auf der Anzahl der Argumente überladen werden, aber embind unterstützt keine Überladung basierend auf dem Typ. Verwenden Sie beim Angeben einer Überladung die Hilfsfunktion select_overload(), um die passende Signatur auszuwählen.
struct HasOverloadedMethods {
void foo();
void foo(int i);
void foo(float f) const;
};
EMSCRIPTEN_BINDING(overloads) {
class_<HasOverloadedMethods>("HasOverloadedMethods")
.function("foo", select_overload<void()>(&HasOverloadedMethods::foo))
.function("foo_int", select_overload<void(int)>(&HasOverloadedMethods::foo))
.function("foo_float", select_overload<void(float)const>(&HasOverloadedMethods::foo))
;
}
Embinds Unterstützung für Enumerationen funktioniert sowohl mit C++98-Enums als auch mit C++11 "enum classes".
enum OldStyle {
OLD_STYLE_ONE,
OLD_STYLE_TWO
};
enum class NewStyle {
ONE,
TWO
};
EMSCRIPTEN_BINDINGS(my_enum_example) {
enum_<OldStyle>("OldStyle")
.value("ONE", OLD_STYLE_ONE)
.value("TWO", OLD_STYLE_TWO)
;
enum_<NewStyle>("NewStyle")
.value("ONE", NewStyle::ONE)
.value("TWO", NewStyle::TWO)
;
}
In beiden Fällen greift JavaScript auf die Enumerationswerte als Properties des Typs zu.
Module.OldStyle.ONE;
Module.NewStyle.TWO;
Um eine C++-constant() für JavaScript zu exponieren, schreiben Sie einfach:
EMSCRIPTEN_BINDINGS(my_constant_example) {
constant("SOME_CONSTANT", SOME_CONSTANT);
}
SOME_CONSTANT kann jeder Typ sein, der embind bekannt ist.
Warnung
Standardmäßig verwenden property()-Bindungen an Objekte return_value_policy::copy, was leicht zu Speicherlecks führen kann, da jeder Zugriff auf die Property ein neues Objekt erstellt, das gelöscht werden muss. Alternativ verwenden Sie return_value_policy::reference, sodass kein neues Objekt allokiert wird und Änderungen am Objekt im Originalobjekt reflektiert werden.
Klassen-Properties können auf verschiedene Arten definiert werden, wie unten zu sehen.
struct Point {
float x;
float y;
};
struct Person {
Point location;
Point getLocation() const { // Note: const is required on getters
return location;
}
void setLocation(Point p) {
location = p;
}
};
EMSCRIPTEN_BINDINGS(xxx) {
class_<Person>("Person")
.constructor<>()
// Bind directly to a class member with automatically generated getters/setters using a
// reference return policy so the object does not need to be deleted JS.
.property("location", &Person::location, return_value_policy::reference())
// Same as above, but this will return a copy and the object must be deleted or it will
// leak!
.property("locationCopy", &Person::location)
// Bind using a only getter method for read only access.
.property("readOnlyLocation", &Person::getLocation, return_value_policy::reference())
// Bind using a getter and setter method.
.property("getterAndSetterLocation", &Person::getLocation, &Person::setLocation,
return_value_policy::reference());
class_<Point>("Point")
.property("x", &Point::x)
.property("y", &Point::y);
}
int main() {
EM_ASM(
let person = new Module.Person();
person.location.x = 42;
console.log(person.location.x); // 42
let locationCopy = person.locationCopy;
// This is a copy so the original person's location will not be updated.
locationCopy.x = 99;
console.log(locationCopy.x); // 99
// Important: delete any copies!
locationCopy.delete();
console.log(person.readOnlyLocation.x); // 42
console.log(person.getterAndSetterLocation.x); // 42
person.delete();
);
}
In manchen Fällen ist es wertvoll, binäre Rohdaten direkt als Typed Array für JavaScript-Code verfügbar zu machen, sodass diese ohne Kopieren verwendet werden können. Dies ist nützlich, um beispielsweise große WebGL-Texturen direkt vom Heap hochzuladen.
Memory Views sollten wie Roh-Pointer behandelt werden; Lebensdauer und Gültigkeit werden nicht von der Runtime verwaltet, und es ist leicht, Daten zu korrumpieren, wenn das zugrunde liegende Objekt modifiziert oder freigegeben wird.
#include <emscripten/bind.h>
#include <emscripten/val.h>
using namespace emscripten;
unsigned char *byteBuffer = /* ... */;
size_t bufferLength = /* ... */;
val getBytes() {
return val(typed_memory_view(bufferLength, byteBuffer));
}
EMSCRIPTEN_BINDINGS(memory_view_example) {
function("getBytes", &getBytes);
}
Der aufrufende JavaScript-Code erhält eine Typed-Array-View in den Emscripten-Heap:
var myUint8Array = Module.getBytes()
var xhr = new XMLHttpRequest();
xhr.open('POST', /* ... */);
xhr.send(myUint8Array);
Die Typed-Array-View hat den passenden Typ, wie etwa Uint8Array für ein unsigned char-Array oder einen entsprechenden Pointer.
val zur Translitteration von JavaScript nach C++¶Embind bietet eine C++-Klasse, emscripten::val, mit der Sie JavaScript-Code nach C++ translitterieren können. Mit val können Sie JavaScript-Objekte aus C++ heraus aufrufen, ihre Properties lesen und schreiben oder sie in C++-Werte wie bool, int oder std::string umwandeln.
Das folgende Beispiel zeigt, wie Sie val verwenden können, um die JavaScript Web Audio API aus C++ aufzurufen:
Hinweis
Dieses Beispiel basiert auf dem exzellenten Web Audio Tutorial: Making sine, square, sawtooth and triangle waves (stuartmemo.com). Ein noch einfacheres Beispiel finden Sie in der emscripten::val-Dokumentation.
Betrachten Sie zunächst den JavaScript-Code unten, der zeigt, wie die API verwendet wird:
// Get web audio api context
var AudioContext = window.AudioContext || window.webkitAudioContext;
// Got an AudioContext: Create context and OscillatorNode
var context = new AudioContext();
var oscillator = context.createOscillator();
// Configuring oscillator: set OscillatorNode type and frequency
oscillator.type = 'triangle';
oscillator.frequency.value = 261.63; // value in hertz - middle C
// Playing
oscillator.connect(context.destination);
oscillator.start();
// All done!
Der Code kann mit val nach C++ translitteriert werden, wie unten gezeigt:
#include <emscripten/val.h>
#include <stdio.h>
#include <math.h>
using namespace emscripten;
int main() {
val AudioContext = val::global("AudioContext");
if (!AudioContext.as<bool>()) {
printf("No global AudioContext, trying webkitAudioContext\n");
AudioContext = val::global("webkitAudioContext");
}
printf("Got an AudioContext\n");
val context = AudioContext.new_();
val oscillator = context.call<val>("createOscillator");
printf("Configuring oscillator\n");
oscillator.set("type", val("triangle"));
oscillator["frequency"].set("value", val(261.63)); // Middle C
printf("Playing\n");
oscillator.call<void>("connect", context["destination"]);
oscillator.call<void>("start", 0);
printf("All done!\n");
}
Zuerst verwenden wir global(), um das Symbol für das globale AudioContext-Objekt zu erhalten (oder webkitAudioContext, falls jenes nicht existiert). Dann nutzen wir new_(), um den Kontext zu erstellen. Von diesem Kontext aus können wir einen oscillator erstellen, seine Properties set()-ten (wiederum mit val) und dann den Ton abspielen.
Das Beispiel kann im Linux/macOS-Terminal wie folgt kompiliert werden:
emcc -O2 -Wall -Werror -lembind -o oscillator.html oscillator.cpp
Standardmäßig bietet embind Konverter für viele Standard-C++-Typen:
C++ Typ |
JavaScript Typ |
|---|---|
|
undefined |
|
true oder false |
|
Number |
|
Number |
|
Number |
|
Number |
|
Number |
|
Number |
|
Number |
|
Number oder BigInt* |
|
Number oder BigInt* |
|
Number |
|
Number |
|
BigInt** |
|
BigInt** |
|
ArrayBuffer, Uint8Array, Uint8ClampedArray, Int8Array oder String |
|
String (UTF-16 Code-Units) |
|
beliebig |
*BigInt wenn MEMORY64 verwendet wird, andernfalls Number.
**Erfordert, dass BigInt-Unterstützung mit dem Flag -sWASM_BIGINT aktiviert ist.
Der Einfachheit halber bietet embind Factory-Funktionen zur Registrierung von Typen wie std::vector<T> (register_vector()), std::map<K, V> (register_map()) und std::optional<T> (register_optional()):
EMSCRIPTEN_BINDINGS(stl_wrappers) {
register_vector<int>("VectorInt");
register_map<int,int>("MapIntInt");
register_optional<std::string>();
}
Ein vollständiges Beispiel ist unten gezeigt:
#include <emscripten/bind.h>
#include <string>
#include <vector>
#include <optional>
using namespace emscripten;
std::vector<int> returnVectorData () {
std::vector<int> v(10, 1);
return v;
}
std::map<int, std::string> returnMapData () {
std::map<int, std::string> m;
m.insert(std::pair<int, std::string>(10, "This is a string."));
return m;
}
std::optional<std::string> returnOptionalData() {
return "hello";
}
EMSCRIPTEN_BINDINGS(module) {
function("returnVectorData", &returnVectorData);
function("returnMapData", &returnMapData);
function("returnOptionalData", &returnOptionalData);
// register bindings for std::vector<int>, std::map<int, std::string>, and
// std::optional<std::string>.
register_vector<int>("vector<int>");
register_map<int, std::string>("map<int, string>");
register_optional<std::string>();
}
Das folgende JavaScript kann verwendet werden, um mit dem obigen C++ zu interagieren:
var retVector = Module['returnVectorData']();
// vector size
var vectorSize = retVector.size();
// reset vector value
retVector.set(vectorSize - 1, 11);
// push value into vector
retVector.push_back(12);
// retrieve value from the vector
for (var i = 0; i < retVector.size(); i++) {
console.log("Vector Value: ", retVector.get(i));
}
// expand vector size
retVector.resize(20, 1);
var retMap = Module['returnMapData']();
// map size
var mapSize = retMap.size();
// retrieve value from map
console.log("Map Value: ", retMap.get(10));
// figure out which map keys are available
// NB! You must call `register_vector<key_type>`
// to make vectors available
var mapKeys = retMap.keys();
for (var i = 0; i < mapKeys.size(); i++) {
var key = mapKeys.get(i);
console.log("Map key/value: ", key, retMap.get(key));
}
// reset the value at the given index position
retMap.set(10, "OtherValue");
// Optional values will return undefined if there is no value.
var optional = Module['returnOptionalData']();
if (optional !== undefined) {
console.log(optional);
}
Embind unterstützt die Generierung von TypeScript-Definitionsdateien aus EMSCRIPTEN_BINDINGS()-Blöcken. Um .d.ts-Dateien zu generieren, rufen Sie emcc mit der Option embind-emit-tsd auf:
emcc -lembind quick_example.cpp --emit-tsd interface.d.ts
Durch Ausführen dieses Befehls wird das Programm mit einer instrumentierten Version von Embind erstellt, die dann in Node ausgeführt wird, um die Definitionsdateien zu erzeugen. Derzeit werden nicht alle Funktionen von Embind unterstützt, aber viele der am häufigsten verwendeten. Beispiele für Ein- und Ausgaben finden Sie in embind_tsgen.cpp und embind_tsgen.d.ts.
val Definitionen¶emscripten::val-Typen werden standardmäßig dem any-Typ von TypeScript zugeordnet, was für APIs, die val-Typen verarbeiten oder produzieren, wenig hilfreiche Informationen liefert. Um bessere Typinformationen bereitzustellen, können benutzerdefinierte val-Typen mit EMSCRIPTEN_DECLARE_VAL_TYPE() in Kombination mit emscripten::register_type registriert werden. Ein Beispiel folgt:
EMSCRIPTEN_DECLARE_VAL_TYPE(CallbackType);
int function_with_callback_param(CallbackType ct) {
ct(val("hello"));
return 0;
}
EMSCRIPTEN_BINDINGS(custom_val) {
function("function_with_callback_param", &function_with_callback_param);
register_type<CallbackType>("(message: string) => void");
}
nonnull Pointer¶C++-Funktionen, die Pointer zurückgeben, generieren standardmäßig TS-Definitionen mit <SomeClass> | null, um nullptr zuzulassen. Wenn garantiert ist, dass die C++-Funktion ein gültiges Objekt zurückgibt, kann der Funktionsbindung ein Policy-Parameter nonnull<ret_val>() hinzugefügt werden, um das | null in TS wegzulassen. Dies erspart die Behandlung des null-Falls in TS.
Zum Zeitpunkt der Erstellung dieses Dokuments gab es keine umfassenden Performancetests für embind, weder gegen Standard-Benchmarks noch im Vergleich zum WebIDL Binder.
Der Overhead für einfache Funktionsaufrufe wurde mit etwa 200 ns gemessen. Während noch Raum für weitere Optimierungen besteht, hat sich die Performance in realen Anwendungen bisher als mehr als akzeptabel erwiesen.