Building ofLive: Interacting between JavaScript and C code

I recently released ofLive, a tool embedding the Emscripten version of OpenFrameworks and binding it to an HTML5 code editor (in this case ACE).

One of the main challenge I encountered when building the application was how to communicate between Emscripten generated code and regular JavaScript. In this post, I will briefly explain how to create an Emscripten library that allow that.

To build ofLive, I used the following C++ libraries:

I also created a small website using PHP/MySQL, see the source in the ofLive repository.

Building a library, the basics

To build a JavaScript library for Emscripten, I first started with a base template:

var LibraryOfLive = {
    $OFLIVE: {
     },

     editor_init: function()
     {
     },
}

autoAddDeps(LibraryOfLive, '$OFLIVE');
mergeInto(LibraryManager.library, LibraryOfLive);

The $OFLIVE array is where you put all the content you need to access outside of Emscripten, you can put for example function pointers that will allow you to call C code directly from JavaScript. The editor_init function will be registered by Emscripten at compilation time, so you can call it from c to invoke JavaScript code. It’s important to note that this function must have a corresponding declaration in the C code. Here’s the header corresponding to this library :

#pragma once

extern "C" {
	//functions calling javascript library from c
	extern void editor_init();
}

The function signature in C must reflect the function signature in JavaScript. Now let’s add another function to our library, this time we will add a C function that we can call from JavaScript. Here’s the edited library code:

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);

Here, we basically use Module.cwrap to search for a C function named ‘backend_loadlua’, the other parameters allow us to specify the return type (number in this case) and the list of parameters the C function has (here a single string parameter). We also set our backend_loadlua JS function pointer in the $OFLIVE array with the result returned by Module.cwrap, so that we can call this function in JavaScript this way

OFLIVE.backend_loadlua('here lua code')

At last, we must obviously implement this function in our native code, here’s the updated header:

#pragma once

extern "C" {
	//functions calling javascript library from c
	extern void editor_init();

	//functions calling c code from javascript
	int backend_loadlua(const char* scriptcontent_from_js);
}

then the definition of the function:

int backend_loadlua(const char* scriptcontent_from_js)
{
       std::string script_content(scriptcontent_from_js);
	ofLogError() << script_content;
}

That's it for the code part, now we need to tell the Emscripten compiler about our library and our C functions that we want to export. Use the flag "--js-library" followed by the path to your JavaScript library to embed it in the final Emscripten app. Then use the flag "-s EXPORTED_FUNCTIONS" to specify the list of C functions to export to JavaScript.

Don't forget to add an underscore in front of your function name and also to specify the main function as Emscripten doesn't add it automatically (If you forget to include it, your app won't start since it can't access the main function)


--js-library ".\library_editor.js" -s EXPORTED_FUNCTIONS='["_main","_backend_loadlua"]'

Here's the final library and header source used in 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);

Small precision: you can use in your library code things like jQuery but ONLY inside a function. Also, the editor_* function are NOT available from JavaScript, only function that you want to call from C must be defined here.

#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);
}

More details about wrapping C functions are available in the official documentation.

This entry was posted in Emscripten, HTML5, OpenFrameworks. Bookmark the permalink.

Comments are closed.