J'ai étudié le mécanisme d'Emscripten en exécutant le solveur allemand de type backtrack implémenté en C dans l'environnement JavaScript en utilisant Emscripten. , Est l'histoire.
emscripten utilise LLVM / Clang pour afficher les programmes C et C ++ dans les navigateurs et nodejs/[iojs](https: C'est un système qui permet l'exécution dans un environnement JavaScript tel que //iojs.org/).
Basé sur le byte code de LLVM qui compile la source C / C ++, le ** programme de conversion pour s'exécuter sur JavaScript et la partie de l'environnement d'exécution pour l'exécuter ** sont compatibles avec gcc, make, etc. Amélioré ʻemcc](https://github.com/kripken/emscripten/blob/master/emcc) et [ʻemmake
) Et d'autres ** lignes de commande ** sont fournies. Le premier est géré par nodejs et le second par python.
La ** bibliothèque d'exécution (fonction) utilisée en C / C ++ telle que libc est conçue pour être émulée du côté JavaScript **. .. Sortie standard vers la console, dessin de SDL/OpenGL, etc. sur l'environnement ou le navigateur nodejs / iojs L'implémentation JS à réaliser, etc. est attachée en standard à emscripten (par exemple, l'implémentation émulée de libc est [/ usr / local / opt / emscripten / libexec / src / library.js
](https: //) github.com/kripken/emscripten/blob/master/src/library.js)).
Dans l'environnement OSX, j'ai installé emscripten par homebrew (brew install emscripten
).
Quand emscripten est inséré, des commandes telles que ʻemcc(commande compatible gcc / clang) sont ajoutées, et quand la commande est exécutée pour la première fois, un fichier
~ / .emscripten` avec l'environnement défini est créé.
Cependant, sous OSX, le contenu n'est pas reflété correctement, comme l'utilisation de la commande clang
attachée au système actuel. Par conséquent, modifiez-le à nouveau comme suit.
# ~/.emscripten
EMSCRIPTEN_ROOT = os.path.expanduser(
os.getenv('EMSCRIPTEN') or
'/usr/local/opt/emscripten/libexec')
LLVM_ROOT = os.path.expanduser(
os.getenv('LLVM') or
'/usr/local/opt/emscripten/libexec/llvm/bin')
NODE_JS = os.path.expanduser(
os.getenv('NODE') or
'/usr/local/opt/node/bin/node')
TMP_DIR = '/tmp'
COMPILER_ENGINE = NODE_JS
JS_ENGINES = [NODE_JS]
En spécifiant le package homebrew de cette manière, même si le package emscripten ou nodejs installé par la mise à niveau homebrew est mis à jour, il peut être exécuté sans réinitialisation à chaque fois. ..
Si vous exécutez la commande ʻemcc` vide avec ce paramètre, vous obtiendrez le résultat suivant s'il est correctement défini.
$ emcc
WARNING root: (Emscripten: settings file has changed, clearing cache)
INFO root: (Emscripten: Running sanity checks)
WARNING root: no input files
Tout d'abord, décrivez un solveur allemand carré 9x9 de type backtrack dans Spécifications C11. Séparez les fichiers dans la partie bibliothèque et la partie main
du code afin qu'ils ne puissent être utilisés que par la bibliothèque ultérieurement.
// libsudoku.c
#include <stdio.h>
// helpers for board data
static inline unsigned masks(unsigned i) {return i ? 1 << (i - 1) : 0;}
static inline unsigned row(unsigned i) {return i / 9;}
static inline unsigned col(unsigned i) {return i % 9;}
static inline unsigned blk(unsigned i) {return i / 27 * 3 + i % 9 / 3;}
extern void output(unsigned board[])
{
char buffer[(9 + 3) * (9 + 3)];
char* cur = buffer;
for (unsigned y = 0; y < 9; y++) {
for (unsigned x = 0; x < 9; x++) {
*cur++ = board[y * 9 + x] > 0 ? board[y * 9 + x] + '0' : '.';
if (x % 3 == 2) *cur++ = x == 8 ? '\n' : '|';
}
if (y == 8) {
*cur++ = '\0';
} else if (y % 3 == 2) {
for (unsigned i = 0; i < 11; i++) *cur++ = i % 4 == 3 ? '+' : '-';
*cur++ = '\n';
}
}
puts(buffer);
}
// sudoku solver
typedef void (*sudoku_cb)(unsigned board[]);
typedef struct {
unsigned board[81];
unsigned rows[9], cols[9], blks[9];
sudoku_cb callback;
} sudoku_t;
static void sudoku_init(sudoku_t* s, unsigned board[])
{
for (unsigned i = 0; i < 81; i++) {
const unsigned mask = masks(board[i]);
s->rows[row(i)] |= mask, s->cols[col(i)] |= mask, s->blks[blk(i)] |= mask;
s->board[i] = board[i];
}
}
static void sudoku_solve(sudoku_t* s, unsigned i)
{
if (i == 81) {
s->callback(s->board);
} else if (s->board[i] != 0) {
sudoku_solve(s, i + 1);
} else {
const unsigned r = row(i), c = col(i), b = blk(i);
const unsigned used = s->rows[r] | s->cols[c] | s->blks[b];
for (unsigned v = 1; v <= 9; v++) {
const unsigned mask = masks(v);
if (used & mask) continue;
s->board[i] = v;
s->rows[r] |= mask, s->cols[c] |= mask, s->blks[b] |= mask;
sudoku_solve(s, i + 1);
s->rows[r] &= ~mask, s->cols[c] &= ~mask, s->blks[b] &= ~mask;
s->board[i] = 0;
}
}
}
extern void sudoku(unsigned board[], sudoku_cb callback)
{
sudoku_t s = {
.board = {0}, .rows = {0}, .cols = {0}, .blks = {0}, .callback = callback};
sudoku_init(&s, board);
sudoku_solve(&s, 0);
}
// sudoku-main.c
#include <stdio.h>
extern void output(unsigned board[]);
typedef void (*sudoku_cb)(unsigned board[]);
extern void sudoku(unsigned board[], sudoku_cb callback);
// example problem from http://rosettacode.org/wiki/Sudoku
static unsigned problem[] = {
8, 5, 0, 0, 0, 2, 4, 0, 0,
7, 2, 0, 0, 0, 0, 0, 0, 9,
0, 0, 4, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 7, 0, 0, 2,
3, 0, 5, 0, 0, 0, 9, 0, 0,
0, 4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 8, 0, 0, 7, 0,
0, 1, 7, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 3, 6, 0, 4, 0
};
extern int main(int argc, char* argv[])
{
if (argc <= 1) {
puts("[problem]");
output(problem);
puts("[solutions]");
sudoku(problem, output);
} else {
for (int i = 1; i < argc; i++) {
char ch = -1;
for (int c = 0; c < 81; c++) {
if (ch == '\0') {
problem[c] = 0;
} else {
ch = argv[i][c];
problem[c] = '0' <= ch && ch <= '9' ? ch - '0' : 0;
}
}
puts("[problem]");
output(problem);
puts("[solution]");
sudoku(problem, output);
}
}
return 0;
}
Ce code est un programme qui peut être normalement compilé / exécuté avec gcc / clang comme indiqué ci-dessous.
$ clang -std=c11 -Wall -Wextra libsudoku.c sudoku-main.c -o sudoku
$ ./sudoku
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.
[solutions]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241
Vous pouvez le compiler en code JavaScript simplement en changeant la partie clang
dans la construction ci-dessus en ʻemcc et en ** ajoutant
.js` au nom du fichier de sortie **.
Le fichier js généré est ** autonome et exécutable sur nodejs / iojs **.
$ emcc -std=c11 -Wall -Wextra libsudoku.c sudoku-main.c -o sudoku.js
$ iojs sudoku.js
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.
[solutions]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241
Le code généré par Emscripten n'est pas ** du code JS qui peut être utilisé comme une fonction JavaScript normale (comme un traducteur tel que CoffeeScript) **. Ce qui est généré est une combinaison du bytecode LLVM (en supposant le modèle d'exécution du langage C) et de son environnement d'exécution. Par exemple, un mécanisme de gestion quantitative de zone de mémoire tel que HEAP existe dans le code JS.
C'est la même chose même si la bibliothèque est générée et que la fonction «externe» que possède la bibliothèque est celle qui exécute le code d'octet en tant que fonction ** C sur l'environnement d'exécution intégré dans la bibliothèque. Il est **.
Par conséquent, afin d'utiliser le code généré par Emscripten comme bibliothèque pour JavaScript, une certaine ingéniosité est requise à certains endroits. Dans le document officiel, Interagir avec le code, les informations nécessaires sont décrites. Cependant, si vous ne reconnaissez pas la différence majeure qu'il s'agit de ** moyens d'accès à l'environnement d'exécution du programme C dans la bibliothèque **, vous en serez accro.
Pour générer libsudoku.js
à utiliser comme bibliothèque sur nodejs / iojs à partir de libsudoku.c
, vous devez spécifier les options attachées -s EXPORTED_FUNCTIONS
et -s RESERVED_FUNCTION_POINTERS
comme suit. il y a.
$ emcc -Wall -Wextra -std=c11 libsudoku.c -o libsudoku.js \
-s EXPORTED_FUNCTIONS="['_sudoku','_output']" \
-s RESERVED_FUNCTION_POINTERS=20
Dans l'ancien ʻEXPORTED_FUNCTIONS, spécifiez le nom de la fonction ʻextern
en C pour l'appeler du côté JavaScript. Cependant, au lieu de lister les noms des fonctions C tels quels, listez les noms ** précédés de _
.
Ce dernier, RESERVED_FUNCTION_POINTERS
, est une variable nécessaire pour ** lorsque vous devez appeler une fonction qui passe un pointeur vers une fonction de rappel, telle que la fonction sudoku (tableau, callback)
, du côté JavaScript **. Ici, réservez le nombre de pointeurs de fonction qui peuvent être définis sur l'environnement d'exécution de code d'octet afin de rappeler la fonction
JavaScript. Puisque libsudoku.js
n'est pas exécuté en parallèle, 1
est suffisant pour libsudoku.js, mais 20
est spécifié ici.
La documentation dit que vous pouvez utiliser ccall
et cwrap
, mais ils ne listent que lorsque la valeur simple number
est échangée, et ont été générés pour utiliser des tableaux et des fonctions comme arguments. J'ai analysé le code et fait des essais et des erreurs.
ccall
Tout d'abord, le wrapper qui appelle void output (unsigned board [])
de libsudoku.c
est le suivant.
var libsudoku = require("./libsudoku.js");
// use ccall to call emscripten C function
var output = function (board) {
// unsigned[] as Uint8Array view of Uint32Array
var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
// ccall(cfuncname, return_type, argument_types, arguments)
// type: "string", "number", "array" or undefined as return type
return libsudoku.ccall("output", undefined, ["array"], [unsignedBoard]);
};
Ici, nous appelons ccall
comme valeur de tableau d'Emscripten avec ʻArrayde JavaScript. L'argument est
library.ccall (nom de la fonction, type de retour, liste des types d'arguments, liste des valeurs des arguments). Ce nom de fonction sera ** le nom de la fonction en C sans le début
_` **, pas l'option de compilation.
Si la valeur de retour est «void», spécifiez «non défini». Le type d'argument peut être soit " number "
, " string "
ou " array "
. Cette spécification de type n'est pas le type dans la déclaration de fonction C, mais le ** type de valeur JavaScript ** qui est réellement passé lors de son appel et de son exécution.
Le type de pointeur de l'argument de ʻoutput () est ʻunsigned (int)
, mais ** Emscripten semble traiter le type ʻintcomme un entier 32 bits **. Et ** l'ordre des octets semble hériter de la dépendance de l'environnement CPU de
TypedArray` tel qu'il est ** (le nouveau Uint32Array (tampon), etc. est décrit dans le code généré).
Si le tableau 32 bits est géré en langage C, l'argument de type ** " array "
lors de l'appel avec ccall
doit être 8 bits ʻInt8Array ou ʻUint8Array
**. En regardant l'implémentation ccall
dans la bibliothèque, l'argument passé lors de l'appel ** copie sa valeur dans la zone mémoire de la bibliothèque et provoque l'exécution de bytecode **. La "chaîne" et le "tableau" indiquent la méthode de copie. Dans le cas de «« array »«, il est copié avec «HEAP8 [...] = array [i]», donc si vous passez «Uint32Array» etc. à «ccall» tel quel, il sera tronqué à 8 bits. Au fait, «string» est passé à C sous la forme d'une chaîne de caractères UTF8 «char».
Le code suivant implémente un wrapper pour sudoku (board, callback)
en utilisant un autre cwrap
. ccall (nom de fonction, type de retour, liste de types d'argument, liste de valeurs d'argument)
est équivalent à cwrap (nom de fonction, type de retour, liste de types d'argument) (liste de valeurs d'argument)
. Lorsque vous appelez la fonction emscripten plusieurs fois, il est préférable de mettre en cache la fonction retournée par cwrap
et de l'appeler en ne passant que l'argument.
Le point ici est que la fonction de rappel JavaScript est définie dans l'environnement d'exécution de code d'octet et que sa valeur de pointeur est passée en tant qu'argument de la fonction créée par cwrap
comme" nombre "
.
// use cwrap to call emscripten C function with callback
var sudoku = function sudoku(board, callback) {
// NOTE: For using addFunction(),
// emcc option "-s REQUIRED_FUNCTION_POINTERS=10" (more than 1) required
var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
// NOTE: copy memory value as array for async use in callback(like IO)
callback([].slice.call(r));
});
var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
sudoku._cfunc(unsignedBoard, callbackPtr);
libsudoku.Runtime.removeFunction(callbackPtr);
};
// pointer as a "number"
sudoku._cfunc = libsudoku.cwrap("sudoku", undefined, ["array", "number"]);
Utilisez ptr = library.Runtime.addFunction (function)
pour définir la fonction
JavaScript dans l'environnement bytecode et recevoir sa valeur de pointeur.
Le nombre d'arguments à la compilation RESERVED_FUNCTION_POINTERS
fait référence au nombre de fonctions JavaScript qui peuvent être enregistrées avec ʻaddFunction` en même temps.
Les arguments passés à la fonction appelée dans le callback sont les nombres exacts dans l'environnement d'exécution (pas les valeurs dans ccall
etc.). ** S'il s'agit d'un pointeur, il sera utilisé comme valeur de décalage de HEAP (library.HEAPU8
etc.) lors de l'exécution **. Pour masquer cela, ** la fonction de rappel a également besoin d'un wrapper **. Dans ce wrapper de rappel, nous créons un ʻArray` régulier à partir de l'offset de mémoire et appelons le rappel réel.
var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
callback([].slice.call(r));
});
Puisque le result
passé dans le callback est ʻunigned [81] sur C, découpez un tableau de 81 entiers 32 bits de HEAP avec ʻUint32Array
, convertissez-le en un ʻArray` normal, et utilisez la fonction de rappel. J'essaye d'appeler. «ccall» et «cwrap» effectuent un tel accès HEAP à l'intérieur pour convertir entre les objets JavaScript et les valeurs de mémoire.
Non limité au pointeur de fonction retourné par library.Runtime.addFunction (f)
, la valeur du pointeur est passée sous forme de "nombre" à l'argument de la fonction de "ccall" ou "cwrap".
Une fois l'appel de fonction terminé, supprimez-le avec library.Runtime.removeFunction (ptr)
et ouvrez un emplacement où ʻaddFunction` peut être fait. Les bibliothèques Emscripten nécessitent cette ** gestion des ressources manuellement ** comme en C.
Le code qui utilise le wrapper ci-dessus et effectue le même traitement que sudoku-main.c
est le suivant.
// sudoku-call-libsudoku.js
var libsudoku = require("./libsudoku.js");
var output = function (board) {
var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
return libsudoku.ccall("output", undefined, ["array"], [unsignedBoard]);
};
var sudoku = function sudoku(board, callback) {
var callbackPtr = libsudoku.Runtime.addFunction(function (resultPtr) {
var r = new Uint32Array(libsudoku.HEAPU8.buffer, resultPtr, 81);
callback([].slice.call(r));
});
var unsignedBoard = new Uint8Array(new Uint32Array(board).buffer);
sudoku._cfunc(unsignedBoard, callbackPtr);
libsudoku.Runtime.removeFunction(callbackPtr);
};
sudoku._cfunc = libsudoku.cwrap("sudoku", undefined, ["array", "number"]);
// main
if (process.argv.length <= 2) {
// example problem from http://rosettacode.org/wiki/Sudoku
var problem = [
8, 5, 0, 0, 0, 2, 4, 0, 0,
7, 2, 0, 0, 0, 0, 0, 0, 9,
0, 0, 4, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 7, 0, 0, 2,
3, 0, 5, 0, 0, 0, 9, 0, 0,
0, 4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 8, 0, 0, 7, 0,
0, 1, 7, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 3, 6, 0, 4, 0
];
console.log("[problem]");
output(problem);
console.log("[solution]");
sudoku(problem, output);
} else {
for (var i = 2; i < process.argv.length; i++) {
var problem = Array(81);
for (var j = 0; j < 81; j++) {
var ch = process.argv[i].charCodeAt(j);
problem[j] = 0 <= ch && ch <= 9 ? v : 0;
}
console.log("[problem]");
output(problem);
console.log("[solution]");
sudoku(problem, output);
}
}
Comme indiqué ci-dessous, il peut être exécuté tel quel sans ** bibliothèque externe requise **, tout comme lors de la compilation avec main
.
$ iojs sudoku-call-libsudoku.js
[problem]
85.|..2|4..
72.|...|..9
..4|...|...
---+---+---
...|1.7|..2
3.5|...|9..
.4.|...|...
---+---+---
...|.8.|.7.
.17|...|...
...|.36|.4.
[solution]
859|612|437
723|854|169
164|379|528
---+---+---
986|147|352
375|268|914
241|593|786
---+---+---
432|981|675
617|425|893
598|736|241
Le temps d'exécution de sudoku
pour une exécution native, sudoku.js
qui convertit tout le code C et sudoku-call-libsudoku.js
qui utilise la bibliothèque libsudoku.js
est le suivant.
$ time ./sudoku
real 0m0.020s
user 0m0.015s
sys 0m0.002s
$ time iojs sudoku-call-libsudoku.js
real 0m0.355s
user 0m0.313s
sys 0m0.037s
$ time iojs sudoku.js
real 0m0.888s
user 0m0.312s
sys 0m0.045s
Il semble qu'il est plus rapide d'utiliser uniquement la bibliothèque que d'implémenter main en C (bien que la fonction de bibliothèque externe évite printf
etc. et n'utilise que put ()
).
À propos, le temps d'exécution dans Code implémenté en arrière avec JavaScript est
$ time iojs sudoku-backtrack.js
real 0m0.723s
user 0m0.684s
sys 0m0.030s
est devenu.
Résoudre la carte une seule fois est plus rapide que de convertir tout le code C, probablement parce que le coût de la partie principale a un effet important. Il a fallu deux fois plus de temps pour utiliser la bibliothèque, et on peut dire que la partie solveur a été doublée avec Emscripten.
struct
par valeur avec ccall
Avec ccall
et cwrap
, il semble qu'il n'y ait aucun moyen de passer une ** valeur struct
** (non passée par un pointeur), telle que vec4
avec quatre float
s.
Semblables à gcc / clang, les options d'optimisation et de débogage incluent O0-3 et g1-4, mais leurs significations sont différentes et ont des significations spécifiques à Emscripten.
--O0: Aucune optimisation (par défaut)
--O1: le code de débogage est supprimé
--O2: la sortie est minifiée (le fichier .js.mem
est généré)
--O3: optimisation supplémentaire (fichier légèrement plus petit que O2)
Le fichier .js.mem
généré à la fois par O2 et O3 est un fichier indispensable pour exécuter le code .js
généré.
L'option de débogage est utilisée en combinaison avec l'option d'optimisation après O2 pour supprimer minify **.
--g1: Enregistrer vide --g2: enregistrer le nom de la fonction --g3: enregistrer le nom de la variable --g4: Générer la carte source (fichier .map)
Le code source complet est dans l'essentiel avec Makefile
.