count: false class: middle # typescripten ## Generating Type-Safe JavaScript bindings for emscripten Sebastian Theophil, think-cell Software, Berlin [stheophil@think-cell.com](stheophil@think-cell.com) --- # JavaScript for Beginners
**One simple problem:** Transform data into tabular format - data could be `number|string|date` - sort, make unique, do binary search --- # JavaScript for Beginners
**One simple problem:** Transform data into tabular format - data could be `number|string|date` - sort, make unique, do binary search --- # JavaScript for Beginners [MDN Web Docs: Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) _"The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values._ _
The time and space complexity of the sort cannot be guaranteed
as it depends on the implementation."_ 🚫 No unique 🚫 No binary search ??? Erm, no thanks. Array.sort exists but I need to write the comparator myself. Including checking which type the union value has and transforming the date to something comparable. Does not give perf guarantees. No binary search! Pull it from npm! Don't forget to check if it actually implements binary search! --- # JavaScript for Beginners
--- # Meanwhile, in C++ land Everything could be so easy: ```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) ``` ??? -> at the same time, I have to interact with a JavaScript library provided by some web service (Use big TODO list as structure --- class: largetext # What Do I Need? Compile C++ for the Web Call JavaScript from C++ Type-safe calls to JS --- # Enter 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++" ??? Refer to intro talks at CppCon, no need to repeat everything --- # Enter WebAssembly The shortest intro to WebAssembly - compiled, binary format, standardised and supported by all major browser vendors - fast and compact - low level data types: integer and floating point numbers - secure per-application sandbox, runs in browser VM
??? no access to environment separate code and data (Harvard architecture), i.e., code is immutable --- # Enter WebAssembly WebAssembly is instantiated from and interacts with JavaScript
--- # Enter WebAssembly WebAssembly is instantiated from and interacts with JavaScript ```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-🎉/ ??? But WebAssembly only supports integer and floating point values --- # Enter 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) ) ) ) ) ... ``` --- # Compiling C++ for the Web - Toolchain based on clang/llvm with WebAssembly backend - Simple DirectMedia Layer API (SDL) for input device access and graphics output - Access to OpenGL API and HTML5 input events - Virtualized file system
--- # emscripten **Easy to compile portable C or C++ to WebAssembly and run it in browser**
--- class: largetext # What Do I Need? ✅ Compile C++ for the Web — **WebAssembly & emscripten** Call JavaScript from C++ Type-safe calls to JS --- # emscripten **How to call JavaScript?** 1. Implement C functions in JS - WebAssembly imports or exports - limited to WebAssembly supported types, integers or floating point 2. Direct Embedding ```C++ int x = EM_ASM_INT({ console.log('I received: ' + $0); return $0 + 1; }, 100); printf("%d\n", x); ``` - `int` or `double` return values --- # emscripten **`emscripten::val` "transliterates JavaScript" to 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` "transliterates JavaScript" to 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:** Convenient interaction with JS objects **Con:** Combines disadvantages of both languages: 1. Compiled 2. Not type-safe --- # emscripten **`emscripten::val` based on WebAssembly imports implemented in JS** ```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` = reference to JavaScript object stored in a table, possibly with reference count --- class: largetext # What Do I Need? ✅ Compile C++ for the Web — **WebAssembly & emscripten** ✅ Call JavaScript from C++ — **emscripten** Type-safe calls to JS ??? What is left? Generating better (idiomatic) interfaces for APIs, possibly automatically Generate them for everything so we can use C++ to write web apps Generate them for 3rd party JS libraries --- class: middle # Live Coding ??? Code - First code sample, JavaScript to C++ - C++ interface identical to JavaScript interfaces, but checked at compile time - In this example, entire DOM API exposed - Compile -> compiler error -> ah, the font size actually has to be a string! - Show html, show lib.dom.d.h, _impl_js_jHTMLElement hides not type-safe emscripten API for us How do we generate these interfaces? --- # Live Coding
--- # TypeScript Type definition libraries: ```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 for over 7000 JavaScript libraries, e.g, AngularJS, bootstrap, tableau.com --- # TypeScript TypeScript ships with **super convenient** parser and 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) - Compiles TypeScript interface declarations to C++ interfaces - **i.e. type-safe, idiomatic calls to JavaScript libraries via 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 **Do we support all TypeScript constructs?** -- ```typescript interface A { func(a: { length: number }) : void; } ``` -- **No.** Need to support common constructs in interface definition files. -- ```typescript interface A { func(a: TypeWithLengthProperty) : void; } ``` --- # typescripten **Supported TypeScript constructs** - Implementation of built-in types `tc::js::any`, `tc::js::undefined`, `tc::js::null`, `tc::js::string` - Optional members, type guards - Support for union types `A|B|C` as `tc::js::union_t
` - Mixed enums like ```typescript enum E { a, b = "that's a string", c = 1.0 } ``` - Passing function callbacks and lambdas to JavaScript as `tc::js::function
` - Generic types, e.g., `tc::js::Array
` or `tc::js::Record
` -- **Self-hosting, i.e., compiles interface definition for TypeScript API that it uses itself** --- # typescripten typescripten itself uses generated interfaces to 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 itself uses generated interfaces to 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 **Declaration order does not matter in TypeScript** ```typescript type FooBar = test.Foo | test.Bar; declare namespace test { export interface Foo { a: string; } export interface Bar { b: number; } } ``` --- # typescripten
**Union types are not like C++ unions** - don't have a discriminating enumeration value - instead, intersection of properties `A|B|C` has members that are _in the intersection_ of members of A, B **and** C -- `A|B|C` constructible from any value that has all members shared by A, B ** and** C ```typescript class D { common: number = 0.0; } let u : A|B|C = new D(); ``` -- **A type is just a set of properties = structural typing** --- # typescripten **C++ does not support structural typing**
`union_t
` converts to _common base classes_ of A, B **and** C -- `union_t
` converts to wider union `union_t
` -- `union_t
` constructible from anything that converts to A, B or C -- **Not as limiting as it sounds** -- ```typescript interface HasCommonProp { common: number; }; interface A extends HasCommonProp {} interface B extends HasCommonProp {} interface C extends HasCommonProp {} ``` --- # typescripten **Mixed enumerations with custom 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; } }; ``` ??? use enum constant names, custom marshaling --- # typescripten **Mixed enumerations with custom 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; } }; ``` ??? use enum constant names, custom marshaling --- class: middle # Code Example #2 ??? --- # typescripten **Reference-counted function objects are complicated** ```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? */ } } ``` - No deterministic destruction - On ownership of reference-counted objects - makes thinking about states complicated --- # typescripten **Ugly syntax but simple state machine** ```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 supports generic classes ``` js::HTMLCollectionOf
htmlcollection = js::document()->body()->getElementsByTagName(js::string("div")); ``` -- Generic classes are translated to C++ templates `interface Array
{}` translates to `template
struct Array {}` -- Generic classes can have constraints ```typescript enum Enum {} interface A
{} ``` -- Expressible as non-type template parameter ```C++ template
struct A {}; ``` --- # typescripten Generic classes support many kinds of constraints ```typescript class Node {} interface A
{} ``` -- might be expressed as ```C++
::value>* = nullptr> struct A {}; ``` Again, the semantics are not identical. --- class: middle # Live Coding #3 ??? Show how to add tableau library Add it to main.emscripten Explain build tool Show debugging, source maps --- class: largetext # ✅ Compile C++ for the Web — **WebAssembly & emscripten** ✅ Call JavaScript from C++ — **emscripten** ✅ Type-safe calls to JS — **typescripten** --- # typescripten typescripten will be superseded by _WebAssembly Interface types_ Still in proposal phase https://github.com/WebAssembly/interface-types Longer Introduction: https://hacks.mozilla.org/2019/08/webassembly-interface-types/ As in ISO C++, maybe good idea to experiment with implementation ??? Early prototype used to be implemented in Rust, but implementation was removed again --- # typescripten **Performance Test** 1.000.000 function calls WebAssembly to JavaScript JS function increments a number - `extern "C" function` from WebAssembly to JavaScript - `EM_ASM_DOUBLE` embedded JS code - _typescripten_ call via `emscripten::val` ```C++ inline auto _impl_js_j_qMyLib_q::_tcjs_definitions::next() noexcept { return emscripten::val::global("MyLib")["next"]().template as
(); } ``` --- # typescripten **Performance Test** 1.000.000 function calls WebAssembly to JavaScript JS function increments a number
-- **Cost of converting C-strings to JavaScript strings** --- # typescripten **Next Challenges:** - Generic constraints ```typescript interface HTMLCollectionOf
extends HTMLCollectionBase { item(index: number): T | null; } ``` -- - Indexed access types ```typescript interface DocumentEventMap { "click": MouseEvent; "keydown": KeyboardEvent; } addEventListener
( type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, ... ): void; ``` --- # typescripten ## Check it out at [https://github.com/think-cell/typescripten](https://github.com/think-cell/typescripten) ## Contributors are very welcome --- class: middle # Thank you! ## Now to your questions! Sebastian Theophil, think-cell Software, Berlin [stheophil@think-cell.com](stheophil@think-cell.com) --- # typescripten `keyof` operator returns the names of class properties ```typescript interface K { foo: string: bar: string; } interface A
{} // T can be "foo" or "bar" ``` maybe best expressed as ```C++ enum class KeyOfK { foo, bar }; template
struct A {} ``` ---
Поделиться
Адрес электронной почты
Facebook
LinkedIn
Twitter
XING
×