Hello again! If you’re a GJS user, I’d like your opinion and ideas. After my last post where I talked about new features coming in GNOME 3.26 to GJS, GNOME’s Javascript engine, I’m happy to say that the patches are nearly ready to be landed. We just need to figure out how to build SpiderMonkey 52 consistently even though Mozilla hasn’t made an official standalone release of it yet.

A better literal depiction of JAVA SCRIPT I could not ask for… (Public domain image courtesy of Engin_Akyurt)
As I reported last time:
After that is done, I will refactor GJS’s class system (
Lang.Class
andGObject.Class
). I believe this needs to be done before GNOME 3.26. That’s because [we will] gain ES6 classes, and I want to avoid the situation where we have two competing, and possibly incompatible, ways to write classes.
That’s what I’m busy doing now, in the run-up to GUADEC later this month, and I wanted to think out loud in this blog post, and give GJS users a chance to comment.
First of all, the legacy Lang.Class
classes will continue to work. You will be able to write ES6 classes that inherit from legacy classes, so you can start using ES6 classes without refactoring all of your code at once.
That was the good news, now the bad
However, there is not an obvious way to carry over the ability to create GObject classes and interfaces from legacy classes to ES6 classes. The main problem is that Lang.Class
and its subclasses formed a metaclass framework. This was used to carry out certain activities at the time the class object itself was constructed, such as registering with the GType system.
ES6 classes don’t have a syntax for that, so we’ll have to get a bit creative. My goals are to invent something (1) that’s concise and pleasant to use, and (2) that doesn’t get in the way when classes gain more features in future ES releases; that is, not too magical. (Lang.Class
is pretty magical, but then again, there wasn’t really an alternative at the time.)
Here is how the legacy classes worked, with illustrations of all the possible bells and whistles:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MyClass = new Lang.Class({ | |
Name: 'MyClass', | |
GTypeName: 'MyNamespaceMyClass', | |
Extends: GObject.Object, | |
Implements: [Gio.Initable, MyCustomInterface], | |
Properties: { | |
'prop': GObject.ParamSpec.int( /* etc., etc. */ ), | |
}, | |
Signals: { | |
'signal': { param_types: [ /* etc., etc. */ ] }, | |
}, | |
_init(props={}) { | |
this.parent(props); | |
// etc. | |
}, | |
get prop() { /* … */ }, | |
method(arg) { /* … */ }, | |
}); |
The metaclass magic in Lang.Class
notices that the class extends GObject.Object
, and redirects the construction of the class object to GObject.Class
. There, the other magic properties such as Properties
and Signals
are processed and removed from the prototype, and it calls a C function to register the type with the GObject type system.
Without metaclasses, it’s not possible to automatically carry out magic like that at the time a class object is constructed. However, that is exactly the time when we need to register the type with GObject. So, you pretty much need to remember to call a function after the class declaration to do the registering.
The most straightforwardly translated (fictional) implementation might look something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyClass extends GObject.Object { | |
static get GTypeName { return 'MyNamespaceMyClass'; } | |
static get Implements { return [Gio.Initable, MyCustomInterface]; } | |
static get Properties { | |
return { | |
'prop': GObject.ParamSpec.int( /* etc., etc. */ ), | |
}; | |
} | |
static get Signals { | |
return { | |
'signal': { /* etc. */ }, | |
}; | |
} | |
constructor(props={}) { | |
super(props); | |
// etc. | |
} | |
get prop() { /* … */ } | |
method(arg) { /* … */ } | |
} | |
GObject.registerClass(MyClass); |
The fictional GObject.registerClass()
function would take the role of the metaclass’s constructor.
This is a step backwards in a few ways compared to the legacy classes, and very unsatisfying. ES6 classes don’t yet have syntax for fields, only properties with getters, and the resulting static get
syntax is quite unwieldy. Having to call the fictional registerClass()
function separately from the class is unpleasant, because you can easily forget it.
On the other hand, if we had decorators in the language we’d be able to make something much more satisfying. If you’re familiar with Python’s decorators, these are much the same thing: the decorator is a function which takes the object that it decorates as input, performs some action on the object, and returns it. There is a proposed decorator syntax for Javascript that allows you to decorate classes and class properties. This would be an example, with some more fictional API:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GObject.Class('MyNamespaceMyClass') | |
@GObject.implements([Gio.Initable, MyCustomInterface]) | |
@GObject.signal('signal', { /* etc. */ }) | |
class MyClass extends GObject.Object { | |
constructor(props={}) { | |
super(props); | |
// etc. | |
} | |
@GObject.property.int('Short name', 'Blurb', GObject.ParamFlags.READABLE, 42) | |
get prop() { /* etc. */ } | |
method(arg) { /* etc. */ } | |
} |
This is a lot more concise and natural, and the property decorators are similar to the equivalent in PyGObject, but unfortunately it doesn’t exist. Decorators are still only a proposal, and none of the major browser engines implement them yet. Nonetheless, we can take the above syntax as an inspiration, use a class expression, and move the registerClass()
function around it and the GObject stuff outside of it:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var MyClass = GObject.registerClass({ | |
GTypeName: 'MyNamespaceMyClass', | |
Implements: [Gio.Initable, MyInterface], | |
Properties: { 'prop': GObject.ParamSpec.int( /* etc. */ ) }, | |
Signals: { 'signal': { /* etc. */ } }, | |
}, class MyClass extends GObject.Object { | |
constructor(props={}) { | |
super(props); | |
// etc. | |
} | |
get prop() { /* … */ } | |
method(arg) { /* … */ } | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var MyInterface = GObject.registerInterface({ | |
GTypeName: 'MyNamespaceMyInterface', | |
Requires: [Gio.Initable], | |
Properties: { 'prop': GObject.ParamSpec.int( /* etc. */ ) }, | |
Signals: { 'signal': { /* etc. */ } }, | |
}, class MyInterface { | |
get prop() { /* … */ } | |
method(arg) { /* … */ } | |
}); |
Here, the body of the class is almost identical to what it would be with the decorator syntax. All the extra stuff for GObject is contained at the top of the class like it would be with the decorators. We don’t have the elegance of the property decorator, but this is quite an improvement on the first iteration. It’s not overly magical, it even acts like a decorator: it takes a class expression, and gives back a GObject-ized class. And when decorators eventually make it into standard Javascript, the basic idea is the same, so converting your code will be easy enough. (Or those who use transpiling tools can already go ahead and implement the decorator-based API.)
This is the best API I’ve been able to come up with so far. What do you think? Would you want to use it? Reply to this post or come talk to me in #javascript on GNOME IRC.
Next steps
Note first of all that none of this code exists yet. Depending on what feedback I get here, I hope to have a draft version working before GUADEC, and around the same time I’ll post a more detailed proposal to the javascript-list mailing list.
In addition, I will be speaking about this and more at GUADEC in my talk, “Modern Javascript in GNOME“. If you are attending, come talk to me there! Thanks to the GNOME Foundation for sponsoring my travel and accommodations.
Will this break existing gnome shell extensions?
No.
(Although at some point deprecation warnings will be added to nudge authors to use the new features.)
> Having to call the fictional registerClass() function separately from the class is unpleasant, because you can easily forget it.
Perhaps I’m misunderstanding the order of operations but is it not possible to have the GObject superclass automatically do the business of registerClass() so that all you need to remember is to call super(props) in the constructor? Or is the point that the properties and signals would not yet be initialized at that stage?
Also, regarding the final proposal you have, what would happen if you started declaring properties on the ES6 class itself instead of (or in addition to) declaring them – correctly – in the Properties field. Would that generate an error/warning by GObject? Because I could see people, especially those used to ES6, doing that by mistake.
> Perhaps I’m misunderstanding the order of operations but is it not possible to have the GObject superclass automatically do the business of registerClass() so that all you need to remember is to call super(props) in the constructor? Or is the point that the properties and signals would not yet be initialized at that stage?
Good question. I think it would be mostly feasible to do it that way, but not in every case. It would mean that a class only registers its type with GObject once an instance of that class is instantiated. I can see a few cases where that would break: (1) using class A as the type of a GObject.ParamSpec.object in another class B, means that constructing a B could fail depending on whether an A has already been constructed; and (2) GObject interfaces have to register their types but are never instantiated, so there would have to be some alternative mechanism for that.
I think it could be done with some workarounds, but see e.g. https://bugzilla.gnome.org/show_bug.cgi?id=779843 for an example of what might go wrong.
> what would happen if you started declaring properties on the ES6 class itself instead of (or in addition to) declaring them – correctly – in the Properties field.
You get a normal Javascript property: not typed, and no notify signal. I think there are cases where you might actually want this. It was also possible with legacy Lang.Class.
Why not just oldschool style like
class MyClass extends GObject.Object {
…
}
MyClass.GTypeName = ‘MyNamespaceMyClass’;
MyClass.Implements = [Gio.Initable, MyCustomInterface];
MyClass.Properties = {
‘prop’: GObject.ParamSpec.int( /* etc., etc. */ ),
};
MyClass.Signals = {
‘signal’: { /* etc. */ },
};
?
And those who are brave to use transpiler will write
class MyClass extends GObject.Object {
static GTypeName = ‘MyNamespaceMyClass’;
static Implements = [Gio.Initable, MyCustomInterface];
static Properties = {
‘prop’: GObject.ParamSpec.int( /* etc., etc. */ ),
};
static Signals = {
‘signal’: { /* etc. */ },
};
}
This syntax will be implemented by major JS engines pretty soon (decorators proposal depends on class fields). I think, it’s important to be future-compatible now and don’t invent syntax which will be thrown away soon.
I agree, being future-compatible is a really good point. However, I don’t want to do that at the expense of unsatisfying API in base GJS — there has to be a compelling reason for apps and extensions authors to switch to the new syntax, and most don’t have a transpiler integrated in their project yet.
I’ve been thinking some more about how to make something that will work in the future with the desired decorator API with as few changes as possible, and I think it should be possible to make the function call work as both a decorator and a regular function call, as well as with class fields and without them, for authors who do wish to use transpilers.
> at the expense of unsatisfying API in base GJS
I’d have to agree with you there. Having a scripting language like GJS available is great, but this is one of the reasons people still shudder at the word “Javascript” in general.
> invent syntax which will be thrown away soon.
Also a cringe-worthy scenario we’re all familiar with. But since GJS really is Gnome Javascript and depends on a framework like GLib to be useful on the desktop (unlike eg. Python), I don’t think some Gnome-specific, out-of-spec syntax or grammar is uncalled for, as long as it can be carried forward.
Typescript features (experimental) support for decorators, if that would be an option. See https://www.typescriptlang.org/docs/handbook/decorators.html
Yes, you can use Typescript with GJS! https://github.com/sammydre/ts-for-gjs
And I intended (but I haven’t tested) that the GObject.registerClass function is usable as a decorator already.