[gjs] gobject: GObject.Interface



commit 9e868b146e26931e74f83024528d64a86be5bcab
Author: Philip Chimento <philip endlessm com>
Date:   Mon Jun 15 11:57:34 2015 -0700

    gobject: GObject.Interface
    
    This adds the ability to define new interfaces in GJS that are known to
    GObject and that have signals and properties. Objects implementing a
    GObject.Interface get the interface's signals automatically. They must,
    however, reimplement the interface's properties or an error will occur.
    This is just as in C, except there we conveniently have
    g_param_spec_override(). In a later commit we will add a
    GObject.ParamSpec.override() to make this less painful in GJS.
    
    This also refactors some code that is now common to both classes and
    interfaces into reusable functions, both on the C and Javascript side
    of things. Where possible without too much churn, it uses newer
    SpiderMonkey APIs in code that it touches.
    
    (Collaboration by Philip Chimento <philip endlessm com> and Roberto
    Goizueta <goizueta endlessm com>)
    
    https://bugzilla.gnome.org/show_bug.cgi?id=751343

 gi/gtype.cpp                               |   22 ++
 gi/gtype.h                                 |    4 +
 gi/interface.cpp                           |   37 +++-
 gi/interface.h                             |    4 +-
 gi/object.cpp                              |  277 ++++++++++++++++++++------
 gi/repo.cpp                                |    4 +-
 installed-tests/js/testGObjectClass.js     |   13 ++
 installed-tests/js/testGObjectInterface.js |  297 ++++++++++++++++++++++++++++
 modules/lang.js                            |   53 +++++
 modules/overrides/GObject.js               |  129 ++++++++++---
 10 files changed, 739 insertions(+), 101 deletions(-)
---
diff --git a/gi/gtype.cpp b/gi/gtype.cpp
index ea2b496..a797194 100644
--- a/gi/gtype.cpp
+++ b/gi/gtype.cpp
@@ -211,3 +211,25 @@ gjs_typecheck_gtype (JSContext             *context,
 {
     return do_base_typecheck(context, obj, throw_error);
 }
+
+const char *
+gjs_get_names_from_gtype_and_gi_info(GType        gtype,
+                                     GIBaseInfo  *info,
+                                     const char **constructor_name)
+{
+    const char *ns;
+    /* ns is only used to set the JSClass->name field (exposed by
+     * Object.prototype.toString).
+     * We can safely set "unknown" if there is no info, as in that case
+     * the name is globally unique (it's a GType name). */
+    if (info) {
+        ns = g_base_info_get_namespace((GIBaseInfo*) info);
+        if (constructor_name)
+            *constructor_name = g_base_info_get_name((GIBaseInfo*) info);
+    } else {
+        ns = "unknown";
+        if (constructor_name)
+            *constructor_name = g_type_name(gtype);
+    }
+    return ns;
+}
diff --git a/gi/gtype.h b/gi/gtype.h
index b2351e6..bde87ff 100644
--- a/gi/gtype.h
+++ b/gi/gtype.h
@@ -46,6 +46,10 @@ JSBool    gjs_typecheck_gtype         (JSContext             *context,
                                        JSObject              *obj,
                                        JSBool                 throw_error);
 
+const char *gjs_get_names_from_gtype_and_gi_info(GType        gtype,
+                                                 GIBaseInfo  *info,
+                                                 const char **constructor_name);
+
 G_END_DECLS
 
 #endif  /* __GJS_INTERFACE_H__ */
diff --git a/gi/interface.cpp b/gi/interface.cpp
index 96c7230..44cc6e5 100644
--- a/gi/interface.cpp
+++ b/gi/interface.cpp
@@ -38,6 +38,9 @@
 typedef struct {
     GIInterfaceInfo *info;
     GType gtype;
+    /* the GTypeInterface vtable wrapped by this JS Object (only used for
+       prototypes) */
+    GTypeInterface *vtable;
 } Interface;
 
 extern struct JSClass gjs_interface_class;
@@ -60,6 +63,8 @@ interface_finalize(JSFreeOp *fop,
     if (priv->info != NULL)
         g_base_info_unref((GIBaseInfo*)priv->info);
 
+    g_clear_pointer(&priv->vtable, (GDestroyNotify)g_type_default_interface_unref);
+
     GJS_DEC_COUNTER(interface);
     g_slice_free(Interface, priv);
 }
@@ -119,6 +124,14 @@ interface_new_resolve(JSContext *context,
     if (priv == NULL)
         goto out;
 
+    /* If we have no GIRepository information then this interface was defined
+     * from within GJS. In that case, it has no properties that need to be
+     * resolved from within C code, as interfaces cannot inherit. */
+    if (priv->info == NULL) {
+        ret = JS_TRUE;
+        goto out;
+    }
+
     method_info = g_interface_info_find_method((GIInterfaceInfo*) priv->info, name);
 
     if (method_info != NULL) {
@@ -172,19 +185,23 @@ JSFunctionSpec gjs_interface_proto_funcs[] = {
 JSBool
 gjs_define_interface_class(JSContext       *context,
                            JSObject        *in_object,
-                           GIInterfaceInfo *info)
+                           GIInterfaceInfo *info,
+                           GType            gtype,
+                           JSObject       **constructor_p)
 {
     Interface *priv;
     const char *constructor_name;
+    const char *ns;
     JSObject *constructor;
     JSObject *prototype;
     jsval value;
 
-    constructor_name = g_base_info_get_name((GIBaseInfo*)info);
+    ns = gjs_get_names_from_gtype_and_gi_info(gtype, (GIBaseInfo *) info,
+                                              &constructor_name);
 
     if (!gjs_init_class_dynamic(context, in_object,
                                 NULL,
-                                g_base_info_get_namespace((GIBaseInfo*)info),
+                                ns,
                                 constructor_name,
                                 &gjs_interface_class,
                                 gjs_interface_constructor, 0,
@@ -203,17 +220,23 @@ gjs_define_interface_class(JSContext       *context,
 
     GJS_INC_COUNTER(interface);
     priv = g_slice_new0(Interface);
-    priv->info = info;
-    priv->gtype = g_registered_type_info_get_g_type(priv->info);
-    g_base_info_ref((GIBaseInfo*)priv->info);
+    priv->info = info == NULL ? NULL : g_base_info_ref((GIBaseInfo *) info);
+    priv->gtype = gtype;
+    priv->vtable = (GTypeInterface *) g_type_default_interface_ref(gtype);
     JS_SetPrivate(prototype, priv);
 
-    gjs_define_static_methods(context, constructor, priv->gtype, priv->info);
+    /* If we have no GIRepository information, then this interface was defined
+     * from within GJS and therefore has no C static methods to be defined. */
+    if (priv->info)
+        gjs_define_static_methods(context, constructor, priv->gtype, priv->info);
 
     value = OBJECT_TO_JSVAL(gjs_gtype_create_gtype_wrapper(context, priv->gtype));
     JS_DefineProperty(context, constructor, "$gtype", value,
                       NULL, NULL, JSPROP_PERMANENT);
 
+    if (constructor_p)
+        *constructor_p = constructor;
+
     return JS_TRUE;
 }
 
diff --git a/gi/interface.h b/gi/interface.h
index cdb41ab..70e71a7 100644
--- a/gi/interface.h
+++ b/gi/interface.h
@@ -33,7 +33,9 @@ G_BEGIN_DECLS
 
 JSBool gjs_define_interface_class (JSContext       *context,
                                    JSObject        *in_object,
-                                   GIInterfaceInfo *info);
+                                   GIInterfaceInfo *info,
+                                   GType            gtype,
+                                   JSObject       **constructor_p);
 
 JSBool gjs_lookup_interface_constructor (JSContext     *context,
                                          GType          gtype,
diff --git a/gi/object.cpp b/gi/object.cpp
index 376dd85..fe5ef85 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -28,6 +28,7 @@
 #include <gjs/gi.h>
 #include "object.h"
 #include "gtype.h"
+#include "interface.h"
 #include "arg.h"
 #include "repo.h"
 #include "gtype.h"
@@ -1997,16 +1998,8 @@ gjs_define_object_class(JSContext      *context,
     if (parent_type != G_TYPE_INVALID)
        parent_proto = gjs_lookup_object_prototype(context, parent_type);
 
-    /* ns is only used to set the JSClass->name field (exposed by Object.prototype.toString).
-     * We can safely set "unknown" if there is no info, as in that case
-     * the name is globally unique (it's a GType name). */
-    if (info) {
-        ns = g_base_info_get_namespace((GIBaseInfo*) info);
-        constructor_name = g_base_info_get_name((GIBaseInfo*) info);
-    } else {
-        ns = "unknown";
-        constructor_name = g_type_name(gtype);
-    }
+    ns = gjs_get_names_from_gtype_and_gi_info(gtype, (GIBaseInfo *) info,
+                                              &constructor_name);
 
     if (!gjs_init_class_dynamic(context, in_object,
                                 parent_proto,
@@ -2449,6 +2442,29 @@ gjs_object_set_gproperty (GObject      *object,
 }
 
 static void
+gjs_interface_init(GTypeInterface *g_iface,
+                   gpointer        iface_data)
+{
+    GPtrArray *properties;
+    GType gtype;
+    guint i;
+
+    gtype = G_TYPE_FROM_INTERFACE (g_iface);
+
+    properties = (GPtrArray *) gjs_hash_table_for_gsize_lookup(class_init_properties, gtype);
+    if (properties == NULL)
+        return;
+
+    for (i = 0; i < properties->len; i++) {
+        GParamSpec *pspec = (GParamSpec *) properties->pdata[i];
+        g_param_spec_set_qdata(pspec, gjs_is_custom_property_quark(), GINT_TO_POINTER(1));
+        g_object_interface_install_property(g_iface, pspec);
+    }
+
+    gjs_hash_table_for_gsize_remove(class_init_properties, gtype);
+}
+
+static void
 gjs_object_class_init(GObjectClass *klass,
                       gpointer      user_data)
 {
@@ -2528,6 +2544,187 @@ gjs_add_interface(GType instance_type,
                                 &interface_vtable);
 }
 
+static gboolean
+validate_interfaces_and_properties_args(JSContext *cx,
+                                        JSObject  *interfaces,
+                                        JSObject  *properties,
+                                        guint32   *n_interfaces,
+                                        guint32   *n_properties)
+{
+    guint32 n_int, n_prop;
+
+    if (!JS_IsArrayObject(cx, interfaces)) {
+        gjs_throw(cx, "Invalid parameter interfaces (expected Array)");
+        return FALSE;
+    }
+
+    if (!JS_GetArrayLength(cx, interfaces, &n_int))
+        return FALSE;
+
+    if (!JS_IsArrayObject(cx, properties)) {
+        gjs_throw(cx, "Invalid parameter properties (expected Array)");
+        return FALSE;
+    }
+
+    if (!JS_GetArrayLength(cx, properties, &n_prop))
+        return FALSE;
+
+    if (n_interfaces != NULL)
+        *n_interfaces = n_int;
+    if (n_properties != NULL)
+        *n_properties = n_prop;
+    return TRUE;
+}
+
+static gboolean
+get_interface_gtypes(JSContext *cx,
+                     JSObject  *interfaces,
+                     guint32   n_interfaces,
+                     GType    *iface_types)
+{
+    guint32 i;
+
+    for (i = 0; i < n_interfaces; i++) {
+        JS::RootedValue iface_val(cx);
+        GType iface_type;
+
+        if (!JS_GetElement(cx, interfaces, i, &iface_val.get()))
+            return FALSE;
+
+        if (!iface_val.isObject()) {
+            gjs_throw (cx, "Invalid parameter interfaces (element %d was not a GType)", i);
+            return FALSE;
+        }
+
+        iface_type = gjs_gtype_get_actual_gtype(cx, &iface_val.toObject());
+        if (iface_type == G_TYPE_INVALID) {
+            gjs_throw (cx, "Invalid parameter interfaces (element %d was not a GType)", i);
+            return FALSE;
+        }
+
+        iface_types[i] = iface_type;
+    }
+    return TRUE;
+}
+
+static gboolean
+save_properties_for_class_init(JSContext *cx,
+                               JSObject  *properties,
+                               guint32    n_properties,
+                               GType      gtype)
+{
+    GPtrArray *properties_native = NULL;
+    guint32 i;
+
+    if (!class_init_properties)
+        class_init_properties = gjs_hash_table_new_for_gsize((GDestroyNotify) g_ptr_array_unref);
+    properties_native = g_ptr_array_new_with_free_func((GDestroyNotify) g_param_spec_unref);
+    for (i = 0; i < n_properties; i++) {
+        JS::RootedValue prop_val(cx);
+
+        if (!JS_GetElement(cx, properties, i, &prop_val.get())) {
+            g_clear_pointer(&properties_native, g_ptr_array_unref);
+            return FALSE;
+        }
+        if (!prop_val.isObject()) {
+            g_clear_pointer(&properties_native, g_ptr_array_unref);
+            gjs_throw(cx, "Invalid parameter, expected object");
+            return FALSE;
+        }
+
+        JS::RootedObject prop_obj(cx, &prop_val.toObject());
+        if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, JS_TRUE)) {
+            g_clear_pointer(&properties_native, g_ptr_array_unref);
+            return FALSE;
+        }
+        g_ptr_array_add(properties_native, g_param_spec_ref(gjs_g_param_from_param(cx, prop_obj)));
+    }
+    gjs_hash_table_for_gsize_insert(class_init_properties, (gsize) gtype,
+                                    g_ptr_array_ref(properties_native));
+
+    g_clear_pointer(&properties_native, g_ptr_array_unref);
+    return TRUE;
+}
+
+static JSBool
+gjs_register_interface(JSContext *cx,
+                       unsigned   argc,
+                       jsval     *vp)
+{
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    char *name = NULL;
+    JSObject *constructor, *interfaces, *properties, *module;
+    guint32 i, n_interfaces, n_properties;
+    GType *iface_types;
+    GType interface_type;
+    GTypeModule *type_module;
+    GTypeInfo type_info = {
+        sizeof (GTypeInterface), /* class_size */
+
+        (GBaseInitFunc) NULL,
+        (GBaseFinalizeFunc) NULL,
+
+        (GClassInitFunc) gjs_interface_init,
+        (GClassFinalizeFunc) NULL,
+        NULL, /* class_data */
+
+        0,    /* instance_size */
+        0,    /* n_preallocs */
+        NULL, /* instance_init */
+    };
+
+    if (!gjs_parse_call_args(cx, "register_interface", "soo", args,
+                             "name", &name,
+                             "interfaces", &interfaces,
+                             "properties", &properties))
+        return JS_FALSE;
+
+    if (!validate_interfaces_and_properties_args(cx, interfaces, properties,
+                                                 &n_interfaces, &n_properties)) {
+        g_clear_pointer(&name, g_free);
+        return JS_FALSE;
+    }
+
+    iface_types = (GType *) g_alloca(sizeof(GType) * n_interfaces);
+
+    /* We do interface addition in two passes so that any failure
+       is caught early, before registering the GType (which we can't undo) */
+    if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types)) {
+        g_clear_pointer(&name, g_free);
+        return JS_FALSE;
+    }
+
+    if (g_type_from_name(name) != G_TYPE_INVALID) {
+        gjs_throw(cx, "Type name %s is already registered", name);
+        g_clear_pointer(&name, g_free);
+        return JS_FALSE;
+    }
+
+    type_module = G_TYPE_MODULE(gjs_type_module_get());
+    interface_type = g_type_module_register_type(type_module,
+                                                 G_TYPE_INTERFACE,
+                                                 name,
+                                                 &type_info,
+                                                 (GTypeFlags) 0);
+    g_clear_pointer(&name, g_free);
+
+    g_type_set_qdata(interface_type, gjs_is_custom_type_quark(), GINT_TO_POINTER(1));
+
+    if (!save_properties_for_class_init(cx, properties, n_properties, interface_type))
+        return JS_FALSE;
+
+    for (i = 0; i < n_interfaces; i++)
+        g_type_interface_add_prerequisite(interface_type, iface_types[i]);
+
+    /* create a custom JSClass */
+    if ((module = gjs_lookup_private_namespace(cx)) == NULL)
+        return JS_FALSE;  /* error will have been thrown already */
+    gjs_define_interface_class(cx, module, NULL, interface_type, &constructor);
+
+    args.rval().setObject(*constructor);
+    return JS_TRUE;
+}
+
 static JSBool
 gjs_register_type(JSContext *cx,
                   unsigned   argc,
@@ -2555,7 +2752,6 @@ gjs_register_type(JSContext *cx,
        gjs_object_custom_init,
     };
     guint32 i, n_interfaces, n_properties;
-    GPtrArray *properties_native = NULL;
     GType *iface_types;
     JSBool retval = JS_FALSE;
 
@@ -2575,42 +2771,16 @@ gjs_register_type(JSContext *cx,
     if (!do_base_typecheck(cx, parent, JS_TRUE))
         goto out;
 
-    if (!JS_IsArrayObject(cx, interfaces)) {
-        gjs_throw(cx, "Invalid parameter interfaces (expected Array)");
-        goto out;
-    }
-
-    if (!JS_GetArrayLength(cx, interfaces, &n_interfaces))
-        goto out;
-
-    if (!JS_IsArrayObject(cx, properties)) {
-        gjs_throw(cx, "Invalid parameter properties (expected Array)");
-        goto out;
-    }
-
-    if (!JS_GetArrayLength(cx, properties, &n_properties))
+    if (!validate_interfaces_and_properties_args(cx, interfaces, properties,
+                                                 &n_interfaces, &n_properties))
         goto out;
 
     iface_types = (GType*) g_alloca(sizeof(GType) * n_interfaces);
 
     /* We do interface addition in two passes so that any failure
        is caught early, before registering the GType (which we can't undo) */
-    for (i = 0; i < n_interfaces; i++) {
-        jsval iface_val;
-        GType iface_type;
-
-        if (!JS_GetElement(cx, interfaces, i, &iface_val))
-            goto out;
-
-        if (!JSVAL_IS_OBJECT(iface_val) ||
-            ((iface_type = gjs_gtype_get_actual_gtype(cx, JSVAL_TO_OBJECT(iface_val)))
-             == G_TYPE_INVALID)) {
-            gjs_throw(cx, "Invalid parameter interfaces (element %d was not a GType)", i);
-            goto out;
-        }
-
-        iface_types[i] = iface_type;
-    }
+    if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types))
+        goto out;
 
     if (g_type_from_name(name) != G_TYPE_INVALID) {
         gjs_throw (cx, "Type name %s is already registered", name);
@@ -2644,27 +2814,8 @@ gjs_register_type(JSContext *cx,
 
     g_type_set_qdata (instance_type, gjs_is_custom_type_quark(), GINT_TO_POINTER (1));
 
-    if (!class_init_properties)
-        class_init_properties = gjs_hash_table_new_for_gsize ((GDestroyNotify)g_ptr_array_unref);
-    properties_native = g_ptr_array_new_with_free_func ((GDestroyNotify)g_param_spec_unref);
-    for (i = 0; i < n_properties; i++) {
-        jsval prop_val;
-        JSObject *prop_obj;
-
-        if (!JS_GetElement(cx, properties, i, &prop_val))
-            goto out;
-
-        if (!JSVAL_IS_OBJECT(prop_val)) {
-            gjs_throw (cx, "Invalid parameter, expected object");
-            goto out;
-        }
-        prop_obj = JSVAL_TO_OBJECT(prop_val);
-        if (!gjs_typecheck_param(cx, prop_obj, G_TYPE_NONE, JS_TRUE))
-            goto out;
-        g_ptr_array_add (properties_native, g_param_spec_ref (gjs_g_param_from_param (cx, prop_obj)));
-    }
-    gjs_hash_table_for_gsize_insert (class_init_properties, (gsize)instance_type,
-                                     g_ptr_array_ref (properties_native));
+    if (!save_properties_for_class_init(cx, properties, n_properties, instance_type))
+        goto out;
 
     for (i = 0; i < n_interfaces; i++)
         gjs_add_interface(instance_type, iface_types[i]);
@@ -2678,7 +2829,6 @@ gjs_register_type(JSContext *cx,
     retval = JS_TRUE;
 
 out:
-    g_clear_pointer(&properties_native, g_ptr_array_unref);
     JS_EndRequest(cx);
 
     return retval;
@@ -2775,6 +2925,7 @@ gjs_signal_new(JSContext *cx,
 }
 
 static JSFunctionSpec module_funcs[] = {
+    { "register_interface", JSOP_WRAPPER((JSNative) gjs_register_interface), 3, GJS_MODULE_PROP_FLAGS },
     { "register_type", JSOP_WRAPPER ((JSNative) gjs_register_type), 4, GJS_MODULE_PROP_FLAGS },
     { "add_interface", JSOP_WRAPPER ((JSNative) gjs_add_interface), 2, GJS_MODULE_PROP_FLAGS },
     { "hook_up_vfunc", JSOP_WRAPPER ((JSNative) gjs_hook_up_vfunc), 3, GJS_MODULE_PROP_FLAGS },
diff --git a/gi/repo.cpp b/gi/repo.cpp
index f44cb40..748d3d5 100644
--- a/gi/repo.cpp
+++ b/gi/repo.cpp
@@ -530,7 +530,9 @@ gjs_define_info(JSContext  *context,
             return JS_FALSE;
         break;
     case GI_INFO_TYPE_INTERFACE:
-        gjs_define_interface_class(context, in_object, (GIInterfaceInfo*) info);
+        gjs_define_interface_class(context, in_object, (GIInterfaceInfo *) info,
+                                   g_registered_type_info_get_g_type((GIRegisteredTypeInfo *) info),
+                                   NULL);
         break;
     default:
         gjs_throw(context, "API of type %s not implemented, cannot define %s.%s",
diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js
index b2017dd..fb45acc 100644
--- a/installed-tests/js/testGObjectClass.js
+++ b/installed-tests/js/testGObjectClass.js
@@ -295,4 +295,17 @@ function testInstanceInit() {
     new MyCustomInit();
 }
 
+function testClassCanHaveInterfaceProperty() {
+    const InterfacePropObject = new Lang.Class({
+        Name: 'InterfacePropObject',
+        Extends: GObject.Object,
+        Properties: {
+            'file': GObject.ParamSpec.object('file', 'File', 'File',
+                GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE | 
GObject.ParamFlags.CONSTRUCT_ONLY,
+                Gio.File.$gtype)
+        }
+    });
+    let obj = new InterfacePropObject({ file: Gio.File.new_for_path('dummy') });
+}
+
 JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
diff --git a/installed-tests/js/testGObjectInterface.js b/installed-tests/js/testGObjectInterface.js
index c29aada..6fce052 100644
--- a/installed-tests/js/testGObjectInterface.js
+++ b/installed-tests/js/testGObjectInterface.js
@@ -1,9 +1,11 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
 
 const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
+const Mainloop = imports.mainloop;
 
 const AnInterface = new Lang.Interface({
     Name: 'AnInterface',
@@ -19,6 +21,101 @@ const GObjectImplementingLangInterface = new Lang.Class({
     }
 });
 
+const AGObjectInterface = new Lang.Interface({
+    Name: 'AGObjectInterface',
+    GTypeName: 'ArbitraryGTypeName',
+    Requires: [ GObject.Object ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.string('interface-prop',
+            'Interface property', 'Must be overridden in implementation',
+            GObject.ParamFlags.READABLE,
+            'foobar')
+    },
+    Signals: {
+        'interface-signal': {}
+    },
+
+    requiredG: Lang.Interface.UNIMPLEMENTED,
+    optionalG: function () {
+        return 'AGObjectInterface.optionalG()';
+    }
+});
+
+const InterfaceRequiringGObjectInterface = new Lang.Interface({
+    Name: 'InterfaceRequiringGObjectInterface',
+    Requires: [ AGObjectInterface ],
+
+    optionalG: function () {
+        return 'InterfaceRequiringGObjectInterface.optionalG()\n' +
+            AGObjectInterface.optionalG(this);
+    }
+});
+
+const GObjectImplementingGObjectInterface = new Lang.Class({
+    Name: 'GObjectImplementingGObjectInterface',
+    Extends: GObject.Object,
+    Implements: [ AGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.string('interface-prop', 'override',
+            'override', GObject.ParamFlags.READABLE, 'foobar'),
+        'class-prop': GObject.ParamSpec.string('class-prop', 'Class property',
+            'A property that is not on the interface',
+            GObject.ParamFlags.READABLE, 'meh')
+    },
+    Signals: {
+        'class-signal': {},
+    },
+
+    get interface_prop() {
+        return 'foobar';
+    },
+
+    get class_prop() {
+        return 'meh';
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {},
+    optionalG: function () {
+        return AGObjectInterface.optionalG(this);
+    }
+});
+
+const MinimalImplementationOfAGObjectInterface = new Lang.Class({
+    Name: 'MinimalImplementationOfAGObjectInterface',
+    Extends: GObject.Object,
+    Implements: [ AGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.string('interface-prop', 'override',
+            'override', GObject.ParamFlags.READABLE, 'foobar')
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {}
+});
+
+const ImplementationOfTwoInterfaces = new Lang.Class({
+    Name: 'ImplementationOfTwoInterfaces',
+    Extends: GObject.Object,
+    Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.string('interface-prop', 'override',
+            'override', GObject.ParamFlags.READABLE, 'foobar')
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {},
+    optionalG: function () {
+        return InterfaceRequiringGObjectInterface.optionalG(this);
+    }
+});
+
 function testGObjectClassCanImplementInterface() {
     // Test considered passing if no exception thrown
     new GObjectImplementingLangInterface();
@@ -38,4 +135,204 @@ function testGObjectCanImplementInterfacesFromJSAndC() {
     new ObjectImplementingLangInterfaceAndCInterface();
 }
 
+function testGObjectInterfaceIsInstanceOfInterfaces() {
+    JSUnit.assertTrue(AGObjectInterface instanceof Lang.Interface);
+    JSUnit.assertTrue(AGObjectInterface instanceof GObject.Interface);
+}
+
+function testGObjectInterfaceCannotBeInstantiated() {
+    JSUnit.assertRaises(() => new AGObjectInterface());
+}
+
+function testGObjectInterfaceTypeName() {
+    JSUnit.assertEquals('ArbitraryGTypeName', AGObjectInterface.$gtype.name);
+}
+
+function testGObjectCanImplementInterface() {
+    // Test considered passing if no exception thrown
+    new GObjectImplementingGObjectInterface();
+}
+
+function testGObjectImplementingInterfaceHasCorrectClassObject() {
+    JSUnit.assertEquals('[object GObjectClass for GObjectImplementingGObjectInterface]', 
GObjectImplementingGObjectInterface.toString());
+    let obj = new GObjectImplementingGObjectInterface();
+    JSUnit.assertEquals(GObjectImplementingGObjectInterface, obj.constructor);
+    JSUnit.assertEquals('[object GObjectClass for GObjectImplementingGObjectInterface]',
+        obj.constructor.toString());
+}
+
+function testGObjectCanImplementBothGObjectAndNonGObjectInterfaces() {
+    // Test considered passing if no exception thrown
+    const GObjectImplementingBothKindsOfInterface = new Lang.Class({
+        Name: 'GObjectImplementingBothKindsOfInterface',
+        Extends: GObject.Object,
+        Implements: [ AnInterface, AGObjectInterface ],
+        Properties: {
+            'interface-prop': GObject.ParamSpec.string('interface-prop',
+                'override', 'override', GObject.ParamFlags.READABLE, 'foobar')
+        },
+
+        _init: function (props={}) {
+            this.parent(props);
+        },
+        required: function () {},
+        requiredG: function () {}
+    });
+    new GObjectImplementingBothKindsOfInterface();
+}
+
+function testGObjectCanImplementRequiredFunction() {
+    // Test considered passing if no exception thrown
+    let obj = new GObjectImplementingGObjectInterface();
+    obj.requiredG();
+}
+
+function testGObjectMustImplementRequiredFunction () {
+    JSUnit.assertRaises(() => new Lang.Class({
+        Name: 'BadObject',
+        Extends: GObject.Object,
+        Implements: [ AGObjectInterface ],
+        Properties: {
+            'interface-prop': GObject.ParamSpec.string('interface-prop',
+                'override', 'override', GObject.ParamFlags.READABLE, 'foobar')
+        }
+    }));
+}
+
+function testGObjectDoesntHaveToImplementOptionalFunction() {
+    // Test considered passing if no exception thrown
+    new MinimalImplementationOfAGObjectInterface();
+}
+
+function testGObjectCanDeferToInterfaceOptionalFunction() {
+    let obj = new MinimalImplementationOfAGObjectInterface();
+    JSUnit.assertEquals('AGObjectInterface.optionalG()', obj.optionalG());
+}
+
+function testGObjectCanChainUpToInterface() {
+    let obj = new GObjectImplementingGObjectInterface();
+    JSUnit.assertEquals('AGObjectInterface.optionalG()', obj.optionalG());
+}
+
+function testGObjectInterfaceCanRequireOtherInterface() {
+    // Test considered passing if no exception thrown
+    new ImplementationOfTwoInterfaces();
+}
+
+function testGObjectInterfaceCanChainUpToOtherInterface() {
+    let obj = new ImplementationOfTwoInterfaces();
+    JSUnit.assertEquals('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()',
+        obj.optionalG());
+}
+
+function testGObjectDefersToLastInterfaceOptionalFunction() {
+    const MinimalImplementationOfTwoInterfaces = new Lang.Class({
+        Name: 'MinimalImplementationOfTwoInterfaces',
+        Extends: GObject.Object,
+        Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+        Properties: {
+            'interface-prop': GObject.ParamSpec.string('interface-prop',
+                'override', 'override', GObject.ParamFlags.READABLE, 'foobar')
+        },
+
+        _init: function (props={}) {
+            this.parent(props);
+        },
+        requiredG: function () {}
+    });
+    let obj = new MinimalImplementationOfTwoInterfaces();
+    JSUnit.assertEquals('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()',
+        obj.optionalG());
+}
+
+function testGObjectClassMustImplementAllRequiredInterfaces() {
+    JSUnit.assertRaises(() => new Lang.Class({
+        Name: 'BadObject',
+        Implements: [ InterfaceRequiringGObjectInterface ],
+        required: function () {}
+    }));
+}
+
+function testGObjectClassMustImplementRequiredInterfacesInCorrectOrder() {
+    JSUnit.assertRaises(() => new Lang.Class({
+        Name: 'BadObject',
+        Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ],
+        required: function () {}
+    }));
+}
+
+function testGObjectInterfaceCanRequireInterfaceFromC() {
+    const InitableInterface = new Lang.Interface({
+        Name: 'InitableInterface',
+        Requires: [ GObject.Object, Gio.Initable ]
+    });
+    JSUnit.assertRaises(() => new Lang.Class({
+        Name: 'BadObject',
+        Implements: [ InitableInterface ]
+    }));
+}
+
+function testGObjectHasInterfaceSignalsAndClassSignals() {
+    let obj = new GObjectImplementingGObjectInterface();
+    let interface_signal_emitted = false, class_signal_emitted = false;
+    obj.connect('interface-signal', () => {
+        interface_signal_emitted = true;
+        Mainloop.quit('signal');
+    });
+    obj.connect('class-signal', () => {
+        class_signal_emitted = true;
+        Mainloop.quit('signal');
+    });
+    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => obj.emit('interface-signal'));
+    Mainloop.run('signal');
+    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => obj.emit('class-signal'));
+    Mainloop.run('signal');
+    JSUnit.assertTrue(interface_signal_emitted);
+    JSUnit.assertTrue(class_signal_emitted);
+}
+
+function testGObjectHasInterfacePropertiesAndClassProperties() {
+    let obj = new GObjectImplementingGObjectInterface();
+    JSUnit.assertEquals('foobar', obj.interface_prop);
+    JSUnit.assertEquals('meh', obj.class_prop);
+}
+
+// Failing to override an interface property doesn't raise an error but instead
+// logs a critical warning.
+function testGObjectMustOverrideInterfaceProperties() {
+    GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL,
+        "Object class * doesn't implement property 'interface-prop' from " +
+        "interface 'ArbitraryGTypeName'");
+    new Lang.Class({
+        Name: 'MyNaughtyObject',
+        Extends: GObject.Object,
+        Implements: [ AGObjectInterface ],
+        _init: function (props={}) {
+            this.parent(props);
+        },
+        requiredG: function () {}
+    });
+    // g_test_assert_expected_messages() is a macro, not introspectable
+    GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js',
+        416, 'testGObjectMustOverrideInterfaceProperties');
+}
+
+// This makes sure that we catch the case where the metaclass (e.g.
+// GtkWidgetClass) doesn't specify a meta-interface. In that case we get the
+// meta-interface from the metaclass's parent.
+function testInterfaceIsOfCorrectTypeForMetaclass() {
+    const MyMeta = new Lang.Class({
+        Name: 'MyMeta',
+        Extends: GObject.Class
+    });
+    const MyMetaObject = new MyMeta({
+        Name: 'MyMetaObject'
+    });
+    const MyMetaInterface = new Lang.Interface({
+        Name: 'MyMetaInterface',
+        Requires: [ MyMetaObject ]
+    });
+    JSUnit.assertTrue(MyMetaInterface instanceof GObject.Interface);
+}
+
 JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
diff --git a/modules/lang.js b/modules/lang.js
index bd6e28a..0f64807 100644
--- a/modules/lang.js
+++ b/modules/lang.js
@@ -293,10 +293,63 @@ Class.prototype._init = function(params) {
                     value: _parent }});
 };
 
+// This introduces the concept of a "meta-interface" which is given by the
+// MetaInterface property on an object's metaclass. For objects whose metaclass
+// is Lang.Class, the meta-interface is Lang.Interface. Subclasses of Lang.Class
+// such as GObject.Class supply their own meta-interface.
+// This is in order to enable creating GObject interfaces with Lang.Interface,
+// much as you can create GObject classes with Lang.Class.
+function _getMetaInterface(params) {
+    if (!params.Requires || params.Requires.length === 0)
+        return null;
+
+    let metaInterface = params.Requires.map((req) => {
+        if (req instanceof Interface)
+            return req.__super__;
+        for (let metaclass = req.prototype.__metaclass__; metaclass;
+            metaclass = metaclass.__super__) {
+            if (metaclass.hasOwnProperty('MetaInterface'))
+                return metaclass.MetaInterface;
+        }
+        return null;
+    })
+    .reduce((best, candidate) => {
+        // This function reduces to the "most derived" meta interface in the list.
+        if (best === null)
+            return candidate;
+        if (candidate === null)
+            return best;
+        for (let sup = candidate; sup; sup = sup.__super__) {
+            if (sup === best)
+                return candidate;
+        }
+        return best;
+    }, null);
+
+    // If we reach this point and we don't know the meta-interface, then it's
+    // most likely because there were only pure-C interfaces listed in Requires
+    // (and those don't have our magic properties.) However, all pure-C
+    // interfaces should require GObject.Object anyway.
+    if (metaInterface === null)
+        throw new Error('Did you forget to include GObject.Object in Requires?');
+
+    return metaInterface;
+}
+
 function Interface(params) {
+    let metaInterface = _getMetaInterface(params);
+    if (metaInterface && metaInterface !== this.constructor) {
+        // Trick to apply variadic arguments to constructors --
+        // bind the arguments into the constructor function.
+        let args = Array.prototype.slice.call(arguments);
+        let curried = Function.prototype.bind.apply(metaInterface, [,].concat(args));
+        return new curried();
+    }
     return this._construct.apply(this, arguments);
 }
 
+Class.MetaInterface = Interface;
+
 /**
  * Use this to signify a function that must be overridden in an implementation
  * of the interface. Creating a class that doesn't override the function will
diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js
index 7764e4f..7e9e9d5 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -24,6 +24,45 @@ const GjsPrivate = imports.gi.GjsPrivate;
 
 let GObject;
 
+// Some common functions between GObject.Class and GObject.Interface
+
+function _createSignals(gtype, signals) {
+    for (let signalName in signals) {
+        let obj = signals[signalName];
+        let flags = (obj.flags !== undefined) ? obj.flags : GObject.SignalFlags.RUN_FIRST;
+        let accumulator = (obj.accumulator !== undefined) ? obj.accumulator : GObject.AccumulatorType.NONE;
+        let rtype = (obj.return_type !== undefined) ? obj.return_type : GObject.TYPE_NONE;
+        let paramtypes = (obj.param_types !== undefined) ? obj.param_types : [];
+
+        try {
+            obj.signal_id = Gi.signal_new(gtype, signalName, flags, accumulator, rtype, paramtypes);
+        } catch (e) {
+            throw new TypeError('Invalid signal ' + signalName + ': ' + e.message);
+        }
+    }
+}
+
+function _createGTypeName(name, gtypename) {
+    if (gtypename)
+        return gtypename;
+    else
+        return 'Gjs_' + name;
+}
+
+function _getGObjectInterfaces(interfaces) {
+    return interfaces.filter((iface) => iface.hasOwnProperty('$gtype'));
+}
+
+function _propertiesAsArray(propertyObj) {
+    let propertiesArray = [];
+    if (propertyObj) {
+        for (let prop in propertyObj) {
+            propertiesArray.push(propertyObj[prop]);
+        }
+    }
+    return propertiesArray;
+}
+
 const GObjectMeta = new Lang.Class({
     Name: 'GObjectClass',
     Extends: Lang.Class,
@@ -35,21 +74,8 @@ const GObjectMeta = new Lang.Class({
 
         this.parent(params);
 
-        if (signals) {
-            for (let signalName in signals) {
-                let obj = signals[signalName];
-                let flags = (obj.flags !== undefined) ? obj.flags : GObject.SignalFlags.RUN_FIRST;
-                let accumulator = (obj.accumulator !== undefined) ? obj.accumulator : 
GObject.AccumulatorType.NONE;
-                let rtype = (obj.return_type !== undefined) ? obj.return_type : GObject.TYPE_NONE;
-                let paramtypes = (obj.param_types !== undefined) ? obj.param_types : [];
-
-                try {
-                    obj.signal_id = Gi.signal_new(this.$gtype, signalName, flags, accumulator, rtype, 
paramtypes);
-                } catch(e) {
-                    throw new TypeError('Invalid signal ' + signalName + ': ' + e.message);
-                }
-            }
-        }
+        if (signals)
+            _createSignals(this.$gtype, signals);
 
        let propertyObj = { };
        Object.getOwnPropertyNames(params).forEach(function(name) {
@@ -98,11 +124,7 @@ const GObjectMeta = new Lang.Class({
             throw new TypeError("Classes require an explicit 'Name' parameter.");
         let name = params.Name;
 
-        let gtypename;
-        if (params.GTypeName)
-            gtypename = params.GTypeName;
-        else
-            gtypename = 'Gjs_' + params.Name;
+        let gtypename = _createGTypeName(params.Name, params.GTypeName);
 
         if (!params.Extends)
             params.Extends = GObject.Object;
@@ -112,18 +134,11 @@ const GObjectMeta = new Lang.Class({
             throw new TypeError('GObject.Class used with invalid base class (is ' + parent + ')');
 
         let interfaces = params.Implements || [];
-        delete params.Implements;
-        let gobjectInterfaces = interfaces.filter((iface) => iface.hasOwnProperty('$gtype'));
+        let gobjectInterfaces = _getGObjectInterfaces(interfaces);
 
-        let properties = params.Properties;
+        let propertiesArray = _propertiesAsArray(params.Properties);
         delete params.Properties;
 
-       let propertiesArray = [];
-        if (properties) {
-            for (let prop in properties) {
-               propertiesArray.push(properties[prop]);
-            }
-        }
         let newClass = Gi.register_type(parent.prototype, gtypename,
             gobjectInterfaces, propertiesArray);
 
@@ -154,6 +169,61 @@ const GObjectMeta = new Lang.Class({
     }
 });
 
+function GObjectInterface(params) {
+    return this._construct.apply(this, arguments);
+}
+
+GObjectMeta.MetaInterface = GObjectInterface;
+
+GObjectInterface.__super__ = Lang.Interface;
+GObjectInterface.prototype = Object.create(Lang.Interface.prototype);
+GObjectInterface.prototype.constructor = GObjectInterface;
+GObjectInterface.prototype.__name__ = 'GObjectInterface';
+
+GObjectInterface.prototype._construct = function (params) {
+    if (!params.Name) {
+        throw new TypeError("Interfaces require an explicit 'Name' parameter.");
+    }
+
+    let gtypename = _createGTypeName(params.Name, params.GTypeName);
+    delete params.GTypeName;
+
+    let interfaces = params.Requires || [];
+    let gobjectInterfaces = _getGObjectInterfaces(interfaces);
+
+    let properties = _propertiesAsArray(params.Properties);
+    delete params.Properties;
+
+    let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
+        properties);
+
+    // See Class.prototype._construct in lang.js for the reasoning
+    // behind this direct __proto__ set.
+    newInterface.__proto__ = this.constructor.prototype;
+    newInterface.__super__ = GObjectInterface;
+    newInterface.prototype.constructor = newInterface;
+
+    newInterface._init.apply(newInterface, arguments);
+
+    Object.defineProperty(newInterface.prototype, '__metaclass__', {
+        writable: false,
+        configurable: false,
+        enumerable: false,
+        value: this.constructor
+    });
+
+    return newInterface;
+};
+
+GObjectInterface.prototype._init = function (params) {
+    let signals = params.Signals;
+    delete params.Signals;
+
+    Lang.Interface.prototype._init.call(this, params);
+
+    _createSignals(this.$gtype, signals);
+};
+
 function _init() {
 
     GObject = this;
@@ -302,6 +372,7 @@ function _init() {
 
 
     this.Class = GObjectMeta;
+    this.Interface = GObjectInterface;
     this.Object.prototype.__metaclass__ = this.Class;
 
     // For compatibility with Lang.Class... we need a _construct



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]