Outils pour utilisateurs

Outils du site


prog:emscripten

Téléchargement et installation et mise à jour

git clone --depth 1 https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

Download and install

Utilisations

Emscripten va permettre de générer un fichier .js qui va contenir le code passerelle entre le code JavaScript et le code WebAssembly qui sera dans le fichier .wasm.

C'est donc un outil plutôt adapté pour créer des librairies, même s'il est possible de créer des programmes entiers avec.

An Introduction to WebAssembly for JavaScript Developers Archive du 21/04/2021 le 24/07/2021

Emscripting a C library to Wasm Archive du 31/07/2020 le 04/08/2021

Compilation

Options de compilation

Liste des settings (-s).

Pour réduire la taille du wrapper:

  • -s ENVIRONMENT=web,worker : réduire les fonctionnalités en supprimant le support de node.js. Réduit très légèrement la taille du wrapper mais enlève des dépendances externes. Utile pour la mise en production dans un projet Angular. Ne doit pas être enlevé si les tests exécutés par node.js,
  • --closure 1 -s MODULARIZE=1 : closure fait de la minification. Afin d'éviter les effets de bord avec d'autres variables globales en Javascript, il est forcement recommandé d'encapsuler le code dans un module,
  • --no-entry : désactive la fonction main qui est inutile si la sortie Javascript est une librairie et non une application indépendante,
  • -s ASSERTIONS=0 : désactive des sécurités.

Pour réduire la taille du wasm:

  • -flto : active l'optimisation globale du lieur,
  • -DNDEBUG : désactive le debug. N'impacte pas le code emscripten mais uniquement le code utilisateur,
  • -Oz : le bytecode est optimisé pour réduire la taille.
  • -s MALLOC='emmalloc' : utilise une version de malloc réduite en fonctionnalité mais spécifique à emscripten.

Pour le debug:

  • -O0 : aucune optimisation.
  • -g : toutes les informations de débogage,
  • -s ASSERTIONS=2 : toutes les assertions à l'exécution,
  • -s DEMANGLE_SUPPORT=1 : affichage des symboles en cas d'affichage de la trace par le compilateur,
  • --emit-symbol-map : génère un fichier .map,
  • -gsource-map : génère un .map compatible LLVM pour déboguer avec Chrome,
  • --source-map-base http://localhost:4200/assets/ : codage en dur du chemin vers le fichier .wasm uploadé et non du chemin dans l'ordinateur.

Pour mémo, le tableau des résultats. La comparaison des options -Ox se fait par rapport à -O0. Puis, les options suivantes s'accumulent avec l'option -Oz. Dans mon cas, -O2 réduit mieux la taille que -Oz. A confirmer sur de plus gros projets.

Optiongros jsgros wasmpetit jspetit wasmgros jsgros wasmpetit jspetit wasm
-O034980188803021744259279----
-O2349801841160214892269710%/0o-5,3%/-46870o-1,2%/-2550o-54,5%/-32308o
-O3349801841966214892269710%/0o-5,2%/-46064o-1,2%/-2550o-54,5%/-32308o
-Os349801842814217442347460%/0o-5,1%/-45216o0%/0o-41,4%/-24533o
-Oz349801842838217442347390%/0o-5,1%/-45192o0%/0o-41,4%/-24540o
-s ENVIRONMENT=web,workerN/AN/A21498434739N/AN/A-1,1%/-2458o0%/0o
--closure 115878084283811813734739-54,6%/-191021o0%/0o-45%/-96847o0%/0o
--no-entry15822384283811758034739-0,4%/-557o0%/0o-0,5%/-557o0%/0o
-flto158223835253117580318670%/0o-0,9%/-7585o0%/0o-8,3%/-2872o
-s ASSERTIONS=0679278351103494431510-57,1%/-90296o0%/-143o-70,3%/-82636o-1,1%/-357o
-DNDEBUG6792783511034944315100%/0o0%/0o0%/0o0%/0o
-s INVOKE_RUN=06792783511034944315100%/0o0%/0o0%/0o0%/0o
-s MALLOC='emmalloc'6792782680334944237030%/0o-1%/-8307o0%/0o-24,8%/-7807o

vcpkg

Après l'installation en utilisant le triplet wasm32-emscripten, il est possible de le cumuler avec emcmake.

source ./emsdk/emsdk_env.sh
.\vcpkg\bootstrap-vcpkg.bat
.\vcpkg\vcpkg install --triplet wasm32-emscripten spdlog
emcmake cmake -S . -B [build_directory] -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=.../emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DVCPKG_TARGET_TRIPLET=wasm32-emscripten -DCMAKE_TOOLCHAIN_FILE=.../vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build [build_directory]

Exemples

Librairies

Une simple librairie

  • Code source
#include <emscripten.h>
 
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
    return a + b;
}
  • Compilation
emcc add.c -o add.js -s EXPORT_NAME="'ModuleName'"
  • Utilisation depuis du code html

Puis on peut utiliser la fonction add depuis le code HTML en passant par la librairie JavaScript générée. Il faudra bien mettre en ligne les deux fichiers .js et .wasm.

Par défaut, tous les objets exportés sont regroupés dans un objet global ModuleName. La fonction est accessible depuis son nom “mangled” (func1@a@@AAEXH@Z par exemple), avec l'ajout d'un préfix _. Decorated Names Archive du 05/09/2018 le 04/01/2021

<head>
<script type="text/javascript">
  var ModuleName = {
    onRuntimeInitialized: function() {
      const a = 1.5;
      const b = 3.8;
      const ret = ModuleName._add(1.5, 3.8);
      console.log(`${a} + ${b} = ${ret}`);
    }
  };
</script>
<script type="text/javascript" src="add.js"></script>
</head>
<body>
</body>

How To Write A WebAssembly App in C/C++ Archive du 12/11/2020 le 04/01/2021

FAQ Archive du 11/01/2021 le 19/03/2021

Module

Certaines librairies imposent l'utilisation des modules (jest).

Il faut utiliser l'option -s MODULARIZE=1.

  • Utilisation en HTML

Il faut bien mettre type="test/javascript" et pas type="module".

<script type="text/javascript" src="add.js"></script>
<script type="text/javascript">
  ModuleName().then(async instance => {
    const a = 1.5;
    const b = 3.8;
    const ret = instance.__Z3addii(1.5, 3.8);
    console.log(`${a} + ${b} = ${ret}`);
  });
</script>
  • Utilisation avec Jest
const Module = require('./add.js');
 
test('test name', async () => {
  return Module().then(async instance => {
    instance.__Z3addii(...);
  });
});

Bindings

C'est quand même plus pratique d'utiliser du style orienté objet plutôt que des symboles “mangled”.

Il faut donc déclarer manuellement :

  • toutes les classes que l'on souhaite utiliser,
  • toutes les fonctions, y compris constructeur, que l'on souhaite utiliser pour chaque classe,
  • toutes les classes utilisées dans les arguments et les valeurs retours des fonctions qui seront utilisées,
  • les relations d'héritage si le type parent est utilisé pour exécuter les fonctions de l'enfant (polymorphisme).

Dans mes tests, j'ai vu que le lieur d'emscripten semble avoir du mal avec les symboles weak multiples. Il est préférable de tout mettre dans un même fichier source.

EMSCRIPTEN_BINDINGS(jessica)
{
  class_<Jessica::Data::Geotechnical::IFoundationStrip>("IFoundationStrip")
      .smart_ptr<
          std::shared_ptr<Jessica::Data::Geotechnical::IFoundationStrip>>(
          "IFoundationStrip")
      .function(
          "setB",
          select_overload<std::shared_ptr<
              Jessica::Data::Geotechnical::IFoundationStrip>(double) const>(
              &Jessica::Data::Geotechnical::IFoundationStrip::B))
      .function("getB", select_overload<double() const>(
                            &Jessica::Data::Geotechnical::IFoundationStrip::B));
 
  class_<Jessica::Data::Geotechnical::FoundationStrip<
             Jessica::Util::Decorator::LogCall<
                 Jessica::Util::Decorator::LogDuration<
                     Jessica::Data::Geotechnical::FoundationStripImpl>>>,
         base<Jessica::Data::Geotechnical::IFoundationStrip>>(
      "FoundationStripDeco")
      .constructor<>()
      .function(
          "clone",
          &Jessica::Data::Geotechnical::FoundationStrip<
              Jessica::Util::Decorator::LogCall<
                  Jessica::Util::Decorator::LogDuration<
                      Jessica::Data::Geotechnical::FoundationStripImpl>>>::
              Clone)
      .function(
          "setB",
          select_overload<std::shared_ptr<
              Jessica::Data::Geotechnical::IFoundationStrip>(double) const>(
              &Jessica::Data::Geotechnical::FoundationStrip<
                  Jessica::Util::Decorator::LogCall<
                      Jessica::Util::Decorator::LogDuration<
                          Jessica::Data::Geotechnical::FoundationStripImpl>>>::
                  B))
      .function(
          "getB",
          select_overload<double() const>(
              &Jessica::Data::Geotechnical::FoundationStrip<
                  Jessica::Util::Decorator::LogCall<
                      Jessica::Util::Decorator::LogDuration<
                          Jessica::Data::Geotechnical::FoundationStripImpl>>>::
                  B));
}

WebAssembly.instantiateStreaming en TypeScript

  • Sans le wrapper .js

Il est possible d'utiliser WebAssembly.instantiateStreaming pour charger le fichier .wasm sans passer par le fichier .js qui sert de wrapper.

Exemple en TypeScript. Nécessite npm install --save @assemblyscript/loader.

import { instantiateStreaming } from "@assemblyscript/loader";
 
interface MyApi extends Record<string, unknown> {
  add(a: number, b: number): number;
}
 
async function getWasm() {
  const imports: any = {};
  var module = instantiateStreaming<MyApi>(fetch('assets/add.wasm'), imports);
  return module;
}
 
@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css'],
})
export class MainComponent implements OnInit {
  private exports?: MyApi;
  constructor() {
    getWasm().then((mod) => {
      this.exports = mod.exports;
      console.log(this.exports.add(2, 4));
    });
  }
 
  ngOnInit(): void { }
}

Mais attention, le deuxième paramètre de instantiateStreaming peut être nécessaire pour définir un environnement précis et de fonctions de callback entre wasm et javascript. C'est notamment le cas pour l'utilisation des bindings de EMSCRIPTEN_BINDINGS.

Dans cette nouvelle configuration, le chargement d'un wasm va donner le message d'erreur :

LinkError: import object field '_embind_register_class' is not a Function

Et cette fonction javascript est déclarée dans le fichier .js générée par emscripten. L'utilisation du fichier .js comme wrapper sera probablement nécessaire.

  • Avec le wrapper .js

Il faut commencer par autoriser l'utilisation du code javascript.

tsconfig.json
{
  "compilerOptions": {
    ...,
    "allowJs": true,
    "esModuleInterop": true
  }
}

Ensuite, utiliser le code TypeScript suivant :

main.component.ts
import { Component, OnInit } from '@angular/core';
import * as Module from './jessica';
 
@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css'],
})
export class MainComponent implements OnInit {
  private instance?: any;
  constructor() {
    Module.default().then(async (instance: any) => {
      this.instance = instance;
      const vert1 = new this.instance.VerticalEccentricRaw();
      const vert2 = vert1.setE(0.2);
      console.log('getE ' + vert2.getE());
    });
  }
 
  ngOnInit(): void {}
}
  • Compatibilité avec node.js

Il est possible que le compilateur râle si on utilise des fonctionnalités de type bindings.

Si la compatibilité avec node.js n'est pas indispensable, on peut tout simplement désactiver via l'option de compilation -s ENVIRONMENT=web,worker. Cette option ne réduit pas la taille du fichier .wasm et seulement de façon marginal le .js mais cela enlève les dépendances spécifiques à node.js.

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
        - install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "path": false }

Pour les modules path, crypto et stream, on peut utiliser une librairie annexe pour résoudre ce problème :

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "path": ["./node_modules/path-browserify"],
      "crypto": ["./node_modules/crypto-browserify"],
      "stream": ["./node_modules/stream-browserify"]
    },
  }
}

et installer les dépendances :

npm i path-browserify -D
npm i crypto-browserify -D
npm i stream-browserify -D

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. Archive du 17/05/2021 le 24/07/2021

Apparemment, il existe un paquet qui résout tous ces problèmes en une fois : node-polyfill-webpack-plugin. Cela résout les problèmes pour les modules assert, buffer, console, constants, crypto, domain, events, http, https, os, path, punycode, process, querystring, stream, _stream_duplex, _stream_passthrough, _stream_readable, _stream_transform, _stream_writable, string_decoder, sys, timers, tty, url, util, vm, zlib.

Par contre, il n'existe pas de module externe pour la fonctionnalité fs. Dans ce cas, le compilateur ne vous proposera pas de solution :

./projects/app-main/src/app/ui/main/jessica.js:380:30-43 - Error: Module not found: Error: Can't resolve 'fs' in '...\projects\app-main\src\app\ui\main'

Dans ce cas, il faut désactiver le module. Les fonctions associées ne fonctionneront pas.

Il se peut que la compatibilité soient nécessaires pour les tests qui nécessitent node.js pour s'exécuter mais que TypeScript ne nécessite pas ces dépendances. Il est possible de tout désactiver pour TypeScript en les mettant tous à false dans la rubrique browser ci-dessous et sans installer les dépendances via npm.

package.json
{
  ...,
  "browser": {
    "fs": false
  }
}

Error: Module not found: Error: Can't resolve 'fs' Archive du 01/11/2017 le 24/07/2021

Erreurs

  • UnboundTypeError: Cannot call FoundationStripRaw.setB due to unbound types: N7Jessica4Data12Geotechnical19FoundationStripImpl4SetBE

Ici, la fonction FoundationStripRaw.setB a besoin du type N7Jessica4Data12Geotechnical19FoundationStripImpl4SetBE (classe Jessica::Data::Geotechnical::FoundationStripImpl::SetB).

Il suffit de rajouter dans EMSCRIPTEN_BINDINGS le class_<Jessica::Data::Load::VerticalEccentricImpl::SetE>("VerticalEccentricImpl_SetE") adapté si besoin.

prog/emscripten.txt · Dernière modification : 2022/07/18 12:49 de root