GTK builds its object model on GType and GObject. GType records runtime type relationships and metadata; GObject adds reference-counted instances, properties, and signals; GIO and GTK then use those facilities for actions and widgets. C macros generate repetitive registration code, but the object and class layouts remain ordinary C structures.
Instance and Class Layout #
A classed, instantiable type has two related structures:
- The instance structure occupies memory in every object. Its first member is the parent instance.
- The class structure is shared by instances of a type. Its first member is the parent class, followed by virtual-function slots and class-wide state.
struct _Shape {
GObject parent_instance;
};
struct _ShapeClass {
GObjectClass parent_class;
double (*area)(Shape *self);
};
struct _Circle {
Shape parent_instance;
double radius;
};
struct _CircleClass {
ShapeClass parent_class;
};The parent member at offset zero lets a Circle * be viewed as a Shape *
without changing the address. The corresponding class prefix makes inherited
virtual slots available in CircleClass. GObject supports one implementation
parent; interfaces provide multiple independent contracts without embedding
multiple parent instances.
Generated cast macros such as SHAPE() combine a C cast with runtime checks in
appropriate builds. They express a registered relationship and do not make an
unrelated structure layout valid.
Declaring and Defining Types #
Public headers declare a type, while one implementation file defines its registration and private representation. A final type cannot be subclassed:
/* note-pad.h */
#define NOTE_TYPE_PAD (note_pad_get_type())
G_DECLARE_FINAL_TYPE(NotePad, note_pad, NOTE, PAD, GObject)
NotePad *note_pad_new(void);/* note-pad.c */
struct _NotePad {
GObject parent_instance;
GString *text;
};
G_DEFINE_TYPE(NotePad, note_pad, G_TYPE_OBJECT)
static void
note_pad_class_init(NotePadClass *klass)
{
/* Install properties, signals, and virtual methods here. */
}
static void
note_pad_init(NotePad *self)
{
self->text = g_string_new(NULL);
}G_DECLARE_FINAL_TYPE() declares note_pad_get_type(), the instance and class
names, cast and type-check helpers, and automatic-cleanup support derived from
the parent type. The implementation owns the complete instance layout, so
fields remain private.
A derivable type exposes the inheritance boundary:
#define DOCUMENT_TYPE (document_get_type())
G_DECLARE_DERIVABLE_TYPE(Document, document, DOCUMENT, OBJECT, GObject)
struct _DocumentClass {
GObjectClass parent_class;
gboolean (*save)(Document *self, GError **error);
gpointer padding[8];
};G_DECLARE_DERIVABLE_TYPE() defines the public instance structure containing
only the parent instance. The API author defines the class structure because
subclasses must compile against its virtual-method slots. Instance state
should remain private:
typedef struct {
char *path;
gboolean modified;
} DocumentPrivate;
G_DEFINE_TYPE_WITH_PRIVATE(Document, document, G_TYPE_OBJECT)document_get_instance_private() then locates storage whose offset GType
assigned during registration. Reserved class padding can preserve room for
future ABI additions, but adding fields to an installed public class structure
without an ABI plan remains unsafe. An implementation-only base can use
G_DEFINE_ABSTRACT_TYPE() when direct construction must be prohibited.
GType Registration and Generated Functions #
A GType is an integer identifier backed by process-wide metadata. The
lower-level static registration API accepts a parent type, a name,
GTypeInfo, and flags:
typedef struct _GTypeInfo {
guint16 class_size;
GBaseInitFunc base_init;
GBaseFinalizeFunc base_finalize;
GClassInitFunc class_init;
GClassFinalizeFunc class_finalize;
gconstpointer class_data;
guint16 instance_size;
guint16 n_preallocs;
GInstanceInitFunc instance_init;
const GTypeValueTable *value_table;
} GTypeInfo;Class and instance sizes describe allocations. Class initialization installs
virtual methods, properties, and signals once for a class; instance
initialization establishes state for every new instance. The value table
integrates non-object values with GValue. Dynamic registration supports
loadable type modules, while registering new fundamental root types is a
specialized operation rather than normal application practice.
G_DEFINE_TYPE(NotePad, note_pad, G_TYPE_OBJECT) generates the supported
note_pad_get_type() implementation and internal registration helpers. In
conceptual terms, the generated function:
- Registers
NotePadas a child ofGObjectexactly once. - Supplies
sizeof(NotePadClass)andsizeof(NotePad). - Associates
note_pad_class_init()andnote_pad_init()with the type. - Returns the stable type identifier on subsequent calls.
Names of generated internal variables are macro implementation details and
must not become API. The public contract consists of note_pad_get_type(),
declared functions, documented properties and signals, and any supported
class layout.
Interfaces use the parallel declaration and definition macros:
#define APP_TYPE_PRINTABLE (app_printable_get_type())
G_DECLARE_INTERFACE(AppPrintable, app_printable, APP, PRINTABLE, GObject)
struct _AppPrintableInterface {
GTypeInterface parent_iface;
void (*print)(AppPrintable *self);
};
G_DEFINE_INTERFACE(AppPrintable, app_printable, G_TYPE_OBJECT)
static void
app_printable_default_init(AppPrintableInterface *iface)
{
(void) iface;
}A concrete type attaches the interface during its one-time registration:
G_DEFINE_TYPE_WITH_CODE(
Report, report, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE(APP_TYPE_PRINTABLE,
report_printable_init))G_IMPLEMENT_INTERFACE() associates the implementation initializer with the
interface while Report is registered.
Signal Declaration and Emission #
Signals are registered against an instantiable type, commonly in class initialization:
enum {
CHANGED,
N_SIGNALS
};
static guint signals[N_SIGNALS];
static void
note_pad_class_init(NotePadClass *klass)
{
signals[CHANGED] =
g_signal_new("changed",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1, G_TYPE_STRING);
}The registration fixes the name, parameter types, return type, emission flags, and optional class-handler slot. Consumers connect handlers to a particular instance:
gulong handler_id =
g_signal_connect(pad, "changed", G_CALLBACK(on_changed), editor);The implementation emits on a particular instance:
g_signal_emit(pad, signals[CHANGED], 0, changed_text);Emission is synchronous on the emitting thread. Depending on signal flags, a
class handler runs before, after, or during cleanup around normal and after
handlers. Normal handlers run in connection order, but handlers can stop an
emission or connect, disconnect, block, and unblock other handlers. Signal
callbacks must therefore tolerate re-entrancy and must not be treated as an
asynchronous queue.
The emitter automatically disconnects its handlers when it is finalized.
Destruction of editor, the user-data object above, does not automatically
disconnect the handler. Safe lifetime choices are:
- Save
handler_idand disconnect it while the receiver is disposed. - Use
g_signal_connect_object()in single-threaded code so receiver destruction disconnects automatically. Upstream documentation states that this helper is not thread-safe. - Use
g_signal_connect_data()with a closure notify and an explicitly held strong reference. This can retain the receiver indefinitely if the handler is never disconnected.
Disconnecting explicitly provides the clearest ownership boundary. Holding an object reference only for the duration of a callback protects that call; it does not make concurrent mutation safe.
Actions, State, and Groups #
GAction names an operation without describing its label, icon, or widget. An
action has:
- an immutable name and parameter type;
- an enabled flag;
- an optional state with an immutable state type; and
- an optional state hint describing valid values.
Activation parameters are transient inputs. State persists and represents a
toggle or one choice from a set. A stateless "open-bookmark" action can take
a string parameter, while a "fullscreen" action can hold Boolean state. A
radio-style "justify" action commonly uses the same string type for its
activation parameter and state.
GSimpleAction is the standard implementation:
GSimpleAction *action =
g_simple_action_new_stateful("wrap",
NULL,
g_variant_new_boolean(FALSE));
g_signal_connect(action, "activate", G_CALLBACK(toggle_wrap), view);
g_action_map_add_action(G_ACTION_MAP(window), G_ACTION(action));
g_object_unref(action);For a custom state transition, connect change-state, validate the proposed
GVariant, apply the operation, and call g_simple_action_set_state().
Changing state and activating are separate operations; neither should be used
to smuggle presentation data into the action.
GActionMap supports adding, looking up, and removing actions by their own
unprefixed names. GActionGroup exposes a group for querying, activating, and
observing actions. GtkApplication and GtkApplicationWindow implement the
relevant interfaces, which makes application and window scopes direct action
maps.
GTK references scoped actions with prefixes:
app.quitresolves in the application scope.win.fullscreenresolves in the current window scope.- A group inserted with
gtk_widget_insert_action_group(widget, "view", group)supplies names such asview.wrapbelow that widget.
Widgets implementing GtkActionable, including buttons and switches, expose
action-name and action-target properties. When the widget activates, GTK
walks its widget ancestry and then the application to resolve the scoped name.
The nearest matching scope controls behavior and state. Class-wide widget
actions installed with gtk_widget_class_install_action() provide another
mechanism for actions shared by every instance of a widget class.
The resulting separation is deliberate: GType defines runtime relationships, GObject defines instances and synchronous signals, GAction defines operations, and GTK resolves those operations through widget, window, and application scopes.
Comments
Read or comment on GitHub (sign-in required to comment)