J’ai récemment mis en ligne ofLive, un outil permettant de coder avec OpenFrameworks directement dans un navigateur via l’éditeur de code HTML5 ACE.
Lors de la création de cet outil, le principal problème à résoudre était comment faire communiquer le code généré par Emscripten avec du code JavaScript classique. J’explique ci-après comment je m’y suis pris via la création d’une librairie JavaScript pour Emscripten.
Pour réaliser ofLive, j’utilise les librairies suivantes :
- La dernière version nightly de OpenFrameworks et du plugin ofxEmscripten
- Une version légèrement modifiée de ofxLua
- La partie application OpenFrameworks c++ de ofLive
J’ai également créé à cette occasion un site web en PHP/MySQL, les sources sont disponible sur le dépôt GitHub de ofLive
Conception d’une librairie, les bases
Pour créer ma librairie JavaScript compatible Emscripten, je suis d’abord parti de la base de code suivante :
var LibraryOfLive = { $OFLIVE: { }, editor_init: function() { }, } autoAddDeps(LibraryOfLive, '$OFLIVE'); mergeInto(LibraryManager.library, LibraryOfLive);
Le tableau $OFLIVE est prévu pour contenir toute les variables qui doivent être accessibles en dehors de Emscripten, il est notamment possible d’y stocker des pointeurs vers des fonctions C. La fonction editor_init est prise en compte par Emscripten lors de la compilation, ce qui permet de l’appeler dans le code C afin d’invoquer du code JavaScript. Cette fonction doit avoir une déclaration correspondante dans la partie native de l’application au niveau d’un fichier d’en-tête .h :
#pragma once extern "C" { //fonctions JavaScript utilisées depuis C extern void editor_init(); }
La signature de la fonction C doit correspondre à la signature de la fonction en JavaScript. Ceci fait, nous pouvons alors ajouter une nouvelle fonction à notre librarie, il s’agira cette fois d’une fonction C utilisable par JavaScript. Voici le code de la librairie mis à jour :
var LibraryOfLive = { $OFLIVE: { backend_loadlua: null, }, editor_init: function() { //bind c glue functions OFLIVE.backend_loadlua = Module.cwrap('backend_loadlua','number',['string']); }, } autoAddDeps(LibraryOfLive, '$OFLIVE'); mergeInto(LibraryManager.library, LibraryOfLive);
Ici, nous utilisons la fonction Module.cwrap pour obtenir un pointeur vers une fonction C nommée ‘backend_loadlua’, les paramètres qui suivent nous permettent de spécifier le type retour (‘number’ dans ce cas) et la liste des paramètres de la fonction C (ici un seul paramètre ‘string’). Le résultat est stocké dans notre variable backend_loadlua du tableau $OFLIVE, ce qui nous permet d’appeler cette fonction directement en JavaScript avec le code suivant :
OFLIVE.backend_loadlua('ici notre code lua')
Pour finir, nous devons bien évidemment ajouter cette fonction C dans notre code natif, voici l’en-tête mis à jour :
#pragma once extern "C" { //fonctions JavaScript utilisées depuis C extern void editor_init(); //fonctions C utilisées depuis JavaScript int backend_loadlua(const char* scriptcontent_from_js); }
et le contenu de la fonction dans le fichier source :
int backend_loadlua(const char* scriptcontent_from_js) { std::string script_content(scriptcontent_from_js); ofLogError() << script_content; }
C’est tout pour la partie code, il nous faut maintenant préciser à Emscripten où se trouve notre librairie et les fonctions à exporter. Le paramètre « –js_library » suivi du chemin vers notre librairie permet de l’intégrer à l’application Emscripten finale. Le paramètre « -s EXPORTED_FUNCTIONS » permet de spécifier la liste des fonctions C à exporter vers JavaScript.
N’oubliez pas d’ajouter un tiret-bas en face du nom de chaque fonctions et aussi d’ajouter à la liste la fonction « main », que Emscripten n’ajoute pas automatiquement dans notre cas. (Sinon notre application ne pourra pas démarrer au chargement de la page Web)
--js-library ".\library_editor.js" -s EXPORTED_FUNCTIONS='["_main","_backend_loadlua"]'
Voici le code final de la librairie et l’en-tête .h tels qu’ils apparaissent dans ofLive :
var LibraryOfLive = { $OFLIVE: { editor: null, backend_loadlua: null, backend_newscript: null, backend_openscript: null, backend_savescript: null, opened_script: "", readonly_script: false, }, editor_init: function() { OFLIVE.editor = ace.edit("editor"); OFLIVE.editor.setTheme("ace/theme/monokai"); OFLIVE.editor.getSession().setMode("ace/mode/lua"); //mount read/write filesystem FS.mkdir('/oflivescripts'); FS.mount(IDBFS,{},'/oflivescripts'); Module.print("ofLive: Start scripts sync..."); Module.syncdone = 0; FS.syncfs(true,function(err) { assert(!err); Module.print("OfLive: End scripts sync"); Module.syncdone = 1; }); //check if we load a shared script, in this case readonly mode if($.trim($('#share_content').text())) { OFLIVE.opened_script = $('#name_script').text(); OFLIVE.readonly_script = true; $('#save_script').attr('class','disabled_link'); $('#shared_script').attr('class','disabled_link'); } //bind c glue functions OFLIVE.backend_loadlua = Module.cwrap('backend_loadlua','number',['string']); OFLIVE.backend_newscript = Module.cwrap('backend_newscript','number',['string']); OFLIVE.backend_openscript = Module.cwrap('backend_openscript','number',['string','number','string']); OFLIVE.backend_savescript = Module.cwrap('backend_savescript','number',['string','string']); //custom commands OFLIVE.editor.commands.addCommand({ name: 'saveScript', bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, exec: function(editor) { //check first if the script is read only and if a name exist if(OFLIVE.readonly_script == true) return; //no name, open name input popup if(OFLIVE.opened_script == "") { $("#save_script_name").click(); } else { OFLIVE.backend_savescript($('#name_script').text(),editor.getValue()); OFLIVE.backend_loadlua(editor.getValue()); } }, readOnly: false }); }, editor_loadscript: function(scriptptr) { var scriptcontent = Pointer_stringify(scriptptr); OFLIVE.editor.setValue(scriptcontent); }, editor_isshare: function() { if($.trim($('#share_content').text())) return 1; return 0; }, } autoAddDeps(LibraryOfLive, '$OFLIVE'); mergeInto(LibraryManager.library, LibraryOfLive);
Dernière précisions : Il est possible d’utiliser du code jQuery dans notre librairie mais SEULEMENT à l’intérieur d’une fonction. Les fonctions editor_* ne sont PAS accessibles depuis JavaScript, seule les fonctions que l’on souhaite appeler via C sont définies à ce niveau.
#pragma once extern "C" { //functions calling javascript library from c extern void editor_init(); extern void editor_loadscript(const char* scriptcontent); extern int editor_isshare(); //functions calling c code from javascript int backend_loadlua(const char* scriptcontent); int backend_newscript(const char* script_name); int backend_openscript(const char* script_name,int isExample,const char* type); int backend_savescript(const char* script_name,const char* scriptcontent); }
Plus de détails sur l’utilisation de fonctions C en JavaScript dans la documentation officielle (EN) .