count: false class: middle # typescripten ## Generating Type-Safe JavaScript bindings for emscripten Sebastian Theophil, software think-cell, Berlín [stheophil@think-cell.com](stheophil@think-cell.com) --- # JavaScript para principiantes
**Un problema sencillo:** Transformar datos en formato de tabla. - Los datos podrían tener el formato `number|string|date`. - Clasificar, convetir en único, realizar búsquedas binarias. --- # JavaScript para principiantes
**Un problema sencillo:** Transformar datos en formato de tabla. - Los datos podrían tener el formato `number|string|date`. - Clasificar, convetir en único, realizar búsquedas binarias. --- # JavaScript para principiantes [MDN Web Docs: Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) _"El orden de clasificación predeterminado es ascendente. Se basa en convertir los elementos en cadenas y, a continuación, comparar sus secuencias de valores de unidades de código UTF-16._ _
La complejidad de tiempo y espacio de la clasificación no se puede garantizar
ya que depende de la implementación."_ 🚫 Imposibilidad de convertir en único 🚫 Imposibilidad de realizar búsquedas binarias ??? Gracias, pero no. Array.sort existe, pero tengo que encargarme yo mismo de escribir el código del comparador. Incluye la verificación de qué tipo de valor de unión tiene y transformar la fecha en algo comparable. No ofrece garantías de rendimiento. ¡No se pueden realizar búsquedas binarias! ¡Las obtendrás en npm! ¡No te olvides de comprobar si realmente implementa búsquedas binarias! --- # JavaScript para principiantes
--- # Entretanto, en C++ Todo podría ser tan fácil: ```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) ``` ??? -> En paralelo, tengo que interactuar con una biblioteca JavaScript proporcionada por cualquier servicio web. (Utilizar una lista de tareas pendienes como estructura.) --- class: largetext # ¿Qué se necesita? Compilación de C++ para la Web Realización de llamadas a JavaScript (JS) desde C++ Realización de llamadas con seguridad de tipos a JS --- # Introducción a 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++” ??? Hacer referencia a las ponencias de presentación de CppCon; no es necesario repetirlo todo. --- # Introducción a WebAssembly Introducción rápida a WebAssembly: - Herramienta compilada, con formato binario, normalizada y admitida por todos los principales proveedores de navegadores - Rápida y compacta - Tipos de datos de bajo nivel: números enteros y de punto flotante - Espacio aislado con seguridad en función de la aplicación, se ejecuta en máquinas virtuales para navegadores
??? sin acceso a código y datos de entornos distintos (arquitectura Harvard), es decir, el código es inmutable --- # Introducción a WebAssembly Las instancias de WebAssembly se crean a partir de JavaScript, lenguaje con el que esta herramienta interactúa.
--- # Introducción a WebAssembly Las instancias de WebAssembly se crean a partir de JavaScript, lenguaje con el que esta herramienta interactúa. ```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-🎉/ ??? Sin embargo, WebAssembly solo admite valores enteros y de punto flotante --- # Introducción a 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) ) ) ) ) ... ``` --- # Compilación de C++ para la Web - Cadena de herramientas basada en clang/llvm con backend de WebAssembly - Sencilla API de Simple DirectMedia Layer (SDL) para tener acceso dispositivos de entrada y para la generación de gráficos - Acceso a eventos de entrada de la API OpenGL y de HTML5 - Sistema de archivos virtualizado
--- # emscripten **Permite compilar C portable o C++ a WebAssembly y ejecutarlo en el navegador, todo ello con facilidad.**
--- class: largetext # ¿Qué se necesita? ✅ Compilación de C++ para la Web: **WebAssembly y emscripten** Realización de llamadas a JavaScript desde C++ Realización de llamadas con seguridad de tipos a JS --- # emscripten **¿Cómo se realizan llamadas a JavaScript?** 1. Implementando funciones de C en JS. - Importaciones o exportaciones de WebAssembly - Restringidas a tipos admitidos por WebAssembly, enteros o números de punto flotante 2. Realizando incrustaciones directas. ```C++ int x = EM_ASM_INT({ console.log('I received: ' + $0); return $0 + 1; }, 100); printf("%d\n", x); ``` - Valores devueltos `int` o `double` --- # emscripten **`emscripten::val` “transcribe JavaScript” a 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` “transcribe JavaScript” a 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 } ``` -- **Ventaja:** Interactúa con agilidad con los objetos de JS. **Inconveniente:** Reúne las desventajas de ambos lenguajes. 1. Compilado 2. No tiene seguridad para tipos --- # emscripten **`emscripten::val` se basa en importaciones de WebAssembly implementadas en 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` = referencia a objeto de JavaScript guardado en una tabla, posiblemente con recuento de referencias --- class: largetext # ¿Qué se necesita? ✅ Compilación de C++ para la Web: **WebAssembly y emscripten** ✅ Realización de llamadas a JavaScript desde C++: **emscripten** Realización de llamadas con seguridad de tipos a JS ??? ¿Qué es lo que falta? Generar interfaces mejores (y naturales) para API, quizás automáticamente Generarlas para todo de modo que podamos escribir aplicaciones web con C++ Generarlas para bibliotecas JS de terceros --- class: middle # Codificación en directo ??? Código - Primera muestra de código, JavaScript a C++ - Interfaz de C++ idéntica a las interfaces de JavaScript, pero verificada en el tiempo de compilación - En este ejemplo, se expone toda la API del dominio - Compilación -> error del compilador -> ¡Sorpresa!: el tamaño de la fuente tiene que ser una cadena. - Mostrar html, mostrar lib.dom.d.h, _impl_js_jHTMLElement nos oculta una API de emscripten que no tiene seguridad para tipos ¿Cómo generamos estas interfaces? --- # Codificación en directo
--- # TypeScript Bibliotecas de definición de tipos: ```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) Repositorio de más de 7000 bibliotecas de JavaScript (p. ej., AngularJS, bootstrap, tableau.com) --- # TypeScript TypeScript incluye una API de solucionador y analizador **extraordinariamente práctica**: ```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) - Compila declaraciones de interfaz de TypeScript a interfaces de C++ - **Es decir, llamadas naturales con seguridad para tipos destinadas a bibliotecas de JavaScript a través de 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 **¿Tenemos compatibilidad con todas las construcciones de TypeScript?** -- ```typescript interface A { func(a: { length: number }) : void; } ``` -- **No.** Tenemos que agregar compatibilidad con las construcciones habituales de los archivos de definición de interfaz. -- ```typescript interface A { func(a: TypeWithLengthProperty) : void; } ``` --- # typescripten **Construcciones de TypeScript compatibles** - Implementación de los tipos integrados `tc::js::any`, `tc::js::undefined`, `tc::js::null`, `tc::js::string` - Miembros opcionales, restricciones de tipo - Compatibilidad con tipos de unión `A|B|C` en forma de `tc::js::union_t
` - Enumeraciones mixtas como: ```typescript enum E { a, b = "that's a string", c = 1.0 } ``` - Transmisión de devoluciones de llamadas y lambdas correspondientes a funciones en forma de `tc::js::function
` - Tipos genéricos, p. ej., `tc::js::Array
` o `tc::js::Record
` -- **Realización de pruebas internas, es decir, compila la definición de la interfaz correspondiente a la API de TypeScript que utiliza el propio typescripten.** --- # typescripten El propio typescripten utiliza interfaces generadas a la API de TypeScript. ```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 El propio typescripten utiliza interfaces generadas a la API de TypeScript. ```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 **El orden de declaración no importa en TypeScript.** ```typescript type FooBar = test.Foo | test.Bar; declare namespace test { export interface Foo { a: string; } export interface Bar { b: number; } } ``` --- # typescripten
**Los tipos de unión no se parecen a las uniones de C++:** - No tienen un valor de enumeración discriminatorio. - En su lugar, cuentan con la intersección de propiedades. `A|B|C` tiene miembros que están _en la intersección_ de miembros de A, B **y** C. -- `A|B|C` es construible a partir de cualquier valor cuyos miembros en su totalidad estén compartidos por A, B ** y** C. ```typescript class D { common: number = 0.0; } let u : A|B|C = new D(); ``` -- **Un tipo es un conjunto de propiedades = tipos estructurales.** --- # typescripten **C++ no es compatible con los tipos estructurales.**
`union_t
` se convierete en _clases de base común_ de A, B **y** C. -- `union_t
` se convierte en una unión más amplia `union_t
` -- `union_t
` construible a partir de cualquier elemento que se convierte en A, B o C. -- **No es tan restrictivo como parece.** -- ```typescript interface HasCommonProp { common: number; }; interface A extends HasCommonProp {} interface B extends HasCommonProp {} interface C extends HasCommonProp {} ``` --- # typescripten **Enumeraciones mixtas con serialización personalizada** ```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; } }; ``` ??? utilizar nombres de constantes de enumeración, serialización personalizada --- # typescripten **Enumeraciones mixtas con serialización personalizada** ```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; } }; ``` ??? utilizar nombres de constantes de enumeración, serialización personalizada --- class: middle # Ejemplo de código n.º 2 ??? --- # typescripten **Los objetos de funciones con recuento de referencias son complejos.** ```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? */ } } ``` - La inexistencia de una destrucción determinista en lo relativo a la propiedad de los objetos con recuento de referencias dificulta trabajar con estados. --- # typescripten **Sintaxis poco atractiva, pero máquina de estados sencilla. ** ```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 admite clases genéricas. ``` js::HTMLCollectionOf
htmlcollection = js::document()->body()->getElementsByTagName(js::string("div")); ``` -- Las clases genéricas se traducen a Las plantillas de C++ `interface Array
{}` se traducen a `template
struct Array {}` -- Las clases genéricas pueden tener limitaciones. ```typescript enum Enum {} interface A
{} ``` -- Se puede expresar en forma de parámetro de plantilla que no sea un tipo. ```C++ template
struct A {}; ``` --- # typescripten Las clases genéricas admiten multitud de tipos de limitaciones: ```typescript class Node {} interface A
{} ``` -- se podría haber expresado así: ```C++
::value>* = nullptr> struct A {}; ``` Además, los significados no son idénticos. --- class: middle # Codificación en directo n.º 3 ??? Mostrar cómo agregar una biblioteca de Tableau. Agregarla a main.emscripten. Explicar herramienta de diseño. Mostrar depuración, mapas de origen. --- class: largetext # ✅ Compilación de C++ para la Web: **WebAssembly y emscripten** ✅ Realización de llamadas a JavaScript desde C++: **emscripten** ✅ Realización de llamadas con seguridad de tipos a JS: **typescripten** --- # typescripten _Los tipos de interfaz de WebAssembly_ van a reemplezar a typescripten. Aún está en fase de propuesta: https://github.com/WebAssembly/interface-types Introducción extendida: https://hacks.mozilla.org/2019/08/webassembly-interface-types/ Al igual que ocurre con la norma ISO C++, quizás convenga hacer pruebas con la implementación. ??? Se ha utilizado un prototipo rudimentario para implementarse en Rust, pero se ha vuelto a retirar la implementación. --- # typescripten **Prueba de rendimiento** Ejecución de 1 000 000 de llamadas de función con WebAssembly dirigidas a JavaScript. La función de JS se incrementa en un número. - `extern ’C" function` de WebAssembly a JavaScript - Código de JS incrustado por `EM_ASM_DOUBLE`. - Se realiza una llamada de _typescripten_ a través de `emscripten::val`. ```C++ inline auto _impl_js_j_qMyLib_q::_tcjs_definitions::next() noexcept { return emscripten::val::global("MyLib")["next"]().template as
(); } ``` --- # typescripten **Prueba de rendimiento** Ejecución de 1 000 000 de llamadas de función con WebAssembly dirigidas a JavaScript. La función de JS se incrementa en un número.
-- **Coste temporal de convertir cadenas de C a cadenas de JavaScript** --- # typescripten **Nuevos desafíos:** - Restricciones genéricas: ```typescript interface HTMLCollectionOf
extends HTMLCollectionBase { item(index: number): T | null; } ``` -- - Tipos de acceso indizado: ```typescript interface DocumentEventMap { "click": MouseEvent; "keydown": KeyboardEvent; } addEventListener
( type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, ... ): void; ``` --- # typescripten ## Echad un vistazo en [https://github.com/think-cell/typescripten](https://github.com/think-cell/typescripten) ## Estamos abiertos a concertar colaboraciones. --- class: middle # ¡Gracias por vuestra atención! ## ¿Alguna pregunta? Sebastian Theophil, software think-cell, Berlín [stheophil@think-cell.com](stheophil@think-cell.com) --- # typescripten El operador `keyof` devuelve los nombres de las propiedades de clase: ```typescript interface K { foo: string: bar: string; } interface A
{} // T can be "foo" or "bar" ``` Quizás quede mejor expresado de esta manera: ```C++ enum class KeyOfK { foo, bar }; template
struct A {} ``` ---
×