I wanted to write a user interface in HTML and Javascript but run the rest of the application in some other language that has access to system facilities. Therefore I needed bi-directional communication between the user interface and the application logic. I also wanted to run it as a normal standalone application.

GTK+ and Qt both let you embed the Webkit browser engine to do this. Another option is to run Chromium in kiosk mode.

This post is about using GTK+'s Webkit component. Future posts will look at Qt and Chromium.

Vala

I'm not a big fan of C++. It's much too complicated for me. Add in GTK+'s wacky reference counting and using GTK+ to run Webkit from C++ gets a big no from me.

Fortunately, there are many other language bindings for GTK+. Although I could have used most of these, I wanted to try something new - so I chose Vala.

Vala's pretty interesting:

  • It's statically typed.
  • It's a a lot like Java/C#, with a similar set of language features.
  • But it's compiled, via source translation to C.
  • The only dependency it has is GLib, which is available pretty much everywhere nowadays.
  • It does the GTK+ reference counting for you.
  • It's officially supported by GNOME/GTK+.

What's not to like?!

Example

We'll have two classes in our example:

  1. A main class for parsing command line options and initializing things.
  2. A window class which embeds Webkit and puts it in a GTK+ window.

Main class

As with most languages, in Vala the startup function is called main and it's declared as a static method:

using Gtk;

class WebkitExample.Main : GLib.Object
{
    public static int main(string[] args)
    {
        ...
    }
}

Our example is going to support the following command line options:

url
which URL to load into Webkit
fullscreen
run in full screen (kiosk) mode
hidecursor
hide the mouse cursor
debug
enable the Webkit developer tools in the context menu

It also needs to create and show our window class (which embeds Webkit) and initialize GTK+.

First we set the defaults for the command line options:

try
{
    url = "file://" + Path.get_dirname(FileUtils.read_link("/proc/self/exe")) + "/test.html";
}
catch (FileError e)
{
    stderr.printf("%s\n", e.message);
    return 1;
}

fullscreen = false;
hidecursor = false;
debug = false;

You'll see by default we load a file named test.html in the same directory as the program.

Parsing command line options in Vala is pretty easy. You list the options in an array, along with the type of argument expected (if any), some help text and in which variable to put the argument. For our options:

static string url;
static bool fullscreen;
static bool showcursor;
static bool debug;

const OptionEntry[] options =
{
    { "url", 'u', 0, OptionArg.STRING, out url, "page to load", "URL" },
    { "fullscreen", 'f', 0, OptionArg.NONE, out fullscreen, "run in fullscreen mode", null },
    { "hidecursor", 'h', 0, OptionArg.NONE, out hidecursor, "hide mouse cursor", null },
    { "debug", 'd', 0, OptionArg.NONE, out debug, "enable web developer tools", null },
    { null }
};

Then you create an OptionContext with a description of the program, and add the options to it:

OptionContext context = new OptionContext("- Webkit example");

context.add_main_entries(options, null);

To parse the options, call the parse method:

try
{
    context.parse(ref args);
}
catch (OptionError e)
{
    stderr.printf("%s: failed to parse arguments: %s\n", prog_name, e.message);
    return 1;
}

To initialize GTK+, we add an optional .gtkrc file (I don't use it but it's good practice) and call Gtk.init:

Gtk.rc_add_default_file("webkit-example.gtkrc");
Gtk.init(ref args);

Then we can create our GTK+ window which embeds Webkit (see the next section for details of our window class):

MainWindow w = new MainWindow(hidecursor, debug);

Make it full screen if the command line option was passed:

if (fullscreen)
{
        w.fullscreen();
}

Show it and all its children (including Webkit):

w.show_all();

And then load the URL into Webkit (this is a method on our window class which ends up calling into Webkit):

w.load(url);

Finally, we have to enter the GTK+ main loop which makes sure things are displayed and user input events are dispatched properly:

Gtk.main();

Window class

This class is going to do the following:

  • Inherit from the GTK+ Window class so it's a... erm... window.
  • Create a new Webkit browser component and add it to the window.
  • Configure things like the the window size and Webkit settings.
  • Arrange for the mouse cursor to be hidden if the user requested to do so.
  • Expose a function to Javascript running in Webkit which returns data read from standard input. This shows we can get data from Vala into the Web app.
  • Expose a function to Javascript running in Webkit which writes its argument to standard output and then terminates the application. This shows we can call Vala functions and pass them data from the Web app.

Setting things up

Here's how we declare our window class:

public class WebkitExample.MainWindow : Window
{
    private const string TITLE = "Webkit Example";

    private WebView webview;
    private Gdk.Cursor cursor;
    private static string data;

You can see we inherit from Window and define a title which we'll set below. There are private instance variables for a Webkit component (webview), an invisible mouse cursor (cursor), and data read from standard input (data).

Next we define the constructor:

public MainWindow (bool hidecursor, bool debug)
{  
    title = TITLE;
    set_default_size(800, 600);
    destroy.connect(Gtk.main_quit);

    if (hidecursor)
    {
        cursor = new Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR);
    }

Simple stuff here:

  • Set the window title and size.
  • Connect the destroy event which is fired when the user closes the window to Gtk.main_quit function which exits the application.
  • If the user wants to hide the cursor, set cursor to an invisible cursor.

Now we can create a Webkit component and initialize it:

    webview = new WebView();

    WebSettings settings = webview.get_settings();

    settings.enable_plugins = true;
    settings.enable_scripts = true;
    settings.enable_universal_access_from_file_uris = true;

Here I'm enabling plugins and scripts. I'm also enabling documents loaded from the local system to make network calls. We won't use it in this example but you'll need it if, for example, you have a user interface bundled with your application that ends up talking to a Web service somewhere.

Next we need to set up the Webkit developer tools (also known as the Web inspector).

if (debug)
{
    settings.enable_developer_extras = true;
    webview.web_inspector.inspect_web_view.connect(getInspectorView);
}

We enable the Inspect Element option in the right-click menu of the main Webkit component, which opens the Web inspector (and the rest of the developer tools like the console and network tracer)

The inspect_web_view event is fired when the user selects the menu option. We connect it to a method (getInspectorView) which returns the Webkit component we want the Webkit inspector to display itself in. The getInspectorView method is described in the next section.

Now we need to connect up another event, window_object_cleared. This is fired by Webkit when a new page is loaded. We'll connect it to a method which exposes functions for Javascript in the page to call:

webview.window_object_cleared.connect(addApp);

We'll get to addApp a bit later on.

Finally, we finish configuring Webkit and add it to the main window:

get_default_session().add_feature_by_type = typeof(CookieJar);

ScrolledWindow sWindow = new ScrolledWindow(null, null);
sWindow.set_policy(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);

sWindow.add(webview);
add(sWindow);

You can see that we enable cookies and allow the Webkit component to scroll.

Returning a Webkit component for the Webkit inspector

Here's getInspectorView, which we hooked up to the inspect_web_view event in the constructor. This involves creating separate window and Webkit components for the Web inspector:

public unowned WebView getInspectorView(WebView v)
{
    Window iWindow = new Window();
    WebView iWebview = new WebView();

    ScrolledWindow sWindow = new ScrolledWindow(null, null);
    sWindow.set_policy(PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);

    sWindow.add(iWebview);
    iWindow.add(sWindow);

Note I add the Webkit component (iWebview) to a scrolled window (sWindow) so it doesn't matter if it doesn't all fit inside. I then add the scrolled window to a top-level window (iWindow).

Next we set iWindow's title based on the main window's title and its size to the same as the main window's size. Then we show iWindow. Finally we return iWebview so the Web inspector uses it to display itself in:

    iWindow.title = title + " (Web Inspector)";

    int width, height;
    get_size(out width, out height);
    iWindow.set_default_size(width, height);

    iWindow.show_all();

    iWindow.delete_event.connect(() =>
    {
        webview.web_inspector.close();
        return false;
    });

    unowned WebView r = iWebview;
    return r;
}

Note intercepting the delete_event from the window in order to close the Web inspector before the window is destroyed. I found I got segmentation faults if I didn't do this.

Note also the unowned keyword. This means there will be no reference count on the Webkit component so it will be deleted once the user closes iWindow.

Hiding the mouse cursor

In the constructor, we set cursor to an invisible cursor if the user asked for the mouse cursor to be hidden. Let's define a function to check if cursor was set and use it on the main window and Webkit component if it was:

private void hide_cursor()
{
    if (cursor != null)
    {
        window.set_cursor(cursor);
        webview.window.set_cursor(cursor);
    }
}

I found I had to call hide_cursor in a couple of places. Firstly, whenever the mouse is moved:

public override bool motion_notify_event(Gdk.EventMotion event)
{
    hide_cursor(); 

    if (base.motion_notify_event == null)
    {
        return false;
    }
    else
    {
        return base.motion_notify_event(event);
    }
}

and secondly when the page is loaded.

Getting data from standard input

We want to read all of standard input and expose it to Javascript running in Webkit. We'll see in the next section how to expose functions for Javascript to call. What we do first is start a thread which reads from standard input:

static construct
{
    try
    {
        Thread.create<void*>(() =>
        {
            StringBuilder sb = new StringBuilder();
            char buffer[1024];

            while (!stdin.eof())
            {
                string s = stdin.gets(buffer);

                if (s != null)
                {
                    sb.append(s);
                }
            }

            lock (data)
            {
                data = sb.str;
            }

            return null;
        }, false);
    }
    catch (ThreadError e)
    {
        stderr.printf("%s: failed to create data reader thread: %s\n", Main.prog_name, e.message);
        Gtk.main_quit();
    }
}

This code is only run once, when the MainWindow class is first used. We build up a string buffer from standard input until end-of-file is reached.

Then we set the data class variable that we declared at the top of the class to the contents of the string buffer. Note we take out a lock on data first because we're going to be reading it from a different thread:

public static JSCore.Value getData(JSCore.Context ctx,
                                   JSCore.Object function,
                                   JSCore.Object thisObject,
                                   JSCore.ConstValue[] arguments,
                                   out JSCore.Value exception)
{
    exception = null;

    lock (data)
    {
        return new JSCore.Value.string(ctx, new JSCore.String.with_utf8_c_string(data));
    }
}

We'll be calling this function from Javascript and exposing it to Webkit in the next section. It simply returns data to Javascript.

Passing data to Javascript

The cleanest way to pass data to Javascript is to expose functions for Javascript to call when it's ready to do so. You can then return the data from those functions.

In the constructor for MainWindow, we arranged for a method called addApp to be called whenever Webkit loaded a new page. Here's the start of addApp:

public void addApp(WebFrame frame, void *context, void *window_object)
{
    unowned JSCore.Context ctx = (JSCore.Context) context;
    JSCore.Object global = ctx.get_global_object();

Here we get the global object from the Javascript context that's passed to us. This represents the global variables in the page our Webkit component (webview) has loaded.

We can then use this to expose the getData method we defined in the previous section:

    JSCore.String name = new JSCore.String.with_utf8_c_string("app_getData");
    JSCore.Value ex;

    global.set_property(ctx,
                        name,
                        new JSCore.Object.function_with_callback(ctx, name, getData),
                        JSCore.PropertyAttribute.ReadOnly,
                        out ex);

In Javascript, getData will be available as app_getData.

Receiving data from Javascript

Let's continue the definition of addApp from the previous section to expose a method, exit, which Javascript can call to exit the application:

    name = new JSCore.String.with_utf8_c_string("app_exit");

    global.set_property(ctx,
                        name,
                        new JSCore.Object.function_with_callback(ctx, name, exit),
                        JSCore.PropertyAttribute.ReadOnly,
                        out ex);
}

exit will take an argument, which it will print to standard output before exiting the application:

public static JSCore.Value exit(JSCore.Context ctx,
                                JSCore.Object function,
                                JSCore.Object thisObject,
                                JSCore.ConstValue[] arguments,
                                out JSCore.Value exception)
{
    exception = null;

    JSCore.String js_string = arguments[0].to_string_copy(ctx, null);

    size_t max_size = js_string.get_maximum_utf8_c_string_size();
    char *c_string = new char[max_size];
    js_string.get_utf8_c_string(c_string, max_size);

    stdout.printf("%s\n", (string) c_string);

    Gtk.main_quit();

    return new JSCore.Value.null(ctx);
}

As you can see, we have to convert the Javascript string argument to UTF-8. You'll need a UTF-8 locale set up to display the string if you use any Unicode characters.

Loading a page

Finally, we need to define the load method which allows users of MainWindow to specify the page which will be loaded into Webkit:

public void load(string url)
{
    webview.open(url);
    hide_cursor();
}

It just calls the open method of our Webkit component and then hides the cursor (if necessary).

Compiling

That's it for the Vala code but there are a couple of other things to do before we can compile it and get a binary we can run.

Firstly, the class and type definitions for interoperating with Javascript aren't built into Vala. They have to be defined separately. This is done by defining them in a VAPI file.

I won't go into the details here, but the hard work has already been done by Sam Thursfield and is available here. I had to make a few patches, which are available here.

Secondly, when you compile a Vala program, especially one which uses some complex types, it's fairly common to get warnings about const and type incompatibility from the C compiler (remember Vala is translated into C). Most people ignore these but I like to compile without warnings. I've adopted a rather skanky workaround to do this. Basically, I insert a script to fix up the types in the generated C source code.

You can find all the source from this article here. You'll also find a working Makefile, a patched version of the Javascript VAPI file, my skanky workaround script and the test Web page described in the next section.

Test Web page

Finally, let's take a look at a Web page we can load into our example application. It needs to:

  • Call app_getData periodically until it returns something other than the empty string. This will be the data read from standard input and we'll display it in the page once it's read.

  • Call app_exit at some point, passing in a message which the application will write to standard output before exiting. We'll do this when the user presses a button.

The HTML turns out to be pretty simple:

<html>
<head>
<script type="text/javascript">
function check_data()
{
    var data = app_getData();

    if (data === "")
    {
        setTimeout(check_data, 1000);
    }
    else
    {
        document.getElementById('data').innerText = data;
    }
}
</script>
</head>
<body onload='check_data()'>
<p>
data: <span id="data"></span>
</p>
<input type="button" value="Exit" onclick="app_exit('goodbye from Javascript')">
</body>
</html>

We poll app_getData every second once the page has loaded. When we have the data from standard input, we display it in the #data element.

When the user clicks on the Exit button, we call app_exit with a message.

You can test it by piping some data through to the webkit-example binary you get by building the source. For example:

echo 'Hello World!' | ./webkit-example


blog comments powered by Disqus

Published

2012-11-10

Tags