9 min read

(For more resources related to this topic, see here.)

Before we start

Most of the discussions in this article require a moderate knowledge of HTML5, JSON, and common client-side JavaScript programming. One particular exercise uses JQuery and JQuery Mobile to show how a real HTML5 application will be implemented.

Embedding WebKit

What we need to learn first is how to embed a WebKit layout engine inside our GTK+ application. Embedding WebKit means we can use HTML and CSS as our user interface instead of GTK+ or Clutter.

Time for action – embedding WebKit

With WebKitGTK+, this is a very easy task to do; just follow these steps:

  1. Create an empty Vala project without GtkBuilder and no license. Name it hello-webkit.

  2. Modify configure.ac to include WebKitGTK+ into the project. Find the following line of code in the file:

    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0])

  3. Remove the previous line and replace it with the following one:

    PKG_CHECK_MODULES(HELLO_WEBKIT, [gtk+-3.0 webkitgtk-3.0])

  4. Modify Makefile.am inside the src folder to include WebKitGTK into the Vala compilation pipeline. Find the following lines of code in the file:

    hello_webkit_VALAFLAGS =
    --pkg gtk+-3.0

  5. Remove it and replace it completely with the following lines:

    hello_webkit_VALAFLAGS =
    --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg
    libsoup-2.4

  6. Fill the hello_webkit.vala file inside the src folder with the following lines:

    using GLib;
    using Gtk;
    using WebKit;
    public class Main : WebView
    {
    public Main ()
    {
    load_html_string("<h1>Hello</h1>","/");
    }
    static int main (string[] args)
    {
    Gtk.init (ref args);
    var webView = new Main ();
    var window = new Gtk.Window();
    window.add(webView);
    window.show_all ();
    Gtk.main ();
    return 0;
    }
    }

  7. Copy the accompanying webkit-1.0.vapi file into the src folder. We need to do this, unfortunately, because the webkit-1.0.vapi file distributed with many distributions is still using GTK+ Version 2.

  8. Run it, you will see a window with the message Hello, as shown in the following screenshot:

What just happened?

What we need to do first is to include WebKit into our namespace, so we can use all the functions and classes from it.

using WebKit;

Our class is derived from the WebView widget. It is an important widget in WebKit, which is capable of showing a web page. Showing it means not only parsing and displaying the DOM properly, but that it’s capable to run the scripts and handle the styles referred to by the document. The derivation declaration is put in the class declaration as shown next:

public class Main : WebView

In our constructor, we only load a string and parse it as an HTML document. The string is Hello, styled with level 1 heading. After the execution of the following line, WebKit will parse and display the presentation of the HTML5 code inside its body:

public Main ()
{
load_html_string("<h1>Hello</h1>","/");
}

In our main function, what we need to do is create a window to put our WebView widget into. After adding the widget, we need to call the show_all() function in order to display both the window and the widget.

static int main (string[] args)
{
Gtk.init (ref args);
var webView = new Main ();
var window = new Gtk.Window();
window.add(webView);

The window content now only has a WebView widget as its sole displaying widget. At this point, we no longer use GTK+ to show our UI, but it is all written in HTML5.

Runtime with JavaScriptCore

An HTML5 application is, most of the time, accompanied by client-side scripts that are written in JavaScript and a set of styling definition written in CSS3. WebKit already provides the feature of running client-side JavaScript (running the script inside the web page) with a component called JavaScriptCore, so we don’t need to worry about it.

But how about the connection with the GNOME platform? How to make the client-side script access the GNOME objects? One approach is that we can expose our objects, which are written in Vala so that they can be used by the client-side JavaScript. This is where we will utilize JavaScriptCore.

We can think of this as a frontend and backend architecture pattern. All of the code of business process which touch GNOME will reside in the backend. They are all written in Vala and run by the main process. On the opposite side, the frontend, the code is written in JavaScript and HTML5, and is run by WebKit internally. The frontend is what the user sees while the backend is what is going on behind the scene.

Consider the following diagram of our application. The backend part is grouped inside a grey bordered box and run in the main process. The frontend is outside the box and run and displayed by WebKit. From the diagram, we can see that the frontend creates an object and calls a function in the created object. The object we create is not defined in the client side, but is actually created at the backend. We ask JavaScriptCore to act as a bridge to connect the object created at the backend to be made accessible by the frontend code.

To do this, we wrap the backend objects with JavaScriptCore class and function definitions. For each object we want to make available to frontend, we need to create a mapping in the JavaScriptCore side. In the following diagram, we first map the MyClass object, then the helloFromVala function, then the intFromVala, and so on:

Time for action – calling the Vala object from the frontend

Now let’s try and create a simple client-side JavaScript code and call an object defined at the backend:

  1. Create an empty Vala project, without GtkBuilder and no license. Name it hello-jscore.

  2. Modify configure.ac to include WebKitGTK+ exactly like our previous experiment.

  3. Modify Makefile.am inside the src folder to include WebKitGTK+ and JSCore into the Vala compilation pipeline. Find the following lines of code in the file:

    hello_jscore_VALAFLAGS =
    --pkg gtk+-3.0

  4. Remove it and replace it completely with the following lines:

    hello_jscore_VALAFLAGS =
    --vapidir . --pkg gtk+-3.0 --pkg webkit-1.0 --pkg
    libsoup-2.4 --pkg javascriptcore

  5. Fill the hello_jscore.vala file inside the src folder with the following lines of code:

    using GLib;
    using Gtk;
    using WebKit;
    using JSCore;
    public class Main : WebView
    {
    public Main ()
    {
    load_html_string("<h1>Hello</h1>" +
    "<script>alert(HelloJSCore.hello())</
    script>","/");
    window_object_cleared.connect ((frame, context) => {
    setup_js_class ((JSCore.GlobalContext) context);
    });
    }
    public static JSCore.Value helloFromVala (Context ctx,
    JSCore.Object function,
    JSCore.Object thisObject,
    JSCore.Value[] arguments,
    out JSCore.Value exception) {
    exception = null;
    var text = new String.with_utf8_c_string ("Hello from
    JSCore");
    return new JSCore.Value.string (ctx, text);
    }
    static const JSCore.StaticFunction[] js_funcs = {
    { "hello", helloFromVala, PropertyAttribute.ReadOnly },
    { null, null, 0 }
    };
    static const ClassDefinition js_class = {
    0, // version
    ClassAttribute.None, // attribute
    "HelloJSCore", // className
    null, // parentClass
    null, // static values
    js_funcs, // static functions
    null, // initialize
    null, // finalize
    null, // hasProperty
    null, // getProperty
    null, // setProperty
    null, // deleteProperty
    null, // getPropertyNames
    null, // callAsFunction
    null, // callAsConstructor
    null, // hasInstance
    null // convertToType
    };
    void setup_js_class (GlobalContext context) {
    var theClass = new Class (js_class);
    var theObject = new JSCore.Object (context, theClass,
    context);
    var theGlobal = context.get_global_object ();
    var id = new String.with_utf8_c_string ("HelloJSCore");
    theGlobal.set_property (context, id, theObject,
    PropertyAttribute.None, null);
    }
    static int main (string[] args)
    {
    Gtk.init (ref args);
    var webView = new Main ();
    var window = new Gtk.Window();
    window.add(webView);
    window.show_all ();
    Gtk.main ();
    return 0;
    }
    }

  6. Copy the accompanied webkit-1.0.vapi and javascriptcore.vapi files into the src folder. The javascriptcore.vapi file is needed because some distributions do not have this .vapi file in their repositories.

  7. Run the application. The following output will be displayed:

What just happened?

The first thing we do is include the WebKit and JavaScriptCore namespaces. Note, in the following code snippet, that the JavaScriptCore namespace is abbreviated as JSCore:

using WebKit;
using JSCore;

In the Main function, we load HTML content into the WebView widget. We display a level 1 heading and then call the alert function. The alert function displays a string returned by the hello function inside the HelloJSCore class, as shown in the following code:

public Main ()
{
load_html_string("<h1>Hello</h1>" +
"<script>alert(HelloJSCore.hello())</script>","/");

In the preceding code snippet, we can see that the client-side JavaScript code is as follows:

alert(HelloJSCore.hello())

And we can also see that we call the hello function from the HelloJSCore class as a static function. It means that we don’t instantiate the HelloJSCore object before calling the hello function.

In WebView, we initialize the class defined in the Vala class when we get the window_object_cleared signal. This signal is emitted whenever a page is cleared. The initialization is done in setup_js_class and this is also where we pass the JSCore global context into. The global context is where JSCore keeps the global variables and functions. It is accessible by every code.

window_object_cleared.connect ((frame, context) => {
setup_js_class ((JSCore.GlobalContext)
context);
});

The following snippet of code contains the function, which we want to expose to the clientside JavaScript. The function just returns a Hello from JSCore string message:

public static JSCore.Value helloFromVala (Context ctx,
JSCore.Object function,
JSCore.Object thisObject,
JSCore.Value[] arguments,
out JSCore.Value exception) {
exception = null;
var text = new String.with_utf8_c_string ("Hello from JSCore");
return new JSCore.Value.string (ctx, text);
}

Then we need to put a boilerplate code that is needed to expose the function and other members of the class. The first part of the code is the static function index. This is the mapping between the exposed function and the name of the function defined in the wrapper. In the following example, we map the hello function, which can be used in the client side, with the helloFromVala function defined in the code. The index is then ended with null to mark the end of the array:

static const JSCore.StaticFunction[] js_funcs = {
{ "hello", helloFromVala, PropertyAttribute.ReadOnly },
{ null, null, 0 }
};

The next part of the code is the class definition. It is about the structure that we have to fill, so that JSCore would know about the class. All of the fields are filled with null, except for those we want to make use of. In this example, we use the static function for the hello function. So we fill the static function field with js_funcs, which we defined in the preceding code snippet:

static const ClassDefinition js_class = {
0, // version
ClassAttribute.None, // attribute
"HelloJSCore", // className
null, // parentClass
null, // static values
js_funcs, // static functions
null, // initialize
null, // finalize
null, // hasProperty
null, // getProperty
null, // setProperty
null, // deleteProperty
null, // getPropertyNames
null, // callAsFunction
null, // callAsConstructor
null, // hasInstance
null // convertToType
};

After that, in the the setup_js_class function, we set up the class to be made available in the JSCore global context. First, we create JSCore.Class with the class definition structure we filled previously. Then, we create an object of the class, which is created in the global context. Last but not least, we assign the object with a string identifier, which is HelloJSCore. After executing the following code, we will be able to refer HelloJSCore on the client side:

void setup_js_class (GlobalContext context) {
var theClass = new Class (js_class);
var theObject = new JSCore.Object (context, theClass,
context);
var theGlobal = context.get_global_object ();
var id = new String.with_utf8_c_string ("HelloJSCore");
theGlobal.set_property (context, id, theObject,
PropertyAttribute.None, null);
}

LEAVE A REPLY

Please enter your comment!
Please enter your name here