count: false class: middle # typescripten ## Generierung typensicherer JavaScript-Bindungen für emscripten, Sebastian Theophil, think-cell Software, Berlin [stheophil@think-cell.com](stheophil@think-cell.com) --- # JavaScript für Einsteiger
**Ein simples Problem:** Daten sollen in ein Tabellenformat umgewandelt werden - mögliche Daten: Zahl|Zeichenfolge|Datum - sortieren, Eindeutigkeit herstellen, binäre Suche durchführen --- # JavaScript für Einsteiger
**Ein simples Problem:** Daten sollen in ein Tabellenformat umgewandelt werden - mögliche Daten: Zahl|Zeichenfolge|Datum - sortieren, Eindeutigkeit herstellen, binäre Suche durchführen --- # JavaScript für Einsteiger [MDN Web Docs: Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) _„Die Standardsortierung ist aufsteigend und basiert auf der Konvertierung der Elemente in Zeichenketten und dem anschließenden Vergleich ihrer Sequenzen von UTF-16-Codeeinheiten._ _
Die Zeit- und Raumkomplexität der Sortierung kann nicht garantiert werden,
da sie von der Implementierung abhängt.“_ 🚫 Keine Eindeutigkeit 🚫 Keine binäre Suche ??? Nein danke. Array.sort ist vorhanden, aber ich muss den Komparator selbst schreiben. Dazu gehört auch die Bestimmung des Union-Wertes und die Umwandlung des Datumswertes in einen vergleichbaren Wert. Er gibt keine Leistungsgarantien. Keine binäre Suche! Von npm beziehen! Unbedingt überprüfen, ob die binäre Suche tatsächlich implementiert ist! --- # JavaScript für Einsteiger
--- # Dabei könnte im C++-Land alles so einfach sein: ```C++ using datavalue = std::variant
std::vector
vecdata; // Fill vecdata auto const rng = std::ranges::unique(std::ranges::sort(vecdata)); std::ranges::binary_search(rng, x) ``` ??? -> gleichzeitig muss ich mit einer JavaScript-Bibliothek arbeiten, die von einem Webdienst bereitgestellt wird (große TODO-Liste als Struktur verwenden --- Klasse: largetext # Was brauche ich? Kompilieren von C++ für das Web Aufrufen von JavaScript aus C++ Typensichere Aufrufe an JS --- # Einführung in WebAssembly **CppCon 2014:** Alon Zakai „Emscripten and asm.js: C++'s role in the modern web“ **CppCon 2014:** Chad Austin „Embind and Emscripten: Blending C++11, JavaScript, and the Web Browser“ **CppCon 2016:** Dan Gohman „C++ on the Web: Let's have some serious fun.“ **CppCon 2017:** Lukas Bergdoll „Web | C++“ **CppCon 2018:** Damien Buhl „C++ Everywhere with WebAssembly“ **CppCon 2019:** Ben Smith „Applied WebAssembly: Compiling and Running C++ in Your Web Browser“ **CppCon 2019:** Borislav Stanimirov „Embrace Modern Technology: Using HTML 5 for GUI in C++“ ??? Siehe Einführungsvorträge auf der CppCon, um nicht alles wiederholen zu müssen --- # Einführung in WebAssembly Die kürzeste Einführung in WebAssembly - kompiliertes, binäres Format, standardisiert und von allen großen Browser-Anbietern unterstützt - schnell und kompakt - Datentypen auf niedriger Ebene: Ganzzahl- und Gleitkommazahlen - sichere Sandbox für jede Anwendung, läuft in der Browser-VM
??? kein Zugang zur Umgebung Trennung von Code und Daten (Harvard-Architektur), d. h., der Code ist unveränderlich --- # Einführung in WebAssembly WebAssembly wird durch JavaScript instanziiert und interagiert damit
--- # Einführung in WebAssembly WebAssembly wird durch JavaScript instanziiert und interagiert damit ```javascript function _abort() { abort(); } function _handle_stack_overflow() { abort("stack overflow"); } var imports = { "_handle_stack_overflow": _handle_stack_overflow, "abort": _abort } var instance = WebAssembly.instantiate( binary, {"env": imports} ).instance; instance.exports["exported_func"](); ``` https://hacks.mozilla.org/2018/10/calls-between-javascript-and-webassembly-are-finally-fast-🎉/ ??? WebAssembly unterstützt jedoch nur Ganzzahl- und Fließkommazahlen --- # Einführung in WebAssembly ```wasm (func $strcmp (param $0 i32) (param $1 i32) (result i32) (local $2 i32) (local $3 i32) (local.set $2 (call $SAFE_HEAP_LOAD_i32_1_U_1 (local.get $1) (i32.const 0) ) ) (block $label$1 (br_if $label$1 (i32.eqz (local.tee $3 (call $SAFE_HEAP_LOAD_i32_1_U_1 (local.get $0) (i32.const 0) ) ) ) ) ... ``` --- # C++ für das Web kompilieren - Toolchain basierend auf clang/llvm mit WebAssembly-Backend - Einfache DirectMedia Layer API (SDL) für den Zugriff auf Eingabegeräte und die Grafikausgabe - Zugriff auf OpenGL API und HTML5-Eingabeereignisse - Virtualisiertes Dateisystem
--- # emscripten **Einfache Kompilierung von portablem C oder C++ zu WebAssembly und Ausführung im Browser**
--- class: largetext # Was brauche ich? ✅ Kompilieren von C++ für das Web — **WebAssembly & emscripten** Aufrufen von JavaScript aus C++ Typensichere Aufrufe an JS --- # emscripten **Wie wird JavaScript aufgerufen?** 1. Implementierung von C-Funktionen in JS - Importe oder Exporte in WebAssembly - begrenzt auf von WebAssembly unterstützte Typen, Ganzzahlen oder Gleitkomma 2. Direktes Einbetten ```C++ int x = EM_ASM_INT({ console.log('I received: ' + $0); return $0 + 1; }, 100); printf("%d\n", x); ``` - Rückgabewerte `int` oder `double` --- # emscripten **`emscripten::val` „transkribiert JavaScript“ in C++** ```C++ using namespace emscripten; int main() { val AudioContext = val::global("AudioContext"); val context = AudioContext.new_(); val oscillator = context.call
("createOscillator"); oscillator.set("type", val("triangle")); oscillator["frequency"].set("value", val(261.63)); // Middle C } ``` --- # emscripten **`emscripten::val` „transkribiert JavaScript“ in C++** ```C++ using namespace emscripten; int main() { val AudioContext = val::global("AudioContext"); val context = AudioContext.new_(); * val oscillator = context.call
("createOscillator"); oscillator.set("type", val("triangle")); oscillator["frequency"].set("value", val(261.63)); // Middle C } ``` -- **Pro:** Praktische Interaktion mit JS-Objekten **Con:** Vereint die Nachteile der beiden Sprachen: 1. Compiled 2. Nicht typensicher --- # emscripten **`emscripten::val` basierend auf in JS implementierten WebAssembly-Importen** ```C++ EM_VAL _emval_new_object(); EM_VAL _emval_new_cstring(const char*); void _emval_incref(EM_VAL value); void _emval_decref(EM_VAL value); void _emval_call_void_method( EM_METHOD_CALLER caller, EM_VAL handle, const char* methodName, EM_VAR_ARGS argv); ``` --- # emscripten
`EM_VAL` = Referenz auf ein in einer Tabelle gespeichertes JavaScript-Objekt, eventuell mit Referenzzählung --- Klasse: largetext # Was brauche ich? ✅ Kompilieren von C++ für das Web — **WebAssembly & emscripten** ✅ Aufrufen von JavaScript aus C++ — **emscripten** Typensichere Aufrufe an JS ??? Was bleibt noch? Generierung besserer (idiomatischer) Schnittstellen für APIs, möglicherweise automatisch Generierung dieser für alles, damit wir C++ zum Schreiben von Webanwendungen verwenden können Generierung für JS-Bibliotheken von Drittanbietern --- class: middle # Live Coding ??? Code - Erstes Codebeispiel, JavaScript zu C++ - C++-Schnittstelle identisch mit JavaScript-Schnittstellen, aber zur Kompilierzeit geprüft - In diesem Beispiel wird die gesamte DOM-API offengelegt - Kompilieren -> Compiler-Fehler -> aha, die Schriftgröße muss tatsächlich eine Zeichenfolge sein! - Show html, show lib.dom.d.h, _impl_js_jHTMLElement blendet nicht typensichere emscripten API für uns aus Wie erzeugen wir diese Schnittstellen? --- # Live Coding
--- # TypeScript Typdefinitionsbibliotheken: ```typescript interface Document extends Node, NonElementParentNode, DocumentOrShadowRoot { readonly URL: string; readonly activeElement: Element | null; readonly anchors: HTMLCollectionOf
; title: string; createElement
( tagName: K, options?: ElementCreationOptions ): HTMLElementTagNameMap[K]; } ``` -- [https://github.com/DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) Repository für über 7000 JavaScript-Bibliotheken, z. B. AngularJS, bootstrap, tableau.com --- # TypeScript TypeScript verfügt über eine **sehr praktische** Parser und Resolver API: ```typescript function transform(file: string) : void { let program = ts.createProgram([file]); const sourceFile = program.getSourceFile(file); ts.forEachChild(sourceFile, node => { if (ts.isFunctionDeclaration(node)) { // do something } else if (ts.isVariableStatement(node)) { // do something else } }); } ``` --- # typescripten ## typescripten — [https://github.com/think-cell/typescripten](https://github.com/think-cell/typescripten) - Kompiliert TypeScript-Schnittstellendeklarationen zu C++-Schnittstellen - **d. h. typensichere, idiomatische Aufrufe von JavaScript-Bibliotheken über emscripten** **JavaScript:** ```javascript document.title = "Hello World from C++"; ``` **C++:** ```C++ using namespace tc; js::document()->title(js::string("Hello World from C++!")); ``` --- # typescripten ```C++ namespace tc::js { struct object_base { * emscripten::val m_emval; }; struct Document : virtual Node, ... { auto URL() noexcept; auto activeElement() noexcept; auto title() noexcept; void title(string v) noexcept; // ... }; inline auto Document::title() noexcept { return m_emval["title"].template as
(); } inline void Document::title(string v) noexcept { m_emval.set("title", v); } inline auto document() noexcept { return emscripten::val::global("document").template as
(); } } ``` --- # typescripten ```C++ namespace tc::js { struct object_base { emscripten::val m_emval; }; struct Document : virtual Node, ... { auto URL() noexcept; auto activeElement() noexcept; auto title() noexcept; void title(string v) noexcept; // ... }; * inline auto Document::title() noexcept { return m_emval["title"].template as
(); } * inline void Document::title(string v) noexcept { m_emval.set("title", v); } inline auto document() noexcept { return emscripten::val::global("document").template as
(); } } ``` --- # typescripten **Unterstützen wir alle TypeScript Konstrukte?** -- ```typescript interface A { func(a: { length: number }) : void; } ``` -- **Nein.** Es müssen gemeinsame Konstrukte in Schnittstellendefinitionsdateien unterstützt werden. -- ```typescript interface A { func(a: TypeWithLengthProperty) : void; } ``` --- # typescripten **Unterstützte TypeScript Konstrukte** - Implementierung von integrierten Typen `tc::js::any`, `tc::js::undefined`, `tc::js::null`, `tc::js::string` - Optionale Mitglieder, Typenüberwachung - Unterstüzung für Union-Typen `A|B|C` wie `tc::js::union_t
` - Gemischte Aufzählungen wie ```typescript enum E { a, b = "that's a string", c = 1.0 } ``` - Weitergabe von Funktionsrückrufen und Lambdas an JavaScript wie `tc::js::function
` - Allgemeine Typen, z. B. `tc::js::Array
` oder `tc::js::Record
` -- **Self-Hosting, d. h. Kompilieren der Schnittstellendefinition für TypeScript API, von der es verwendet wird** --- # typescripten typescripten selbst verwendet generierte Schnittstellen zur TypeScript API ```typescript function transform(file: string) : void { let program = ts.createProgram([file]); const sourceFile = program.getSourceFile(file); ts.forEachChild(sourceFile, node => { if (ts.isFunctionDeclaration(node)) { // do something } else if (ts.isVariableStatement(node)) { // do something else } }); } ``` --- # typescripten typescripten selbst verwendet generierte Schnittstellen zur TypeScript API ```C++ void transform(js::string const& file) noexcept { js::Array
files(jst::create_js_object, tc::single(file)); auto const program = js::ts::createProgram(files, ...); auto const sourceFile = program->getSourceFile(file); js::ts::forEachChild(sourceFile, js::lambda( * [](js::ts::Node node) noexcept { if (js::ts::isFunctionDeclaration(node)) { // do something } else if (js::ts::isVariableStatement(node)) { // do something else } } ) ); } ``` --- # typescripten **Die Reihenfolge der Deklaration spielt in TypeScript keine Rolle** ```typescript type FooBar = test.Foo | test.Bar; declare namespace test { export interface Foo { a: string; } export interface Bar { b: number; } } ``` --- # typescripten
**Union-Typen sind nicht wie C++-Unions** - haben keinen diskriminierenden Aufzählungswert - stattdessen hat die Schnittmenge der Eigenschaften `A|B|C` Mitglieder, die _in der Schnittmenge_ der Mitglieder von A, B **und** C -- `A|B|C` ist aus jedem Wert konstruierbar, der alle Mitglieder von A, B **und** C gemeinsam hat ```typescript class D { common: number = 0.0; } let u : A|B|C = new D(); ``` -- **Ein Typ ist nur eine Menge von Eigenschaften = strukturelle Typisierung** --- # typescripten **C++ unterstützt keine strukturelle Typisierung**
`union_t
` konvertiert in _gemeinsame Basisklassen_ von A, B **nd** C -- `union_t
` konvertiert zu einer größeren Union `union_t
` -- `union_t
` aus allem konstruierbar, was sich in A, B oder C umwandeln lässt -- **Nicht so restriktiv wie es scheint** -- ```typescript interface HasCommonProp { common: number; }; interface A extends HasCommonProp {} interface B extends HasCommonProp {} interface C extends HasCommonProp {} ``` --- # typescripten **Gemischte Aufzählungen mit benutzerdefiniertem Marshaling** ```typescript export enum FunnyEnum { foo = "foo", bar = 1.5 } ``` -- ```C++ enum class FunnyEnum { foo, bar }; template<> struct MarshalEnum
{ static inline auto const& Values() { static tc::dense_map
vals{ {FunnyEnum::foo, js::string("foo")}, {FunnyEnum::bar, js::any(1.5)} }; return vals; } }; ``` ??? Verwendung von Konstantennamen für Aufzählungen, benutzerdefiniertes Marschaling --- # typescripten **Gemischte Aufzählungen mit benutzerdefiniertem Marshaling** ```typescript export enum FunnyEnum { foo = "foo", bar = 1.5 } ``` ```C++ enum class FunnyEnum { foo, bar }; template<> struct MarshalEnum
{ static inline auto const& Values() { static tc::dense_map
vals{ * {FunnyEnum::foo, js::string("foo")}, * {FunnyEnum::bar, js::any(1.5)} }; return vals; } }; ``` ??? Verwendung von Konstantennamen für Aufzählungen, benutzerdefiniertes Marshaling --- class: middle # Code-Beispiel #2 ??? --- # typescripten **Funktionsobjekte mit Referenzzählung sind kompliziert** ```typescript class SomeButton { constructor() { const button = document.createElement(...); button.addEventListener("click", () => this.OnClick()); } function OnClick(ev: MouseEvent) : void { /* do something */ /* but in which states will this be called? */ } } ``` - Keine deterministische Destruktion - Eigentum an referenzgezählten Objekten - verkompliziert das Verständnis von Zuständen --- # typescripten **Unschöne Syntax, aber einfacher Zustandsrechner** ```cpp struct SomeButton { SomeButton() { const button = js::document()->createElement(...); button->addEventListener("click", OnClick); } ~SomeButton() { button->remove(); // Our callback will also be destroyed! 🎉 } TC_JS_MEMBER_FUNCTION(S, OnClick, void, (js::MouseEvent ev)) { // do something } }; ``` --- # typescripten ```C++ // 1. Create RAII wrapper OnClick static emscripten::val OnClickWrapper(void* pvThis, emscripten::val const& emvalThis, emscripten::val const& emvalArgs) noexcept; jst::function
OnClick{&OnClickWrapper, this}; ``` -- ```javascript // 2. jst::function ctor calls to JS and creates JS function object *Module.CreateJsFunction = function(iFuncPtr, iThisPtr) { const fnWrapper = function() { if(iFuncPtr !== null) { return Module.tc_js_CallCpp(iFuncPtr, iThisPtr, this, arguments); } }; fnWrapper.detach = function() { iFuncPtr = null; } return fnWrapper; } // 3. JS function object held as emscripten::val ``` --- # typescripten ```C++ // 1. Create RAII wrapper OnClick static emscripten::val OnClickWrapper(void* pvThis, emscripten::val const& emvalThis, emscripten::val const& emvalArgs) noexcept; jst::function
OnClick{&OnClickWrapper, this}; ``` ```javascript // 2. jst::function ctor calls to JS and creates JS function object Module.CreateJsFunction = function(iFuncPtr, iThisPtr) { * const fnWrapper = function() { if(iFuncPtr !== null) { return Module.tc_js_CallCpp(iFuncPtr, iThisPtr, this, arguments); } }; fnWrapper.detach = function() { iFuncPtr = null; } * return fnWrapper; } // 3. JS function object held as emscripten::val ``` --- # typescripten ```C++ // 1. Create RAII wrapper OnClick static emscripten::val OnClickWrapper(void* pvThis, emscripten::val const& emvalThis, emscripten::val const& emvalArgs) noexcept; jst::function
OnClick{&OnClickWrapper, this}; ``` ```javascript // 2. jst::function ctor calls to JS and creates JS function object Module.CreateJsFunction = function(iFuncPtr, iThisPtr) { const fnWrapper = function() { if(iFuncPtr !== null) { * return Module.tc_js_CallCpp(iFuncPtr, iThisPtr, this, arguments); } }; fnWrapper.detach = function() { iFuncPtr = null; } return fnWrapper; } // 3. JS function object held as emscripten::val ``` --- # typescripten ```C++ // 1. Create RAII wrapper OnClick static emscripten::val OnClickWrapper(void* pvThis, emscripten::val const& emvalThis, emscripten::val const& emvalArgs) noexcept; jst::function
OnClick{&OnClickWrapper, this}; ``` ```javascript // 2. jst::function ctor calls to JS and creates JS function object Module.CreateJsFunction = function(iFuncPtr, iThisPtr) { const fnWrapper = function() { if(iFuncPtr !== null) { return Module.tc_js_CallCpp(iFuncPtr, iThisPtr, this, arguments); } }; fnWrapper.detach = function() { * iFuncPtr = null; } return fnWrapper; } // 3. JS function object held as emscripten::val ``` --- # typescripten ```C++ // 4. When called, JS function object passes function pointer back to generic C++ function emscripten::val Call(PointerNumber iFuncPtr, PointerNumber iArgPtr, emscripten::val emvalThis, emscripten::val emvalArgs) noexcept { // 5. Casts function pointer to correct signature and calls it } ``` -- ```C++ static emscripten::val OnClickWrapper(void* pvThis, emscripten::val const& emvalThis, emscripten::val const& emvalArgs) noexcept { // 6. Cast this pointer, unpack arguments from emvalArgs and call OnClickImpl } ``` -- ```C++ void OnClickImpl(js::MouseEvent ev) noexcept { /* ... user code */ } ``` --- # typescripten TypeScript unterstützt generische Klassen ``` js::HTMLCollectionOf
htmlcollection = js::document()->body()->getElementsByTagName(js::string("div")); ``` -- Generische Klassen werden in C++-Vorlagen übersetzt `interface Array
{}` wird übersetzt in `template
struct Array {}` -- Generische Klassen können Beschränkungen haben ```typescript enum Enum {} interface A
{} ``` -- Als Nicht-Typ-Vorlagenparameter ausdrückbar ```C++ template
struct A {}; ``` --- # typescripten Generische Klassen unterstützen viele Arten von Beschränkungen ```typescript class Node {} interface A
{} ``` -- kann ausgedrückt werden als ```C++
::value>* = nullptr> struct A {}; ``` Auch hier ist die Semantik nicht identisch. --- class: middle # Live Coding #3 ??? Erläuterung, wie man die Tableau-Bibliothek hinzufügt Hinzufügen zu main.emscripten Erläuterung des Build-Tools Erläuterung von Debugging, Source-Maps --- Klasse: largetext # ✅ Kompilieren von C++ für das Web — **WebAssembly & emscripten** ✅Aufrufen von JavaScript aus C++ — **emscripten** ✅ Typensichere Aufrufe an JS — **typescripten** --- # typescripten typescripten wird durch _WebAssembly Schnittstellentypen abgelöst_ Noch in der Vorschlagsphase https://github.com/WebAssembly/interface-types Ausführlichere Einführung: https://hacks.mozilla.org/2019/08/webassembly-interface-types/ Wie in ISO C++, eventuell gute Idee, mit der Implementierung zu experimentieren ??? Der frühe Prototyp war in Rust implementiert, die Implementierung wurde allerdings wieder entfernt --- # typescripten **Leistungstest** 1.000.000 Funktionsaufrufe WebAssembly zu JavaScript JS-Funktion steigert eine Zahl - `extern "C" function` von WebAssembly zu JavaScript - `EM_ASM_DOUBLE` eingebetteter JS-Code - _typescripten_ Aufruf über `emscripten::val` ```C++ inline auto _impl_js_j_qMyLib_q::_tcjs_definitions::next() noexcept { return emscripten::val::global("MyLib")["next"]().template as
(); } ``` --- # typescripten **Leistungstest** 1.000.000 Funktionsaufrufe WebAssembly zu JavaScript JS-Funktion steigert eine Zahl
-- **Kosten für die Konvertierung von C-Zeichenfolgen in JavaScript-Zeichenfolgen** --- # typescripten **Weitere Herausforderungen:** - Allgemeine Beschränkungen ```typescript interface HTMLCollectionOf
extends HTMLCollectionBase { item(index: number): T | null; } ``` -- - Indizierte Zugriffsarten ```typescript interface DocumentEventMap { "click": MouseEvent; "keydown": KeyboardEvent; } addEventListener
( type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, ... ): void; ``` --- # typescripten ## Mehr dazu unter [https://github.com/think-cell/typescripten](https://github.com/think-cell/typescripten) ## Mitwirkende sind herzlich willkommen --- class: middle # Vielen Dank! ## Nun zu Euren Fragen! Sebastian Theophil, think-cell Software, Berlin [stheophil@think-cell.com](stheophil@think-cell.com) --- # typescripten Operator `keyof` gibt die Namen der Klasseneigenschaften zurück ```typescript interface K { foo: string: bar: string; } interface A
{} // T can be "foo" or "bar" ``` möglicherweise am besten ausgedrückt mit ```C++ enum class KeyOfK { foo, bar }; template
struct A {} ``` ---
×