JavaScript/C++ application framework
LoCO is a set of Qt C++ classes that make it easy to create command-line and GUI applications with any language that compiles to JavaScript.
JavaScript is used to glue together binary components, optionally loaded at run-time, developed in C++.
Objects are connected through signals/slots or by direct reference through a QObject pointer, giving to the objects the responsibility to check the interface semantics or the object type.
##What does it look like ?
Say you want to create an OpenGL-based application with a HUD-like GUI, here is how you do it.
JavaScript
The OpenGL app is stored into a binary run-time loadable object and knows nothing about the surrounding environment(i.e. LoCO); it simply exposes invokabe QObject slots or Q_INVOKABLE methods.
var glapp = Loco.ctx.loadQtPlugin( Loco.ctx.args[ 2 ] );
var glformat = glapp.glFormat ? glapp.glFormat : { alpha: true, depth: true };
var view = Loco.gui.create( "GraphicsView" );
view.initGL.connect( glapp.initGL );
view.createOpenGLViewport( glformat );
view.createGraphicsSceneProxy();
var ww = Loco.gui.create( "GraphicsWebWindow" );
ww.addObjectToContext( glapp, "app" );
// add GUI object to access e.g. native dialogs
ww.addObjectToContext( Loco.gui, "gui" );
ww.setTransparent();
ww.syncLoad( "./test33-overlay.html" );
view.show( 50, 50, 800, 600 );C++ application
The application is implemented as a stand-alone dynamically loadable object which exposes a set of methods invoked from the run-time environment. There is no requirement for the application to know anything about the LoCO environment; a set of classes is however provided to help with sharing common information with the LoCO run-time and other loaded modules should the need arise.
class GLApp : public QObject, ... {
Q_OBJECT
Q_PROPERTY( QVariantMap bkcolor READ GetBkColor
WRITE SetBkColor )
...
public slots:
void initGL() {
GLenum status = glewInit();
if( status != GLEW_OK ) {
emit error( QString( "GLEW initialization failed" ) +
QString( (char*) glewGetErrorString( status ) ) );
return;
}
... C++ driver application
LoCO is a library. You need to create an application which uses such library to configure and bootstrap the JavaScript environment.
Many options are available for creating sandboxed environment with access restriction on local files and network resources.
A sample application(total source size 6.9kb), locoplay, is provided; you can just take it and taylor it to suit your needs, although in most cases you can simply use it as it is and simply change the compile-time configuration flags as needed.
excerpt from locoplay:
...
LocoQtApp qtApp( argc, argv );
loco::App app( qtApp, argc, argv, i );
#ifdef LOCOPLAY_WKIT
app.SetInterpreter( new WebKitJSCore );
#else
app.SetInterpreter( new QScriptInterpreter );
#endif
#ifdef LOCOPLAY_FSYSTEM
app.AddModuleToJS( new FileSystem );
app.SetAllowFileAccess( true );
app.SetFilterFileAccess( false );
#endif
...
app.InitContext();
...
app.ParseCommandLine();
ret = app.Execute();There is an ovverridable naming convention to map loco::Object derived C++ objects to JavaScript ones; by default all the exposed objects(filesystem, context, console…) are accessed from JavaScript through the global Loco object.
This project started several years ago when I got tired of spending time writing C and C++ code to build MVP/MVVM/MVC application logic and binding UI events to callbacks with MOTIF/MFC/GTK/Qt/WPF… + a few mobile frameworks. After some time spent experimenting with different scripting languages and their bindings to GUI frameworks I settled on Qt for desktop applications simply because it has been and still is the fastest path to building cross-platform applications scriptable in a widespread scripting language such as ECMAScript/JavaScript.
If I had the bandwidth I would take V8 and build wrappers around native GUIs(with declarative support through standard JSON ), WebKit, filesystem, network and multimedia libraries with the addition of a resource management system and i18n support. Since however I cannot afford to do that I am using Qt.
The code you see here was physically extracted from a private project hosted on GitHub, so the GitHub project you see is now what it used to be my private development branch and it is therefore a work in progress.
The current project is a stripped down, cleaned-up, partially rewritten version of a larger and much garbled project which also had some Lua, Python, Tcl and Scheme bindings; the only additional parts I am planning to move into the new project are:
but I might make also available other pieces as brand new projects, as I did with QLua or code snippets as in the case of one of my many Any type implementations.
LoCO is distributes under the terms of the New BSD License
Use JavaScript to invoke methods and access properties in QObject-derived objects.
Connect:
Load _QObject_s from binary plugins.
Distribute applications as a standalone executable with all the resources stored in the executable itself.
Use standard web tools to develp desktop applications.
The main GUI toolkit is intended to be WebKit, but in order to support native widgets a number of wrappers are already available for accessing system dialogs and controls such as the MacOS drawer and top level menu; others are being added. In the future it will be possible to specify an entire native GUI through JSON and use Knockoutjs to manage the user interface and application logic, as done for web applications.
HUD-type interfaces are supported through WebKit or QGraphicsWidget_s layered on top of a _QGraphicsView; a proof of concept was implemented as a plugin, checkout the gui.js and gui.html files here.
A minimal OpenGL-_WebView_ integration example is available as well:
WebKit is exposed to LoCO through a WebWindow or GraphicsWebWindow(works inside an OpenGL context) object.
WebKit events are forwarded to the JavaScript context that creates the WebKit window and can therefore be handled outside the WebKit JavaScript context.
It is possible to inject JavaScript code and add QObject-derived types at page loading time.
The page DOM tree is available and can be manipulated from outside the page.
A custom plugin factory is available to add _LoCO QWidget_s directly into a web page. Example here.
Note: I plan to keep supporting the WebKit1 interface, not WebKit2 since it requires one additional process for each web page which is not something I want to have in a desktop application. The current version of QtWebKit based on WebKit 2.2 works well and will be supported for quite some time anyway with commitments to fix all the high priority bugs, and commercial support available.
Sample code:
Load a webpage and change the DOM tree on the fly setting the background to yellow and
rotating all the <div> elements.
try {
var print = Loco.console.println;
var ctx = Loco.ctx;
// command line
var cmdParam = ctx.cmdLine()[ctx.cmdLine().length - 1];
var WEBSITE = cmdParam.lastIndexOf( ".js" ) < 0 ?
cmdParam : "http://www.nyt.com";
// create main window
var ww = Loco.gui.create( "WebWindow" );
// setup main window
ww.setAttributes( {DeveloperExtrasEnabled: true,
LocalContentCanAccessFileUrls: true,
LocalContentCanAccessRemoteUrls: true,
AcceleratedCompositingEnabled: true } );
ww.setEnableContextMenu( true );
ww.setForwardKeyEvents( true );
if( !ww.syncLoad( WEBSITE, 5000 ) ) throw "Load failure";
var elements = ww.findElements( "div" );
print( elements.length );
print( elements[ 0 ].attributeNames() );
print( elements[ 0 ].eval( "this.id" ));
elements = ww.forEachElement( "*", "this.childNodes.length === 0" );
print( elements.length );
elements = ww.forEachElement( "div", "this.style.backgroundColor='yellow'; false;" );
print( elements.length );
elements = ww.forEachElement( "div", "this.style['-webkit-transform']='rotate(1deg)'; false;" );
print( elements.length );
ww.show();
} catch(e) {
Loco.console.printerrln(e);
Loco.ctx.exit( -1 );
}The code/bytes passed to the LoCO intepreter are transformed trough a chain of filters before the actual code is delivered to the interpreter. This allows to e.g. load a source file and use Skulpt or CoffeeScript to generate JavaScript code on the fly and further pass the generated code to lint.
Another use of filters is to distribute applications which read enrypted/obfuscated source code and decode it on the fly.
Sample code:
try {
Loco.console.println("Interpreter: " + Loco.ctx.jsInterpreterName() );
var WKIT = Loco.ctx.jsInterpreterName().indexOf( "webkit" ) >= 0;
Loco.ctx.include( "../../filters/coffee-script-1.2.js" );
if( !WKIT ) {
alert = Loco.console.println;
}
var c = CoffeeScript.compile( "x = 32", {bare: true} );
Loco.console.println( "COFFEE: x = 32\nJAVASCRIPT:\n" + c );
Loco.ctx.addScriptFilter( "coffeescript", "loco_coffeeCompile_",
"function loco_coffeeCompile_( coffeeCode ) {" +
" return CoffeeScript.compile( coffeeCode, {bare: true} );" +
"}" );
Loco.ctx.evalFile( "./test5.coffee", ["coffeescript"] );
Loco.ctx.exit( 0 );
} catch( e ) {
Loco.console.printerrln( e );
Loco.ctx.exit( -1 );
}Where applicable I replaced async calls with callbacks with sync calls with soft real-time guarantees i.e. invoke a synchronous function telling it how long you are willing to wait for completion.
E.g.
webWindow.syncLoad( "http://www.github.com", 5000 /*ms*/ );JavaScript code can be run through either Qt’s own script engine or the JavaScript engine embedded in WebKit. In both cases the code is JIT compiled before execution, unless you explicitly disable JIT compilation when building Qt.
In case QtScript is used it is possible to remove dependencies on QtWebKit and/or QtGUI.
JavaScript code can be evaluated from within JavaScript through the eval function or(if exposed) <LocoContextname>.eval, in the former case the code is interpreted, in the latter it goes through JIT compilation. It is also possible to entire disable access to run-time code
evaluation in slave contexts if needed.
It is possible to create other JavaScript contexts from within any existing JavaScript context and marshal data between parent and child context. This allow the creation of sandboxed contexts with only a subset of the JavaScript environment exposed to scripts.
Sample code:
//check type of current context(WebKit or QtScript)
var WEBKIT =
Loco.ctx.jsInterpreterName().indexOf( "webkit" ) >= 0,
CONTEXT_TYPE = WEBKIT ? "JavaScriptCoreContext" : "QtScriptContext";
var newCtx = Loco.ctx.create( CONTEXT_TYPE );
newCtx.onError.connect( err );
newCtx.javaScriptConsoleMessage.connect(
function( msg, line, src ) {
print( msg + " at line " + line );
} );
print( "Enable storage of source code passed for evaluation" );
newCtx.storeCode = true;
newCtx.addObject( newCtx, "ctx" );
print( "Added new context reference as 'ctx' into new context itself" );
newCtx.addObject( Loco.console, "io" );
print( "Added 'Loco.console' as 'io' into new context" );
var CODE = "io.println(ctx.code)";
print( "Evaluating code '" + CODE + "' in new context" );
newCtx.eval( CODE ); //prints out code passed to newCtx itself!Data are marshalled between contexts by
expicitly adding a QObject derived instance to the context
newCtx.addObject( newCtx, "ctx" );or wrapping a JavaScript object with a new instance of the loco::DataType which contains a data member used to access the JavaScript data
var json = { key: "value" };
newCtx.data( json, "parentData" );
newCtx.eval( "io.println( parentData.data.key )" );Network and filesystem access is controlled by resource access managers which can be configured through a regex engine or entirely replaced to:
Sample code 1: enable file and network access from driver(C++) application
...
loco::App app( qtApp, argc, argv );
#ifdef ACCESS_TO_FILESYSTEM_ENABLED
app.AddModuleToJS( new loco::FileSystem );
app.SetAllowFileAccess( true );
app.SetFilterFileAccess( false );
#endif
#ifdef ACCESS_TO_NETWORK_ENABLED
app.SetAllowNetAccess( true );
app.SetFilterNetRequests( false );
app.AddModuleToJS( new loco::Network );
#endifSample code 2: forbid read-write access to files with extension “config”
...
app.SetAllowFileAccess( true );
app.SetFilterFileAccess( true );
app.SetDenyFileRule( QRegExp( ".*\\.config$" ), QIODevice::ReadWrite );Note that access control is not entirely exposed to JavaScript, to allow for the creation of binaries that have built-in, user-configurable access control pre-configured inside the interpreter.
This is something I borrowed from the mobile environment, desktop applications are unfortunately still being developed with patterns from the 80s/90s with no concept of security or access control, any desktop application can easily access the file system to read and broadcast data over the internet with little control over it, not to mention direct access to audio/video devices with no access protection at all(this is something I’ll be addessing in future releases).
Custom protocol handlers can be installed in the web engine to allow for addition of new schemes or filtering of requests for standard schemes.
Sample code: create and install protocol handler
var customReqHandler = ctx.create( "ProtocolHandler" );
ctx.setEnableCustomProtocolHandlers( true );
ctx.addProtocolHandler( "myprotocol", customReqHandler );
function handleCustomRequest( req, reply ) {
//generate some text content
var content = ...
reply.setHeader( "ContentType", "text/html; charset ASCII" );
reply.setHeader( "ContentLength", content.length );
reply.setContent( content );
reply.setUrl( "file://" );
}
customReqHandler.handleRequest.connect( handleCustomRequest );Note that the custom protocol handler is added into the main context not the WebWindow: a scheme -> network-request-handler map is stored into the context itself, which allows to match schemes/protocols in every WebWindow with the proper handler.
Overriding the standard “http(s)” scheme it is possible to create proxys or embedded web applications with all the code written in JavaScript.
Example:
Scripts can be run in multiple contexts mapped to different threads.
Two thread objects currently available:
Input data are passed to threads through their parent context.
Results can be retrieved from the parent context/thread by:
A working example is available.
Support for tcp/udp sockets, http and ssl is included.
Sample code 1: http request
var http = Loco.net.create( "Http" );
var reply = http.get( "http://www.marinij.com", 10000 );
var headers = "";
for( var h in reply.headers ) {
headers += h + ": ";
if( reply.headers[ h ] ) headers += reply.headers[ h ];
headers += "\n";
}
if( headers.length > 0 ) fwrite( "test24-output-headers.txt", headers );
fwrite( "test24-output.html", reply.content );
if( Loco.gui ) {
var ww = Loco.gui.create( "WebWindow" );
ww.load( "test24-output.html" );
ww.show();
}Sample code 2: ssl socket
var socket = Loco.net.create( "tcp-ssl-socket" );
socket.connectTo( "bugs.kde.org", 443, 5000 );
//if ( !socket.waitForEncrypted( 50000 ) ) throw socket.errorMsg();
socket.write( "GET / HTTP/1.1\r\n"+
"Host: bugs.kde.org\r\n"+
"Connection: Close\r\n\r\n" );
var data = "";
while( socket.waitForReadyRead( 5000 ) ) data += socket.readAll();
var suffixLength = 5;
print( data.slice(0,data.length-suffixLength) ); //remove non printable chars
print( "done" );A full SSL example is available as well: encrypted fortune
It is also possible to use the WebWindow object as a headless web browser to isssue http(s) requests, handle responses and save page snapshots to image or PDF files from command line applications.