Alright, that’s not too impressive… but it’s sufficient to explore the basic concepts required to make an object-oriented Scala Native binding to Gtk+.
Example in C
Here’s what this example looks like in C:
#include <gtk/gtk.h>
// callback used to close the window and exit the application
static void destroy (GtkWidget*, gpointer);
int main(int argc, char* argv[]) {
GtkWidget *window, *label
gtk_init(&argc, &argv);
/* Create and configure main window */
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "Hello Scala Native!");
gtk_container_set_border_width(GTK_CONTAINER(window), 10);
gtk_widget_set_size_request(window, 200, 100);
/* Connect the main window to the 'destroy' signal.
* This signal is emitted when we click on the window 'close' button. */
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);
/* Create a label */
label = gtk_label_new(NULL);
gtk_label_set_markup("<span size='large'>Hello Scala Native!</span>");
/* Add the label as a child of the window */
gtk_container_add(GTK_CONTAINER(window), label);
/* Display window and all of its descendants */
gtk_widget_show_all(window);
/* Enter main loop */
gtk_main();
return 0;
}
void destroy(GtkWidget* widget, gpointer data) {
gtk_main_quit();
}
If you’re not familiar with Gtk+, here are some explanations:
-
First we must initialize the library. In doing so, we can pass the command line arguments to Gtk+ which then will automatically handle all recognized arguments and remove them from the
argv
array. -
Next we create a new toplevel window, set the window title, the border width (an inner padding between window border and its child), and request a preferred window size. We need the calls to
GTK_WINDOW()
andGTK_CONTAINER()
1 to castwindow
to the expected type. -
Then we register an event handler (Gtk+ calls them “signals”) so that we can exit the application when the user clicks on the window close button.
-
We create a new label to display some text (using Pango markup language, which is similar to HTML), and add the new label as child to our window.
-
Finally we set all widgets to be visible and enter enter Gtk+ main event loop. Note that this loop won’t exit until we call
gtk_main_quit
, which happens in ourdestroy
callback.
Create Object-Oriented Bindings
The Gtk+ C API
Since Gtk+ is a pure C library, we can easily create an @extern
binding object for it and then
translate the example above line by line to Scala Native, e.g.:
import scalanative.native._
@extern
object gtk {
def gtk_init(argc: Int, argv: Ptr[CString]): Unit = extern
def gtk_window_new(windowType: Int): Ptr[Byte] = extern
def gtk_window_set_title(window: Ptr[Byte], title: CString): Unit = extern
def gtk_container_set_border_width(container: Ptr[Byte], width: Int): Unit = extern
...
}
object Main {
import gtk._
def main(args: Array[String]): Unit = {
gtk_init(0,null)
val window = gtk_window_new(0)
gtk_window_set_title(window, c"Hello Scala Native")
...
}
}
However, Gtk+ is an object-oriented library2: we have some global functions (gtk_init
, gtk_main
),
but the bulk are either
- object constructors (
gtk_window_new
,gtk_label_new
), - or operate on a previously created object, which is always passed as the first argument to the function
(
gtk_window_set_title
,gtk_container_set_border_width
, …). These are nothing else than instance methods in Scala, with the exception of the different call syntax.
The naming conventions of Gtk+ indicate on which type of object a function operates. Obviously, there are
objects of type GtkWindow
and GtkLabel
(we can construct both using a corresponding _new
function). Furthermore,
since we can cast a GtkWindow
to a GtkContainer
and then call gtk_container_set_border_width
on it,
GtkWindow
seems to be a subtype of GtkContainer
. And both, window
and label
are GtkWidget
s (we know that since
they were declared as such in the C example :).
Actually, GtkWidget
is not the immediate parent of GtkWindow
and `GtkLabel. Here’s their full genealogy:
We’ll only consider the types marked with *, and ignore the rest of them for this example.
There’s one more function in our example, that takes an object as its first argument: g_signal_connect()
registers an
event handler for any GObject
. Hence, despite it’s name prefix g_signal_
, we can consider it as an instance method of `GObject.
Modelling the Gtk+ Types in Scala
Since we’re using Scala, we want to get rid of that nasty casting business, so let’s make that Gtk+ type hierarchy explicit in Scala. Here’s what a first draft of our Scala API to Gtk+ could look like:
@extern
object Gtk {
@name("gtk_init")
def init(argc: Ptr[Int], argv: Ptr[Ptr[CString]]): Unit = extern
@name("gtk_main")
def main(): Unit = extern
@name("gtk_main_quit")
def mainQuit(): Unit = extern
}
abstract class GObject {
// In GObject, the c_handler actually receives 2 args: the pointer to the object that received the signal,
// and the pointer to a user-defined data object (which may be null)
def signalConnect(detailed_signal: CString, c_handler: CFunctionPtr0[Unit], data: Ptr[Byte]): UInt = ???
}
abstract class GtkWidget extends GObject {
def setSizeRequest(width: Int, height: Int): Unit = ???
def showAll(): Unit = ???
}
abstract class GtkContainer extends GtkWidget {
def setBorderWidth(width: Int): Unit = ???
def add(widget: GtkWidget): Unit = ???
}
class GtkWindow(windowType: Int) extends GtkContainer {
def setTitle(title: CString): Unit = ???
}
class GtkLabel(str: CString) extends GtkWidget {
def setMarkup(str: CString): Unit = ???
}
You’ll notice that I’ve translated the snake_case function names to camelCase method names, and stripped the class suffix
from the names. All classes are abstract except GtkWindow
and GtkLabel
, which are the only ones we can actually
instantiate.
And here is the main
of our example translated to this Scala API:
object Main {
def main(args: Array[String]): Unit = {
Gtk.init(null,null)
val window = new GtkWindow(0) // 0 = top level window
window.setTitle(c"Hello Scala Native!")
window.setBorderWidth(10)
window.setSizeRequest(200, 100)
window.signalConnect(c"destroy", CFunctionPtr.fromFunction0(destroy), null)
val label = new GtkLabel(null)
label.setMarkup(c"<span size='large'>Hello Scala Native!</span>")
window.add(label)
window.showAll()
Gtk.main()
}
def destroy(): Unit = {
Gtk.mainQuit()
}
}
Implement Bindings
Let’s start with the binding implementation for GtkLabel
. We’d like to use it as a normal Scala class and
create new instances using the new
operator. To this end we need to call gtk_label_new()
from the primary constructor.
But what should we do with the instance pointer returned by the C function? We need to store it with our scala instance,
since we must pass it back to Gtk+ every time we call an instance method on our object.
One solution to this problem is to replace the currently defined primary constructor with one that takes the C pointer
as its only argument, and then define a secondary constructor that has the parameter signature of gtk_label_new
:
trait Ref
class GtkLabel private(val ref: Ref) extends GtkWidget {
def this(str: CString) = this(GtkLabel.gtk_label_new(str).cast[Ref])
def setMarkup(str: CString): Unit = ???
}
@extern
object GtkLabel {
def gtk_label_new(str: CString): Ptr[Byte] = extern
}
This snippet requires some explanations:
-
We put all external declarations for a Gtk+ “class” in its companion object.
-
The type of the external C reference
ref
to our Gtk+ object is a special marker traitRef
, notPtr[Byte]
(as one would expect, since the return type ofgtk_label_new()
isPtr[Byte]
). The reason for this becomes clear when we consider the signature of the secondary constructor: it takes one argument of typeCString
. ButCString
is an alias forPtr[Byte]
– so we would have two constructors with identical signatures (which is not possible, of course). Hence the special marker traitRef
, which we know will never occur in the signatures of the C API. -
We make the primary constructor private to avoid accidentially using it directly.
The implementation of the instance method setMarkup
is then straightforward:
class GtkLabel private(val ref: Ref) extends GtkWidget {
...
def setMarkup(str: CString): Unit = GtkLabel
.gtk_label_set_markup(ref.cast[Ptr[Byte]],str)
}
@extern
object GtkLabel {
...
def gtk_label_set_markup(self: Ptr[Byte], str: CString): Unit = extern
}
Next we implement the parent class GtkWidget
, where we don’t define the special primary constructor, but instead declare
an abstract ref
:
abstract class GtkWidget extends GObject {
def ref: Ref
def setSizeRequest(width: Int, height: Int): Unit = GtkWidget
.gtk_widget_set_size_request(ref.cast[Ptr[Byte]],width,height)
def showAll(): Unit = GtkWidget.gtk_widget_show_all(ref.cast[Ptr[Byte]])
}
@extern
object GtkWidget {
def gtk_widget_set_size_request(self: Ptr[Byte], width: Int, height: Int): Unit = extern
def gtk_widget_show_all(self: Ptr[Byte]): Unit = extern
}
The implementation for GtkContainer
is analogous, with one notable exception: in the add()
method we must
pass the C ref
of the widget to Gtk+, not the Scal aobject (which is the reason we made ref
a public val
):
abstract class GtkContainer extends GtkWidget {
def ref: Ref
def setBorderWidth(width: Int): Unit = GtkContainer
.gtk_container_set_border_width(ref.cast[Ptr[Byte]],width)
def add(widget: GtkWidget): Unit = GtkContainer
.gtk_container_add(ref.cast[Ptr[Byte]], widget.ref.cast[Ptr[Byte]])
}
@extern
object GtkContainer {
def gtk_container_set_border_width(self: Ptr[Byte], width: Int): Unit = extern
def gtk_container_add(self: Ptr[Byte], widget: Ptr[Byte]): Unit = extern
}
The implementation of GObject.signalConnect
deviates from the standard pattern: gtk_signal_connect
is actually
a macro in Gtk+, so we need to call gtk_signal_connect_data
instead (which is what the C macro does):
abstract class GObject {
def ref: Ref
// In GObject, the c_handler actually receives 2 args: the pointer to the object that received the signal,
// and the pointer to a user-defined data object (which may be null)
def signalConnect(detailed_signal: CString, c_handler: CFunctionPtr0[Unit],
data: Ptr[Byte]): UInt =
GObject.g_signal_connect_data(ref.cast[Ptr[Byte]],detailed_signal,
c_handler,data,null,0)
}
@extern
object GObject {
def g_signal_connect_data(self: Ptr[Byte],
detailed_signal: CString,
c_handler: CFunctionPtr0[Unit],
data: Ptr[Byte],
destroy_data: CFunctionPtr0[Unit],
connect_flags: Int): UInt = extern
}
And we’re done! You can view the complete sample code,
or checkout the complete project and
run it with sbt manual/run
.
Generate Binding Code Automatically
Phew! Our example runs, but hacking in all that binding code is a little bit tedious, especially since most of it can be derived from the class declaration using some simple rules. The experimental scalanative-obj-interop project does exactly that. We just need to specify the interface to the Gtk+ types:
@CObj
object Gtk {
def init(argc: Ptr[Int], argv: Ptr[Ptr[CString]]): Unit = extern
def main(): Unit = extern
def mainQuit(): Unit = extern
}
@CObj
abstract class GObject {
def signalConnect(detailed_signal: CString, c_handler: CFunctionPtr0[Unit],
data: Ptr[Byte]): UInt =
signalConnectData(detailed_signal,c_handler,data,null,0)
@name("g_signal_connect_data")
def signalConnectData(detailed_signal: CString, c_handler: CFunctionPtr0[Unit],
data: Ptr[Byte], destroy_data: CFunctionPtr0[Unit],
connect_flags: Int): UInt = extern
}
@CObj
abstract class GtkWidget extends GObject {
def setSizeRequest(width: Int, height: Int): Unit = extern
def showAll(): Unit = extern
}
@CObj
abstract class GtkContainer extends GtkWidget {
def setBorderWidth(width: Int): Unit = extern
def add(widget: GtkWidget): Unit = extern
}
@CObj
class GtkWindow(windowType: Int) extends GtkContainer {
def setTitle(title: CString): Unit = extern
}
@CObj
class GtkLabel(str: CString) extends GtkWidget {
def setMarkup(str: CString): Unit = extern
}
and the rest will be generated during compilation by the @CObj
annotation.
If you like to try it, check out the sample project
and run sbt generated/run
. If you’d lik to inspect the generated code, you can add a @debug
annotation to a class,
and the expanded class + companion object are printed out to the console during compilation:
import de.surfice.smacrotools.debug
@CObj
@debug
abstract class GObject {
...
}
Of course, you don’t need to create the Gtk+ bindings yourself: the scalantive-gtk project intends to provide bindings for GLib, Gtk+, and possibly other libraries from the GNOME project (but it’s still in a very early stage).
That’s it – have fun coding Gtk+ apps in Scala!