Canonical Voices

What Blogging in the Wind talks about

Posts tagged with 'programming'

rvr

In the first post we described what WebKit and JavaScriptCore is, how to program a simple WebKitGTK+ application and how to extend the JavaScript functionality with a dumb (empty) class. In the second post, we extended JavaScript to enable desktop notifications. In the third post,  WebKit/JavaScriptCore were extended to access UPower properties using D-Bus. And in the fourth post, WebKit/JavaScriptCore were extended to use JavaScript callbacks for UPower methods.

In this article we are introducing another way to extend JavaScriptCore: Seed.

 

Seed: A JavaScriptCore engine

In previous articles we've seen the power of JavaScriptCore. However, its features are tied to the WebKit engine. Wouldn't be great if we could run standalone JavaScript programs using JavaScriptCore? That's exactly what Seed does. Seed is a JavaScript interpreter, able to run pure JavaScript programs. It uses WebKitGTK+, not to browse, but to run JavaScript code.

Indeed, Seed comes in two flavors:

  • As a command line interpreter (/usr/bin/seed).
  • As a C library (libseed).

As a command line, Seed is like a Python or Perl interpreter: a script is loaded and executed. However, outside the web environment, JavaScript is pretty much useless. That's why, as we did in our previous examples, Seed adds support for some native libraries. And specifically, Seed can be used to program GTK+ applications using JavaScript, thanks to its GObject Introspection/JavaScript bridge.

libseed, the C library, allows third-parties to easily build scripting capabilities for their programs. It adds a thin layer to JavaScriptCore API. The functions and methods available in JSC are usually available in libseed. This mean we can define new features and access JavaScript objects from C.

 

A simple Seed script

To install Seed in Ubuntu, type:

$ sudo apt-get install seed libseed-gtk3-dev

This is a simple GTK+ program, which just shows a window with a button.

#!/usr/bin/seed

/* Imports GTK+ libraries */
Gtk = imports.gi.Gtk;

/* Inits GTK+ */
Gtk.init(null, null);

/* Creates window */
var window = new Gtk.Window();

/* Terminates program if window is closed */
window.signal.hide.connect(Gtk.main_quit);

/* Creates button */
var button = new Gtk.Button();
button.set_label('Hello world');

/* Terminates program if button is clicked */
button.signal.clicked.connect(Gtk.main_quit);

/* Add button to window */
window.add(button);

/* Shows */
window.set_default_size(100,100);
window.show_all();

/* Main loop */
Gtk.main();

To run it:

$ seed gtk.js

This shows:

Pantallazo del 2011-11-14 01:48:08

WebViews and Seed

In our previous articles, we've extended JavaScript features manually, programming C methods to create JS classes. Those classes were available to our Web Views. But Seed already provides a lot of libraries. In the next example, we'll see how to use libseed to extend WebKit.

Seed is built upon JavaScriptCore, which is a core element of WebKit. In order to expose libseed features in a browser, we need to connect Seed with WebKit. How to do that? When a WebView is created, a JavaScriptCore's Context Object is created. This Context Object stores the status of the JavaScript engine. Generally, Seed creates is own Context Object, but there is function to use an already created context. Using this function, Seed will populate the WebView's context with Seed's libraries.

This the source code of our program which exposes Seed libraries to a WebKitGTK+ web view.

#include <stdio.h>
#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <seed.h>
#include <JavaScriptCore/JavaScript.h>

SeedEngine *engine;

static void window_object_cleared_cb(WebKitWebView  *web_view,
                                WebKitWebFrame *frame,
                                gpointer        context,
                                gpointer        arg3,
                                gpointer        user_data)

{
    JSGlobalContextRef jsContext = webkit_web_frame_get_global_context(frame);
    engine = seed_init_with_context (NULL, NULL, jsContext);
}

static void destroy_cb(GtkWidget* widget, gpointer data)
{
    g_free (engine);
    gtk_main_quit ();
}

static GtkWidget* main_window;
static WebKitWebView* web_view;

static GtkWidget* create_browser()
{
    GtkWidget* scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    web_view = WEBKIT_WEB_VIEW (webkit_web_view_new ());
    gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (web_view));

    g_signal_connect (G_OBJECT (web_view), "window-object-cleared", G_CALLBACK(window_object_cleared_cb), web_view);

    return scrolled_window;
}

static GtkWidget* create_window()
{
    GtkWidget* window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size (GTK_WINDOW (window), 500, 500);
    g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy_cb), NULL);
    return window;
}

int main (int argc, char* argv[]) {
    gtk_init (&argc, &argv);
    if (!g_thread_supported())
       g_thread_init (NULL);

    GtkWidget* vbox = gtk_vbox_new(FALSE, 0);
    gtk_box_pack_start(GTK_BOX(vbox), create_browser (), TRUE, TRUE, 0);

    main_window = create_window();
    gtk_container_add(GTK_CONTAINER (main_window), vbox);

    gchar* uri = (gchar*) "file://webkit-seed.html";
    webkit_web_view_load_uri(web_view, uri);

    gtk_widget_grab_focus (GTK_WIDGET (web_view));
    gtk_widget_show_all (main_window);
    gtk_main ();

    return 0;
}

To compile it, type:

$ gcc -o webkit-seed webkit-seed.c `pkg-config --cflags --libs webkitgtk-3.0 seed`

And this is the webkit-seed.html file:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

    <script type="text/javascript">
    var sqlite = imports.sqlite;
    sql = new sqlite.Database('/tmp/webkit_seed_test.db')
    sql.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)');
    sql.close();
    </script>

</body>
</html>

To run the program:

$ webkit-seed

This time, the browser will appear, but the fun action will be in the /tmp directory. There we'll found a new SQLite 3 database, with a two-field table.

Of course, Seed gives access to a lot of sensible libraries. Safety would be at risk if we let any JavaScript program to run this way, but it also opens a whole world of possibilities!

P.S.: Of course, I tried to run the GTK+ example inside the WebView. Unfortunately, it didn't work :-(

Read more
rvr

In the previous articles dedicated to WebKit, we've used the WebKitGTK+ in plain C. Thanks to GObject and GLib libraries,  doing bindings for others languages is quite easy. And of course, bindings for WebKitGTK+ exists for Python, one of my favorite programming languages. Python is ideal for rapid prototyping and/or fast development.

To use WebKitGTK+ in Python and Ubuntu, python-webkit library must be installed:

$ sudo apt-get install python-webkit

This is the source code of a simple WebKit browser in PyGTK+.

#!/usr/bin/env python
import pygtk
import gtk
import webkit

# Creates the GTK+ app
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.scrolled_window = gtk.ScrolledWindow()
# Creates a WebKit view
self.web_view = webkit.WebView()
self.scrolled_window.add(self.web_view)
self.window.add(self.scrolled_window)
# Sets the window size
self.window.set_default_size(1024, 800)
# Loads this URL
self.web_view.load_uri('http://rvr.typepad.com/')
gtk.main()

Using Python we get rid of the manual memory management. Now, let's use WebKit and Python to build a tool.

Facebook and desktop applications: OAuth 2.0.

Facebook has a programmable API, so application can interact with user data. Probably you know Farmville, the popular Facebook game. Farmville uses Facebook API to access to our data. For applications to be able to interact with our data, developers must register the application in Facebook and the each user grants permission access to the app.

This protocol to obtain user access is called OAuth 2.0, and is has become a popular standard among many popular web sites. The workflow is:

  1. The application launchs a browser and load FB's OAuth dialog, passing as arguments the app key and a redirection URL: https://www.facebook.com/dialog/oauth?client_id=YOUR_APP_ID&redirect_uri=...
  2. The user authenticates himself in Facebook.
  3. The user accepts granting access to the application.
  4. Facebook returns an access token to a callback URL.

The problem with this workflow is that desktop applications don't usually run a web server, so it's difficult to get this access token. How can we do it? Using WebKit!

Facebook states that the callback URL can be set to https://www.facebook.com/connect/login_success.html which will receive the access token as a parameter (login_sucess.html#access_token=...). We are going to use WebKit to identify this URLs using this callback:

self.web_view.connect('load-committed', self._load_committed_cb) # Load page

The 'load-commited' signal is emmited by the WebKit view when a page is about to be loaded. In the _load_commited_cb function, the page URL is checked to get Facebook's application access token. This token is finally saved to a file and the browser is closed.

This is the source code:

#!/usr/bin/env python
import pygtk
import gtk
import webkit
import urllib
import urlparse

FB_TOKEN_FILE = 'access_token.txt'

class Browser:
    """ Creates a web browser using GTK+ and WebKit to authorize a
        desktop application in Facebook. It uses OAuth 2.0.
        Requires the Facebook's Application ID. The token is then
        saved to FB_TOKEN_FILE.
    """

    def __init__(self, app_key, scope='offline_access'):
        """ Constructor. Creates the GTK+ app and adds the WebKit widget
            @param app_key Application key ID (Public).

            @param scope A string list of permissions to ask for. More at
            http://developers.facebook.com/docs/reference/api/permissions/
        """
        self.token = ''
        self.token_expire = ''
        self.scope = scope
        # Creates the GTK+ app
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.scrolled_window = gtk.ScrolledWindow()
        # Creates a WebKit view
        self.web_view = webkit.WebView()
        self.scrolled_window.add(self.web_view)
        self.window.add(self.scrolled_window)
        # Connects events
        self.window.connect('destroy', self._destroy_event_cb) # Close window
        self.web_view.connect('load-committed', self._load_committed_cb) # Load page
        self.window.set_default_size(1024, 800)
        # Loads the Facebook OAuth page
        self.web_view.load_uri(
            'https://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=%s&response_type=token&scope=%s' % (urllib.quote(app_key), urllib.quote('https://www.facebook.com/connect/login_success.html'), urllib.quote(self.scope))
            )

    def _load_committed_cb(self, web_view, frame):
        """ Callback. The page is about to be loaded. This event is captured
            to intercept the OAuth 2.0 redirection, which includes the
            access token.

            @param web_view A reference to the current WebKitWebView.

            @param frame A reference to the main WebKitWebFrame.
        """
        # Gets the current URL to check whether is the one of the redirection
        uri = frame.get_uri()
        parse = urlparse.urlparse(uri)
        if (hasattr(parse, 'netloc') and hasattr(parse, 'path') and
            hasattr(parse, 'fragment') and parse.netloc == 'www.facebook.com' and
            parse.path == '/connect/login_success.html' and parse.fragment):
            # Get token from URL
            params = urlparse.parse_qs(parse.fragment)
            self.token = params['access_token'][0]
            self.token_expire = params['expires_in'][0] # Should be equal to 0, don't expire
            # Save token to file
            token_file = open(FB_TOKEN_FILE, 'w')
            token_file.write(self.token)
            token_file.close()
            print "Authentication done. Access token available at %s" % (FB_TOKEN_FILE)
            gtk.main_quit() # Finish

    def _destroy_event_cb(self, widget):
        """ Callback for close window. Closes the application. """
        return gtk.main_quit()

    def authorize(self):
        """ Runs the app. """
        self.window.show_all()
        gtk.main()

if (__name__ == '__main__'):
    # Creates the browser
    browser = Browser(app_key='XXXXXXXXXXX', scope='offline_access,read_stream')
    # Launch browser window
    browser.authorize()
    # Token available?
    print "Token: %s" % (browser.token)

Easy, isn't it?

The complete source code is available at github.com/vrruiz/FacebookAuthBrowser

Read more
rvr

In the first post we described what WebKit and JavaScriptCore is, how to program a simple WebKitGTK+ application and how to extend the JavaScript functionality with a dumb (empty) class. In the second post, we extended JavaScript to enable desktop notifications. In the third post,  WebKit/JavaScriptCore were extended to access UPower properties using D-Bus.

 

Callbacks

As explained in the previous article, D-Bus is object oriented, and interfaces can expose objects with their methods, properties and events. We used D-Bus to access battery status, and extended WebKit/JavaScript Core to read it. In this post we are going to connect D-Bus signals with JavaScript callback functions. What we want to do is execute custom JavaScript code after a D-Bus signal has been emitted (and received).

D-Bus signals are events that clients can subscribe to. As explained, UDisks interface emits signals everytime a new external drive is attached to the system, and Linux desktops capture this events to launch file managers (and other applications).  UPower.Device also emits signals, and we are going to capture Changed().

It's easy to connect functions using  D-Bus Glib bindings. First, we add a signal to the proxy object, which is a  way to indicate the type of the arguments. Changed() doesn't has any parameter, so the call is this:

    dbus_g_proxy_add_signal (proxy, "Changed", G_TYPE_INVALID);

And then we connect the D-Bus signal with a callback function (battery_changed_cb).

    dbus_g_proxy_connect_signal(proxy,
                                "Changed",
                                G_CALLBACK(battery_changed_cb),
                                ref,
                                (GClosureNotify) g_free);

This callback function will receive two parameters, proxy and ref, with references to its correspondent JavaScriptCore context and global object.

So, when UPower updates the battery status, it emits a Changed() signal. This signal will be received by our JavaScript Battery() objects. What happens then? battery_changed_cb will check whether Battery has a property called onChange and whether is of type function. So, if Battery.onChange stores a function, battery_changed_cb will call it. You'll see it more clearly in the JavaScript file.

Above is the source code of our previous D-Bus program. Remember that is based on that of the previous example.

#include <stdlib.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib.h>
#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <JavaScriptCore/JavaScript.h>

DBusGConnection *conn;
DBusGProxy *proxy;
DBusGProxy *properties_proxy;

typedef struct {
    JSContextRef context;
    JSObjectRef object;
} RefContextObject;

/* Callback for UPower.Device.Changed() */
static void battery_changed_cb(DBusGProxy *_proxy, RefContextObject *ref) 
{
    g_message("Battery changed");
    
    /* Get onChange property */
    JSStringRef string_onchange;
    string_onchange = JSStringCreateWithUTF8CString("onChange");
    JSValueRef func = JSObjectGetProperty(ref->context, ref->object, string_onchange, NULL);
    JSObjectRef function = JSValueToObject(ref->context, func, NULL);
    JSStringRelease(string_onchange);
    
    if (!JSObjectIsFunction(ref->context, function)) {
         g_message("JSObject is not function or is not set");
         return;
    }
        
    JSValueRef result = JSObjectCallAsFunction(ref->context, // The execution context to use
                                               function, // The JSObject to call as a function.
                                               ref->object, // The object to use as "this," or NULL to use the global object as "this."
                                               0, //  An integer count of the number of arguments in arguments.
                                               NULL, // A JSValue array of arguments to pass to the function. Pass NULL if argumentCount is 0.
                                               NULL); // A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception.
        
}

/* Class initialize */
static void battery_init_cb(JSContextRef ctx,
                            JSObjectRef object)
{
    GError *error = NULL;
    
    conn = dbus_g_bus_get (DBUS_BUS_SYSTEM, &error);

    if (conn == NULL)
    {
        g_printerr ("Failed to open connection to bus: %s\n", error->message);
        g_error_free (error);
        return;
    }

    /* Create a proxy object for "org.freedesktop.UPower" */
    proxy = dbus_g_proxy_new_for_name (conn,
                                       "org.freedesktop.UPower",
                                       "/org/freedesktop/UPower/devices/battery_BAT0",
                                       "org.freedesktop.UPower.Device");
    if (proxy == NULL)
    {
        g_printerr ("Failed to create proxy object\n");
        return;
    }
    
    /* Create a proxy object for "org.freedesktop.UPower.Properties" */
    properties_proxy = dbus_g_proxy_new_from_proxy (proxy,
                                                    "org.freedesktop.DBus.Properties",
                                                    dbus_g_proxy_get_path (proxy));
    if (properties_proxy == NULL)
    {
        g_printerr ("Failed to create proxy object\n");
        return;
    }

    /* Connect callback to signal org.freedestop.UPower.Device.Changed() */
    RefContextObject *ref = g_new(RefContextObject, 1);
    ref->context = ctx;
    ref->object = object;
    
    dbus_g_proxy_add_signal (proxy, "Changed", G_TYPE_INVALID);
    dbus_g_proxy_connect_signal(proxy,
                                "Changed",
                                G_CALLBACK(battery_changed_cb),
                                ref,
                                (GClosureNotify) g_free);
  
    error = NULL;
}

/* Class constructor */
static JSObjectRef battery_constructor_cb(JSContextRef context,
                                          JSObjectRef constructor,
                                          size_t argumentCount,
                                          const JSValueRef arguments[],
                                          JSValueRef *exception)
{
    return constructor;
}                                       

/* Class finalize */
static void battery_destroy_cb(JSObjectRef object)
{
    /* Ends Battery */
    if (proxy != NULL) g_object_unref (proxy);
    if (properties_proxy != NULL) g_object_unref (properties_proxy);
}

static gboolean proxy_property_value(char *property,
				                     GValue *get_value,
                                     GError **error)
{
    /* Call ListNames method, wait for reply */
    return dbus_g_proxy_call (properties_proxy, "Get", error,
                              G_TYPE_STRING, "/org/freedesktop/UPower/devices/battery_BAT0",
                              G_TYPE_STRING, property,
                              G_TYPE_INVALID,
                              G_TYPE_VALUE, get_value,
                              G_TYPE_INVALID);
}

static JSValueRef proxy_double_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Call method, wait for reply */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        gdouble value = g_value_get_double(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeNumber(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

static JSValueRef proxy_uint64_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Call method, wait for reply */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        guint64 value = g_value_get_uint64(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeNumber(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

static JSValueRef proxy_boolean_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Call method, wait for reply */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        gboolean value = g_value_get_boolean(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeBoolean(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

/* Battery.capacity method callback implementation */
static JSValueRef battery_capacity_cb(JSContextRef context,
                                      JSObjectRef function,
                                      JSObjectRef thisObject,
                                      size_t argumentCount,
                                      const JSValueRef arguments[],
                                      JSValueRef *exception)
{
    return proxy_double_value(context, "Capacity", argumentCount);
}

/* Battery.percentage method callback implementation */
static JSValueRef battery_percentage_cb(JSContextRef context,
                                        JSObjectRef function,
                                        JSObjectRef thisObject,
                                        size_t argumentCount,
                                        const JSValueRef arguments[],
                                        JSValueRef *exception)
{
    return proxy_double_value(context, "Percentage", argumentCount);
}

/* Battery.voltage method callback implementation */
static JSValueRef battery_voltage_cb(JSContextRef context,
                                     JSObjectRef function,
                                     JSObjectRef thisObject,
                                     size_t argumentCount,
                                     const JSValueRef arguments[],
                                     JSValueRef *exception)
{
    return proxy_double_value(context, "Voltage", argumentCount);
}

/* Battery.updateTime method callback implementation */
static JSValueRef battery_update_time_cb(JSContextRef context,
                                         JSObjectRef function,
                                         JSObjectRef thisObject,
                                         size_t argumentCount,
                                         const JSValueRef arguments[],
                                         JSValueRef *exception)
{
    return proxy_uint64_value(context, "UpdateTime", argumentCount);
}

/* Battery.PowerSupply method callback implementation */
static JSValueRef battery_power_supply_cb(JSContextRef context,
                                          JSObjectRef function,
                                          JSObjectRef thisObject,
                                          size_t argumentCount,
                                          const JSValueRef arguments[],
                                          JSValueRef *exception)
{
    return proxy_boolean_value(context, "PowerSupply", argumentCount);
}

/* Class method declarations */
static const JSStaticFunction battery_staticfuncs[] =
{
    { "capacity", battery_capacity_cb, kJSPropertyAttributeReadOnly },
    { "percentage", battery_percentage_cb, kJSPropertyAttributeReadOnly },
    { "voltage", battery_voltage_cb, kJSPropertyAttributeReadOnly },
    { "updateTime", battery_update_time_cb, kJSPropertyAttributeReadOnly },
    { "powerSupply", battery_power_supply_cb, kJSPropertyAttributeReadOnly },
    { NULL, NULL, 0 }
};

static const JSClassDefinition notification_def =
{
    0,                     // version
    kJSClassAttributeNone, // attributes
    "Battery",             // className
    NULL,                  // parentClass
    NULL,                  // staticValues
    battery_staticfuncs,   // staticFunctions
    battery_init_cb,       // initialize
    battery_destroy_cb,    // finalize
    NULL,                  // hasProperty
    NULL,                  // getProperty
    NULL,                  // setProperty
    NULL,                  // deleteProperty
    NULL,                  // getPropertyNames
    NULL,                  // callAsFunction
    battery_constructor_cb, // callAsConstructor
    NULL,                  // hasInstance  
    NULL                   // convertToType
};

/* Callback - JavaScript window object has been cleared */
static void window_object_cleared_cb(WebKitWebView  *web_view,
                                     WebKitWebFrame *frame,
                                     gpointer        context,
                                     gpointer        window_object,
                                     gpointer        user_data)

{
    /* Add classes to JavaScriptCore */
    JSClassRef classDef = JSClassCreate(&notification_def);
    JSObjectRef classObj = JSObjectMake(context, classDef, context);
    JSObjectRef globalObj = JSContextGetGlobalObject(context);
    JSStringRef str = JSStringCreateWithUTF8CString("Battery");
    JSObjectSetProperty(context, globalObj, str, classObj, kJSPropertyAttributeNone, NULL);
}

/* Destroy callback */
static void destroy(GtkWidget *widget,
                    gpointer   data )
{
    gtk_main_quit();
}

int
main (int argc, char* argv[])
{
    /* Initialize the widget set */
    gtk_init (&argc, &argv);
    
    /* Create the window widgets */
    GtkWidget *main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    
    /* Create the WebKit Web View widget */
    GtkWidget *web_view = webkit_web_view_new ();
    
    /* Connect the window object cleared event with callback */
    g_signal_connect (G_OBJECT (web_view), "window-object-cleared", G_CALLBACK(window_object_cleared_cb), web_view);


    /* Place the WebKitWebView in the GtkScrolledWindow */
    gtk_container_add (GTK_CONTAINER (scrolled_window), web_view);
    gtk_container_add (GTK_CONTAINER (main_window), scrolled_window);

    /* Connect the destroy window event with destroy function */
    g_signal_connect (G_OBJECT (main_window), "destroy", G_CALLBACK (destroy), NULL);
    
    /* Open webpage */
    webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), "file://webkit-05.html");

    /* Create the main window */
    gtk_window_set_default_size (GTK_WINDOW (main_window), 800, 600);
    
    /* Show the application window */
    gtk_widget_show_all (main_window);

    /* Enter the main event loop, and wait for user interaction */
    gtk_main ();
    
    /* The user lost interest */
    return 0;
}

To compile it on Ubuntu:

$ gcc webkit-05.c -o webkit.c `pkg-config --cflags --libs webkitgtk-3.0 dbus-glib-1`

Above, the corresponding HTML file webkit-05.html.

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
  <h1>Extending JavaScript with WebKit. Battery class.</h1>
  <div id="battery-status"></div>
  <script type="text/javascript">
    var display = function () {
        var div = document.getElementById("battery-status");
        var battery_status = "Capacity: " + Battery.capacity() + "<br>";
        battery_status += "Percentage: " + Battery.percentage() + "%<br>";
        battery_status += "Voltage: " + Battery.voltage() + "<br>";
        battery_status += "Update Time: " + Battery.updateTime() + "<br>";
        battery_status += "Power supply: " + Battery.powerSupply() + "<br>";
        div.innerHTML = battery_status;
    }
  
    var battery = new Battery();
    battery.onChange = display;
    display();
  </script>
</body>
</html>

var display stores a reference to a function. This function fills the empty <div id=battery-status> with the actual battery status. Finally, a Battery() object is created and battery.onChange is defined.

If we run our program

$ ./webkit-05

when UPower.Device.Changed() is emitted, our JavaScript function will be called and the battery status, automatically updated in the web page.

Pantallazo del 2011-11-02 19:15:45
As before, the complete source code is available at github.com/vrruiz/WebKit-JavaScriptCore-Extensions.

Read more
rvr

In the first post we described what WebKit and JavaScriptCore is, how to program a simple WebKitGTK+ application and how to extend the JavaScript functionality with a dumb (empty) class. In the second post, we extended JavaScript to enable desktop notifications.

In this third tutorial we are going to extend WebKit/JavaScriptCore to enable access to system information, specifically, battery status. We'll do this using D-Bus.

 

What's D-Bus?

Today's desktop environments, like GNOME and KDE, quickly respond to USB events: when an external drive is connected, a file navigator appears. How does it work?

GNOME and KDE provide a standard system for interprocess communication, called D-Bus. D-Bus communications is object oriented: applications expose objects with their methods, properties and events, and they are remotely available.

There are GUI applications to play with D-Bus. One of them is D-Feet. To install it on Ubuntu, type:

$ sudo apt-get install d-feet

In D-Feet we can connect to two default buses: the session bus and the system bus. Regular desktop applications use the session bus, tied to the user running the current desktop session. The system bus has a lower level and used by services in the operating system. This screenshot shows D-Feet connected to the session bus:

Pantallazo del 2011-10-27 20:58:36

And finally the enigma is solved: if the desktop wants to know when a new disk is attached to the computer, it just can do it using D-Bus and UDisks interface: actually, the operating system uses the system bus to communicate disk events.


D-Bus and battery status: UPower.

In the screenshot above, on the right we can see the interfaces, methods and properties exposed by UPower in the system bus.  For example, the list of power devices (batteries and power line) is available using the EnumerateDevices() method, which returns the device list as and array of objects. Another interface, UPower.Device, gives us access to the battery status, and we'll use it to build our custom JavaScript class.

In order to connect to D-Bus, we use the Glib's D-Bus bindings. To install the development libraries, type:

$ sudo apt-get install libdbus-glib-1-dev

The actual code is this:

#include <stdlib.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib.h>
#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <JavaScriptCore/JavaScript.h>

DBusGConnection *conn;
DBusGProxy *proxy;
DBusGProxy *properties_proxy;

/* Class initialize */
static void battery_init_cb(JSContextRef ctx,
                            JSObjectRef object)
{
    GError *error = NULL;
    
    /* Connection to the system bus */ 
    conn = dbus_g_bus_get (DBUS_BUS_SYSTEM, &error);

    if (conn == NULL)
    {
        g_printerr ("Failed to open connection to bus: %s\n", error->message);
        g_error_free (error);
        return;
    }

    /* Create a proxy object for "org.freedesktop.UPower" */
    proxy = dbus_g_proxy_new_for_name (conn,
                                       "org.freedesktop.UPower",
                                       "/org/freedesktop/UPower/devices/battery_BAT0",
                                       "org.freedesktop.UPower.Device.Properties");
    if (proxy == NULL)
    {
        g_printerr ("Failed to create proxy object\n");
        return;
    }

    /* Creates a proxy using an existing proxy as a template */
    properties_proxy = dbus_g_proxy_new_from_proxy (proxy,
                                                    "org.freedesktop.DBus.Properties",
                                                    dbus_g_proxy_get_path (proxy));
    if (properties_proxy == NULL)
    {
        g_object_unref (proxy)
        g_printerr ("Failed to create proxy object\n");
        return;
    }
  
    error = NULL;
}

/* Class finalize */
static void battery_destroy_cb(JSObjectRef object)
{
    /* Ends Battery. Free allocated memory. */
    if (proxy != NULL) g_object_unref (proxy);
    if (properties_proxy != NULL) g_object_unref (properties_proxy);
}

static gboolean proxy_property_value(char *property,
				                     GValue *get_value,
                                     GError **error)
{
    /* Call Get method, wait for reply */
    return dbus_g_proxy_call (properties_proxy, "Get", error,
                              G_TYPE_STRING, "/org/freedesktop/UPower/devices/battery_BAT0",
                              G_TYPE_STRING, property,
                              G_TYPE_INVALID,
                              G_TYPE_VALUE, get_value,
                              G_TYPE_INVALID);
}

static JSValueRef proxy_double_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    /* Calls to UPower to get a double value */
    
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Get property value */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        /* Convert value to double */
        gdouble value = g_value_get_double(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeNumber(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

static JSValueRef proxy_uint64_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    /* Calls to UPower to get a uint64 value */
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Get property value */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        /* Convert value to uint64 */
        guint64 value = g_value_get_uint64(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeNumber(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

static JSValueRef proxy_boolean_value(JSContextRef context,
                                     char *property,
                                     size_t argumentCount)
{
    /* Calls to UPower to get a boolean value */
    
    GError *error = NULL;
    GValue get_value = {0, };
  
    if (argumentCount == 0) {
        /* Call method, wait for reply */
        if (!proxy_property_value(property, &get_value, &error))
        {
            g_printerr ("Error: %s\n", error->message);
            g_error_free (error);
            return JSValueMakeUndefined(context);
        }
        
        /* Convert value to boolean */
        gboolean value = g_value_get_boolean(&get_value);
        g_value_unset(&get_value);
        return JSValueMakeBoolean(context, value);
    }
    
    return JSValueMakeUndefined(context);
}

/* Battery.capacity method callback implementation */
static JSValueRef battery_capacity_cb(JSContextRef context,
                                      JSObjectRef function,
                                      JSObjectRef thisObject,
                                      size_t argumentCount,
                                      const JSValueRef arguments[],
                                      JSValueRef *exception)
{
    /* Get battery capacity status */
    return proxy_double_value(context, "Capacity", argumentCount);
}

/* Battery.percentage method callback implementation */
static JSValueRef battery_percentage_cb(JSContextRef context,
                                        JSObjectRef function,
                                        JSObjectRef thisObject,
                                        size_t argumentCount,
                                        const JSValueRef arguments[],
                                        JSValueRef *exception)
{
    /* Get battery percentage status */
    return proxy_double_value(context, "Percentage", argumentCount);
}

/* Battery.voltage method callback implementation */
static JSValueRef battery_voltage_cb(JSContextRef context,
                                     JSObjectRef function,
                                     JSObjectRef thisObject,
                                     size_t argumentCount,
                                     const JSValueRef arguments[],
                                     JSValueRef *exception)
{
    /* Get battery voltage status */
    return proxy_double_value(context, "Voltage", argumentCount);
}

/* Battery.updateTime method callback implementation */
static JSValueRef battery_update_time_cb(JSContextRef context,
                                         JSObjectRef function,
                                         JSObjectRef thisObject,
                                         size_t argumentCount,
                                         const JSValueRef arguments[],
                                         JSValueRef *exception)
{
    /* Get battery update time */
    return proxy_uint64_value(context, "UpdateTime", argumentCount);
}

/* Battery.PowerSupply method callback implementation */
static JSValueRef battery_power_supply_cb(JSContextRef context,
                                          JSObjectRef function,
                                          JSObjectRef thisObject,
                                          size_t argumentCount,
                                          const JSValueRef arguments[],
                                          JSValueRef *exception)
{
    /* Get battery power supply */
    return proxy_boolean_value(context, "PowerSupply", argumentCount);
}

/* Class method declarations */
static const JSStaticFunction battery_staticfuncs[] =
{
    { "capacity", battery_capacity_cb, kJSPropertyAttributeReadOnly },
    { "percentage", battery_percentage_cb, kJSPropertyAttributeReadOnly },
    { "voltage", battery_voltage_cb, kJSPropertyAttributeReadOnly },
    { "updateTime", battery_update_time_cb, kJSPropertyAttributeReadOnly },
    { "powerSupply", battery_power_supply_cb, kJSPropertyAttributeReadOnly },
    { NULL, NULL, 0 }
};

static const JSClassDefinition notification_def =
{
    0,                     // version
    kJSClassAttributeNone, // attributes
    "Battery",             // className
    NULL,                  // parentClass
    NULL,                  // staticValues
    battery_staticfuncs,   // staticFunctions
    battery_init_cb,       // initialize
    battery_destroy_cb,    // finalize
    NULL,                  // hasProperty
    NULL,                  // getProperty
    NULL,                  // setProperty
    NULL,                  // deleteProperty
    NULL,                  // getPropertyNames
    NULL,                  // callAsFunction
    NULL,                  // callAsConstructor
    NULL,                  // hasInstance  
    NULL                   // convertToType
};

/* Callback - JavaScript window object has been cleared */
static void window_object_cleared_cb(WebKitWebView  *web_view,
                                     WebKitWebFrame *frame,
                                     gpointer        context,
                                     gpointer        window_object,
                                     gpointer        user_data)

{
    /* Add classes to JavaScriptCore */
    JSClassRef classDef = JSClassCreate(&notification_def);
    JSObjectRef classObj = JSObjectMake(context, classDef, context);
    JSObjectRef globalObj = JSContextGetGlobalObject(context);
    JSStringRef str = JSStringCreateWithUTF8CString("Battery");
    JSObjectSetProperty(context, globalObj, str, classObj, kJSPropertyAttributeNone, NULL);
}

/* Destroy callback */
static void destroy(GtkWidget *widget,
                    gpointer   data )
{
    gtk_main_quit();
}

int
main (int argc, char* argv[])
{
    /* Initialize the widget set */
    gtk_init (&argc, &argv);
    
    /* Create the window widgets */
    GtkWidget *main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    
    /* Create the WebKit Web View widget */
    GtkWidget *web_view = webkit_web_view_new ();
    
    /* Connect the window object cleared event with callback */
    g_signal_connect (G_OBJECT (web_view), "window-object-cleared", G_CALLBACK(window_object_cleared_cb), web_view);


    /* Place the WebKitWebView in the GtkScrolledWindow */
    gtk_container_add (GTK_CONTAINER (scrolled_window), web_view);
    gtk_container_add (GTK_CONTAINER (main_window), scrolled_window);

    /* Connect the destroy window event with destroy function */
    g_signal_connect (G_OBJECT (main_window), "destroy", G_CALLBACK (destroy), NULL);
    
    /* Open webpage */
    webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), "file://webkit-04.html");

    /* Create the main window */
    gtk_window_set_default_size (GTK_WINDOW (main_window), 800, 600);
    
    /* Show the application window */
    gtk_widget_show_all (main_window);

    /* Enter the main event loop, and wait for user interaction */
    gtk_main ();
    
    /* The user lost interest */
    return 0;
}

This program creates a new JavaScript class, called Battery, with some useful methods, like Battery.percentage(), Battery.capacity() and Battery.powerSupply(). When the class is initiated, a system bus connection is created, which is used later by the methods to obtain actual battery values.

This is webkit-04.html, a simple web file which uses the newly created Battery class:

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
  <h1>Extending JavaScript with WebKit. Battery class.</h1>
  <script type="text/javascript">
    document.write("Capacity: " + Battery.capacity() + "<br>");
    document.write("Percentage: " + Battery.percentage() + "%<br>");
    document.write("Voltage: " + Battery.voltage() + "<br>");
    document.write("Update Time: " + Battery.updateTime() + "<br>");
    document.write("Power supply: " + Battery.powerSupply() + "<br>");
  </script>
</body>
</html>

To compile and run the program, type:

$ gcc -o webkit-04 webkit-04.c `pkg-config --cflags --libs webkitgtk-3.0 dbus-glib-1`

$ ./webkit-04

The program, if running on a laptop with batteries, will display something like this:

Pantallazo del 2011-10-27 21:28:55

Great, isn't it!? :-)

The source code is available at github.com/vrruiz/WebKit-JavaScriptCore-Extensions.

As we've seen, D-Bus is a powerful system. Mixing JavaScript with D-Bus gives us incredible possibilities. See you in the next post!

Read more
rvr

In recent years, JavaScript has become a very popular language. Google has promoted rich web applications which compete with desktop programs: Gmail, Google Maps and Google Docs are rivals of Outlook, Google Earth and Office. Today HTTP, HTML, CSS and JavaScript are key technologies in which companies are heavily investing so they can develop even more powerful web applications,  i.e. HTML5 features enable off-line web applications (i.e. store local data).

JavaScript has a restricted programming model in order to meet security concerns. But, wouldn't be fun if we could extend JavaScript to create wonderful programs which mix desktop and web technologies? Absolutely! In order to do that, we'll use WebKit and Gtk+.

The source code of this tutorial is available at github.com/vrruiz/WebKit-JavaScriptCore-Extensions.

 

WebKit

Chances are that you're using a browser which comes with WebKit. This open source technology was originally developed by KDE, and forked by Apple. WebKit powers Apple's Safari, Google's Chrome and many other browsers.

WebKit is to browsers what an engine is to cars: many bodyworks can carry the same engine model. WebKit provides the basic functions to download, parse, run and display web pages. However, WebKit doesn't provide a user interface to introduce URL address, change settings, navigation buttons, etc. That's the developer's job.

WebKit is multiplatform. Interesting to us is that WebKit is extensible and provides ways to interact with its JavaScript default engine, JavaScriptCore.

Currently, KDE, Gnome and MacOS X support WebKit, to provide HTML views inside desktop applications. WebKit has been ported to many platforms (Mac, Linux, Windows), many SDK's (Cocoa, Gtk, Qt) and many languages.  One of this ports is WebKitGTK+.

 

WebKitGTK+

GTK+ is multiplatform graphical toolkit, a set of graphical libraries to program desktop applications (the popular Linux desktop enviroment GNOME is built upon GTK+). WebKitGTK+ allows GTK+ applications to display web pages using WebKit. Originally, WebKit is programmed in C++, but WebKitGTK+ has a C interface using GObject (part of GLib, which "enables" object-oriented programming in plain C). 

To install GTK+ and WebKitGTK+ development files in Ubuntu do this in the command line:

$ sudo apt-get install gnome-devel libwebkitgtk-3.0-dev

 

A simple web view.

This program creates a GTK+ application. The main window has 800 x 600 pixels, and contains a scrolled window which finally holds the web view. The web view displays this blog.

#include <gtk/gtk.h>
#include <webkit/webkit.h>

/* Destroy callback */
static void destroy_cb( GtkWidget *widget,
                     gpointer   data )
{
    gtk_main_quit();
}

int
main (int argc, char* argv[])
{
    /* Initialize the widget set */
    gtk_init (&argc, &argv);
    
    /* Create the window widgets */
    GtkWidget *main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    
    /* Create the WebKit Web View widget */
    GtkWidget *web_view = webkit_web_view_new ();

    /* Place the WebKitWebView in the GtkScrolledWindow */
    gtk_container_add (GTK_CONTAINER (scrolled_window), web_view);
    gtk_container_add (GTK_CONTAINER (main_window), scrolled_window);

    /* Connect the destroy window event with destroy function */
    g_signal_connect (G_OBJECT (main_window), "destroy", G_CALLBACK (destroy_cb), NULL);
    
    /* Open webpage */
    webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), "http://rvr.typepad.com/");

    /* Create the main window */
    gtk_window_set_default_size (GTK_WINDOW (main_window), 800, 600);
    
    /* Show the application window */
    gtk_widget_show_all (main_window);

    /* Enter the main event loop, and wait for user interaction */
    gtk_main ();
    
    /* The user lost interest */
    return 0;
}

To compile and run this program, type:

$ gcc webkit-01.c -o webkit-01 `pkg-config --cflags --libs webkitgtk-3.0`

$ ./webkit-01

An application with a web view will appear.

Your own browser in just a minute. Easy, isn't it?

 

Interacting with JavaScriptCore.

What's great about WebKit is that the JavaScript engine can be extended to support custom functions. PhoneGap SDK actually uses this feature to provide mobile developers access to low-level OS features via JavaScript (i.e. accelerometer).

As previously stated, JavaScriptCore is WebKit's JavaScript engine, at it provides an API to extend JavaScript and add new classes. Basically, for each class we need to provide callbacks for the constructor and destructor class, and a list of class methods and a callback for each of them.

Next is the source code of a bare JavaScript class declaration. It does nothing, except to print a messages in the console when the class is initialized and the constructor method called.

#include <gtk/gtk.h>
#include <webkit/webkit.h>
#include <JavaScriptCore/JavaScript.h>

/* Class initialize */
static void class_init_cb(JSContextRef ctx,
                          JSObjectRef object)
{
    g_message("Custom class initialize.");
}

/* Class finalize */
static void class_finalize_cb(JSObjectRef object)
{
    g_message("Custom class finalize.");
}


/* Class constructor. Called at "new CustomClass()" */
JSObjectRef class_constructor_cb(JSContextRef ctx,
                                 JSObjectRef constructor,
                                 size_t argumentCount,
                                 const JSValueRef arguments[],
                                 JSValueRef* exception)
{
    g_message("Custom class constructor");
}

static const JSClassDefinition class_def =
{
    0,                     // version
    kJSClassAttributeNone, // attributes
    "CustomClass",         // className
    NULL,                  // parentClass
    NULL,                  // staticValues
    NULL,                  // staticFunctions
    class_init_cb,         // initialize
    class_finalize_cb,     // finalize
    NULL,                  // hasProperty
    NULL,                  // getProperty
    NULL,                  // setProperty
    NULL,                  // deleteProperty
    NULL,                  // getPropertyNames
    NULL,                  // callAsFunction
    class_constructor_cb,  // callAsConstructor
    NULL,                  // hasInstance  
    NULL                   // convertToType
};

/* Callback - JavaScript window object has been cleared */
static void window_object_cleared_cb(WebKitWebView  *web_view,
                                     WebKitWebFrame *frame,
                                     gpointer        context,
                                     gpointer        window_object,
                                     gpointer        user_data)

{
    /* Add classes to JavaScriptCore */
    JSClassRef classDef = JSClassCreate(&class_def);
    JSObjectRef classObj = JSObjectMake(context, classDef, context);
    JSObjectRef globalObj = JSContextGetGlobalObject(context);
    JSStringRef str = JSStringCreateWithUTF8CString("CustomClass");
    JSObjectSetProperty(context, globalObj, str, classObj, kJSPropertyAttributeNone, NULL);
}

/* Destroy callback */
static void destroy(GtkWidget *widget,
                    gpointer   data )
{
    gtk_main_quit();
}

int
main (int argc, char* argv[])
{
    /* Initialize the widget set */
    gtk_init (&argc, &argv);
    
    /* Create the window widgets */
    GtkWidget *main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    GtkWidget *scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    
    /* Create the WebKit Web View widget */
    GtkWidget *web_view = webkit_web_view_new ();
    
    /* Connect the window object cleared event with callback */
    g_signal_connect (G_OBJECT (web_view), "window-object-cleared", G_CALLBACK(window_object_cleared_cb), web_view);

    /* Place the WebKitWebView in the GtkScrolledWindow */
    gtk_container_add (GTK_CONTAINER (scrolled_window), web_view);
    gtk_container_add (GTK_CONTAINER (main_window), scrolled_window);

    /* Connect the destroy window event with destroy function */
    g_signal_connect (G_OBJECT (main_window), "destroy", G_CALLBACK (destroy), NULL);
    
    /* Open webpage */
    webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), "file://webkit-class.html");

    /* Create the main window */
    gtk_window_set_default_size (GTK_WINDOW (main_window), 800, 600);
    
    /* Show the application window */
    gtk_widget_show_all (main_window);

    /* Enter the main event loop, and wait for user interaction */
    gtk_main ();
    
    /* The user lost interest */
    return 0;
}

This program loads a local file, webkit-02.html, which only defines a CustomClass object.

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
  <h1>Extending JavaScript with WebKit. Custom Class.</h1>
  <script type="text/javascript">
    custom = new CustomClass();
  </script>
</body>
</html>

To compile this, type:

$ gcc webkit-02.c -o webkit-02 `pkg-config --cflags --libs webkitgtk-3.0`

The execution of this page will run our callback methods.

$ ./webkit-02

** Message: Custom class initialize.

** Message: Custom class constructor.

Of course, our custom class do nothing, except to print messages.

In the next post, we'll see add some real features to JavaScript. Enjoy!

Read more