[gjs] gobject: GObject.Interface
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs] gobject: GObject.Interface
- Date: Mon, 13 Jul 2015 21:05:51 +0000 (UTC)
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]