Introduction
peel is the project providing modern C++ bindings1 for GObject-based libraries, including GTK and GStreamer.
peel is designed to:
-
Integrate deeply with the GObject type system, supporting and faithfully exposing GObject features such as per-type instance and class initializers, properties, dynamic instantiation, and non-Object-derived types.
This notably enables modern GTK patterns such as composite templates, list models, and expressions.
-
Have no runtime overhead compared to the equivalent code in plain C. peel follows the zero-overhead principle, which states:
What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
peel’s C++ wrappers are completely optimized away by the compiler, and the produced machine code is equivalent2 to what you’d get if you implemented the same logic in plain C.
-
Be easy to set up and use on a wide range of systems, including Windows. peel only requires C++11 (though there is some support for C++20 coroutines). peel’s bindings generator is written in Python with no external dependencies. There is some integration with CMake and Meson build systems, and peel can be easily pulled into your project as a Meson subproject.
-
Have very complete and up-to-date API coverage.
The majority of the wrapper code is generated automatically based on GObject introspection data (GIR), and peel has rather good support for various GIR features, including some less used ones. This means that it’s easy to get peel working with pretty much any GObject-based library3, and that new features (like new libadwaita widgets) can be used with peel immediately when they appear in the upstream library—without having to wait for updated bindings to be uploaded to some sort of a central binding repository.
-
“modern” here relates to “GObject bindings”, not “C++” ↩
-
or at times better, when we’re able to take advantage of C++ features such as the strong type system to do things more efficiently than plain C ↩
-
if a part of your project is implemented as an internal library, you should be able to use that with peel, too ↩
Build Setup
Running peel-gen
TL;DR:
peel-gen --recursive Gtk 4.0
Most of the C++ code which makes up the peel bindings is not manually written, but gets automatically generated from GIR, the machine-readable XML-based API description format.
The generated bindings are not shipped along with peel. Rather, you should
generate bindings yourself. You could generate the bindings once, and then
check them into a source control repository. However, we recommend you to
instead generate the bindings right at your project’s build time, as a part of
your project’s build system if you use one. This way, you get bindings
tailored for your specfic environment you’re building in, such as the specific
version of GTK that you have, and the system-dependent definitions such as
GPid and goffset.
To generate the bindings, you should, first of all, obtain the relevant GIR
files for the libraries you’re going to use; for example, the file describing
GTK 4 is named Gtk-4.0.gir. Typically, on a Linux distribution, these files
are part of the relevant development packages, along with the C headers. For
instance, the GIR file for GTK is contained in the libgtk-4-dev package on
Debian, and in the gtk4-devel package on Fedora. Typically, system-wide GIR
files are installed into /usr/share/gir-1.0/.
Secondly, you need the peel-gen executable, which is the peel bindings
generator. It is implemented in Python 3 with no external dependencies (same
as Meson). It can be run directly from the peel source repository
(peel-gen.py, this is known as running peel-gen “uninstalled”), or as
peel-gen command if peel is installed.
Using peel with CMake
Many C++ projects are built with CMake. peel provides a CMake package to make it easier to use peel in CMake projects.
Start by importing the peel CMake package:
# CMakeLists.txt
find_package (peel REQUIRED)
This will look for the file named peel-config.cmake, which peel installs into
$libdir/cmake/peel/ (e.g. /usr/lib64/cmake/peel/peel-config.cmake). If you
have peel installed into a non-standard prefix, you can tell CMake to look
there by passing it as CMAKE_PREFIX_PATH value at project configuration time.
See the CMake Using Dependencies Guide for more details about how
find_package can be used, and where CMake looks for packages.
The peel CMake package defines the peel::peel imported target, which you
can use when defining another target to add peel’s own (non-generated) header
directory to the include path:
find_package (peel REQUIRED)
add_executable (my-app main.cpp)
target_link_libraries (my-app PRIVATE peel::peel)
To run the bindings generator, use the peel_generate function:
find_package (peel REQUIRED)
peel_generate (Gtk 4.0 RECURSIVE)
This function will, at build time, create a peel-generated directory inside
the current build directory and run the peel generator to generate bindings
inside of it. The function defines an imported target which is named same as
the first argument (the GIR repo name, so in this case, peel::Gtk); this
imported target can be linked into another target to add the peel-generated
directory to the include path, as well as to ensure a proper dependency edge
between generating bindings and compiling the dependent target.
Note that the peel imported target does not, by itself, link to the actual library you’re generating bindings for; you still need to do that yourself. So a complete working example of using peel with GTK 4 might look like this:
# Find GTK 4 using pkg-config:
find_package (PkgConfig)
pkg_check_modules (gtk4 IMPORTED_TARGET gtk4 REQUIRED)
# Find peel and generate bindings for GTK 4:
find_package (peel REQUIRED)
peel_generate (Gtk 4.0 RECURSIVE)
add_executable (my-app main.cpp)
# Build and link the executable with GTK 4,
# as well as peel bindings for GTK 4:
target_link_libraries (my-app PRIVATE PkgConfig::gtk4 peel::Gtk)
See examples/cmake-project in the peel repository for a small, yet
complete example of a CMake project that uses GTK 4 with peel.
Using peel with Meson
Many projects in the GLib and GNOME ecosystem are built with Meson. peel itself is built with Meson as well, and provides some integration to make it easier to use peel in Meson projects.
Start by looking up the peel dependency:
# meson.build
peel = dependency('peel')
The dependency can be installed system-wide, or provided by a subproject,
perhaps coming from a wrap file. Specifically, to make peel just work as a
wrap-based subproject, place the following into subprojects/peel.wrap:
[wrap-git]
url=https://gitlab.gnome.org/bugaevc/peel.git
revision=HEAD
depth=1
[provide]
program_names=peel-gen
Meson famously doesn’t allow build scripts to define custom functions1, which is why it’s not possible for peel to provide the same level of automation for Meson as it does for CMake, particularly around code generation.
So to generate the bindings, we have to find the peel-gen program and define
a custom target manually:
peel_gen = find_program('peel-gen')
peel_gtk = custom_target('peel-codegen',
command: [
peel_gen,
'--recursive',
'--out-dir', '@OUTDIR@',
'Gtk', '4.0',
],
output: 'peel',
)
output: 'peel' here refers to the fact that the peel-gen invocation
generates a peel/ directory.
You can then use the generated bindings as sources in your target, while
passing the peel dependency object as a dependency:
gtk = dependency('gtk4')
executable('example',
'example.cpp',
peel_gtk,
dependencies: [gtk, peel],
)
Note that including the generated bindings as sources into your target does not, by itself, cause your target to depend on the actual library you’re generating bindings for; you still need to do that yourself.
A complete example:
project('example', 'cpp')
gtk = dependency('gtk4')
peel = dependency('peel')
peel_gen = find_program('peel-gen')
peel_gtk = custom_target('peel-codegen',
command: [
peel_gen,
'--recursive',
'--out-dir', '@OUTDIR@',
'Gtk', '4.0',
],
output: 'peel',
)
executable('example',
'example.cpp',
peel_gtk,
dependencies: [gtk, peel],
)
Using peel with Flatpak
Many applications in the GNOME ecosystem are built and packaged using
Flatpak. Flatpak comes with a flatpak-builder tool, which builds an
application along with any of its bundled dependencies inside a Flatpak build
environment, according to a manifest.
To use peel in a your app, add peel to the list of modules in your manifest:
{
"name": "peel",
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/bugaevc/peel.git",
"branch": "main"
}
],
"cleanup": ["*"]
}
This goes before your app itself. So your overall manifest might look somewhat like this:
{
"id": "org.example.MyApp",
"runtime": "org.gnome.Platform",
...
"modules": [
{
"name": "peel",
...
},
{
"name": "MyApp",
...
}
]
}
Note that when using Meson to build your app, you should, preferably, still include a wrap file into your repository, as described in Using peel with Meson. This enables your app to be transparently built both inside and outside of Flatpak.
The Basics
Most GObject APIs are mapped straightforwardly into C++. GObject classes are mapped to C++ classes, and their methods are mapped to C++ methods.
The following is an example of invoking the Gtk::Button::set_label and
Gtk::Widget::set_valign methods on a button:
#include <peel/Gtk/Gtk.h>
using namespace peel;
Gtk::Button *button = /* ... */;
button->set_label ("My Button");
button->set_valign (Gtk::Align::CENTER);
GObject constructors are mapped to C++ static factory methods, with the “new”
part of a constructor name replaced with “create” (because a bare new is a
C++ keyword):
#include <peel/Gtk/Gtk.h>
using namespace peel;
auto window = Gtk::Window::create ();
window->set_title ("My Window");
auto button = Gtk::Button::create_with_label ("My Button");
To connect to a GObject signal, use the corresponding
.connect_signal_name () method,
which you can pass a C++ lambda to:
#include <peel/Gtk/Gtk.h>
using namespace peel;
auto button = Gtk::Button::create_with_label ("Click me");
int times_clicked = 0;
/* Connect a handler to the "clicked" signal of the button */
button->connect_clicked ([×_clicked] (Gtk::Button *button)
{
g_print ("You clicked the button %d times!\n", ++times_clicked);
});
The following is a complete working example of a tiny application:
#include <peel/Gtk/Gtk.h>
#include <peel/GLib/GLib.h>
using namespace peel;
int
main ()
{
Gtk::init ();
auto window = Gtk::Window::create ();
window->set_title ("Example");
auto button = Gtk::Button::create_with_label ("Click me");
int times_clicked = 0;
button->connect_clicked ([×_clicked] (Gtk::Button *button)
{
g_print ("You clicked the button %d times!\n", ++times_clicked);
});
window->set_child (std::move (button));
window->present ();
auto main_loop = GLib::MainLoop::create (nullptr, false);
main_loop->run ();
return 0;
}
Value
The GObject::Value (aliased as just peel::Value for short) shows up a lot
less in C++ code using peel compared to the frequency of GValue in plain C
code (this is in no small part because of peel implementing the
get_property and set_property vfuncs automatically). Still, it plays an
important role in various APIs.
To create a new value, define a variable of type Value (on the stack) and
initialize it by passing the type that it’s going to hold:
#include <peel/GObject/Value.h>
using namespace peel;
Value value { Type::of<int> () };
To get and set the value, use the method templates get and set:
#include <peel/GObject/Value.h>
using namespace peel;
Value value { Type::of<int> () };
value->set<int> (42);
int fourty_two = value->get<int> ();
The template type argument used here
Signals
GObject signals are a system1 for registering handlers for specific events.
In peel, any type of callable can be a signal handler. The first argument of a handler is a pointer to the object emitting the signal. The return type and any other arguments of the handler are defined by the signal.
Note
The type of the handler’s first argument is the type that defined the signal. For example, if the signal was defined in a base class and you connect to an instance of a derived class, the first argument of the handler is a pointer to a base class instance. This pointer can be
castto the derived class.For example, the
clickedsignal is defined onGtk::Button. Even thoughGtk::LinkButtoninherits the signal, the first argument of any handler toclickedis aGtk::Button *.#include <peel/Gtk/Gtk.h> #include <peel/signal.h> using namespace peel; RefPtr<Gtk::LinkButton> link_button = Object::create<Gtk::LinkButton> (); link_button->set_uri ("https://bugaevc.pages.gitlab.gnome.org/peel/"); link_button->set_label ("peel documentation"); link_button->connect_clicked( [] (Gtk::Button *button) { g_print ("The link was visited.\n"); button->cast<Gtk::LinkButton> ()->set_visited (true); });
Handlers are connected to signals by calling
.connect_signal_name () on the object emitting the signal.
There are two different ways to call .connect_signal_name ():
- Pass a GObject and member function.
- Pass a C++ callable (like a lambda or function pointer).
Using a member function is the recommended way because your handler will automatically be disconnected when it is finalized.
The result of .connect_signal_name () can be assigned to a
SignalConnection which can be used to disconnect or block the handler.
You can define your own signals in a custom GObject. Read the page on defining signals for details.
Connecting GObject member functions
If your signal is handled by a custom GObject class, this is the recommended way to connect to the signal.
Define a member function on your class, and then call
.connect_signal_name () passing in
- a pointer to the class instance,
- a pointer to the member function that handles the signal.
This example of a window with a button and a label, shows how to use a member function as a signal handler:
#include <peel/Gtk/Gtk.h>
#include <peel/GLib/MainContext.h>
#include <peel/class.h>
#include <cstring>
using namespace peel;
class MyWindow final : public Gtk::Window
{
PEEL_SIMPLE_CLASS (MyWindow, Gtk::Window)
RefPtr<Gtk::Button> m_button;
RefPtr<Gtk::Label> m_label;
int m_times_clicked;
void
init (Class *);
void
vfunc_dispose ();
void
on_button_clicked (Gtk::Button *);
};
/* ... */
PEEL_CLASS_IMPL (MyWindow, "MyWindow", Gtk::Window)
void
MyWindow::Class::init ()
{
override_vfunc_dispose<MyWindow> ();
}
void
MyWindow::vfunc_dispose ()
{
m_button = nullptr;
m_label = nullptr;
parent_vfunc_dispose<MyWindow> ();
}
void
MyWindow::on_button_clicked (Gtk::Button *)
{
char label_text[64];
snprintf (label_text, sizeof (label_text),
"The button was clicked %d times", ++m_times_clicked);
m_label->set_label (label_text);
}
void
MyWindow::init (Class *)
{
FloatPtr<Gtk::Box> box = Gtk::Box::create (Gtk::Orientation::VERTICAL, 6);
m_button = Gtk::Button::create_with_label ("Click me");
m_label = Gtk::Label::create ("The button was clicked 0 times.");
box->append (m_label);
box->append (m_button);
set_child (std::move (box));
/* Allocate the button and label ... */
m_button->connect_clicked (this, &MyWindow::on_button_clicked);
}
int
main ()
{
Gtk::init ();
bool should_stop = false;
MyWindow *window = Object::create<MyWindow> ();
window->connect_destroy (
[&should_stop] (Gtk::Widget *)
{
should_stop = true;
});
window->present ();
GLib::MainContext *main_context = GLib::MainContext::default_ ();
while (!should_stop)
main_context->iteration (true);
}
This method only works when the class is derived from GObject::Object.
Ordinary C++ classes and classes derived from other types in the GObject type
system such as Dex::Object won’t work.
Connecting using member functions is recommended for two lifetime safety reasons:
- The signal runtime will keep the object alive while the handler is running by adding an additional reference before the handler is called.
- The signal handler is automatically disconnected when the object is destructed, preventing use-after-frees.
Connecting C++ callables
A signal handler can be any C++ callable (any object with an
operator ()). This includes lambdas, plain functions, std::function, etc.
Although handlers in peel do not include gpointer user_data as their last
argument, a lambda can capture any variables you want the handler to access.
The lambda can capture a variable of any size and can capture multiple variables.
Here is the same example from previous section using a lambda instead:
#include <peel/Gtk/Gtk.h>
#include <peel/GLib/MainContext.h>
#include <peel/class.h>
#include <cstring>
using namespace peel;
int
main ()
{
Gtk::init ();
FloatPtr<Gtk::Box> box = Gtk::Box::create (Gtk::Orientation::VERTICAL, 6);
RefPtr<Gtk::Button> button = Gtk::Button::create_with_label ("Click me");
RefPtr<Gtk::Label> label = Gtk::Label::create ("The button was clicked 0 times.");
box->append (label);
box->append (button);
bool should_stop = false;
Gtk::Window *window = Gtk::Window::create ();
window->set_child (std::move (box));
button->connect_clicked (
[label, count = 0] (Gtk::Button *) mutable
{
char label_text[64];
snprintf (label_text, sizeof(label_text),
"The button was clicked %d times", ++count);
label->set_label (label_text);
});
window->connect_destroy (
[&should_stop] (Gtk::Widget *)
{
should_stop = true;
});
window->present ();
GLib::MainContext *main_context = GLib::MainContext::default_ ();
while (!should_stop)
main_context->iteration (true);
}
Warning
Signal handlers connected this way do not automatically disconnect when a capture variable is destructed. This can cause use-after-free vulnerabilities if the object emitting the signal outlasts the lambda capture 2.
To avoid the possibility of a use-after-free you can either:
- Use a member function as the handler instead.
- Manually disconnect the signal handler using a
SignalConnection.
SignalConnection
A SignalConnection is used to disconnect a signal handler
either manually or automatically when it is destructed.
Before a SignalConnection can be used, it must be assigned
to the result of a .connect_signal_name () call.
SignalConnection connection = button->connect_clicked (some_function);
Disconnecting a handler
The SignalConnection will automatically disconnect the signal handler
when the SignalConnection is destructed. This makes it very convenient to
have a SignalConnection as a member variable of a custom GObject. When
the object is finalized, the handler is disconnected.
You can also manually disconnect the handler by calling .disconnect ()
on the SignalConnection.
Warning
If you both:
- connect to a signal using a GObject and member function,
- save that connection to a
SignalConnectionmember variable,then, there are two mechanisms to disconnect the handler when the object destructs. One will disconnect the handler, and the other will try to disconnect an already disconnected handler. This is an error, so a critical warning is emitted reading:
g_signal_handler_disconnect: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failedTo work around this issue, manually disconnect the handler in the object’s
disposefunction.class MyObject final : public Object { PEEL_SIMPLE_CLASS (MyObject, Object) SignalConnection m_connection; RefPtr<Gtk::Button> m_button; inline void on_clicked (Gtk::Button *); inline void init (Class *); } inline void MyObject::init (Class *) { m_button = Object::create<Gtk::Button> (); m_connection = m_button->connect_clicked (this, &MyObject::on_clicked); } inline void MyObject::vfunc_dispose () { /* Disconnecting manually avoids a double disconnect. */ m_connection.disconnect (); parent_vfunc_dispose<MyObject> (); }
Blocking a handler
Blocking temporarily stops a handler from running. This is commonly useful when you programmatically want to set a property on an object, but you don’t want your handler to run because of the change.
To block a handler, call .block () on your SignalConnection.
This returns a SignalBlockGuard.
When the SignalBlockGuard destructs, or when .unblock () is called on it,
the handler is unblocked.
It is recommended to block the handler in a syntatic block (inside a { })
so that the handler is automatically unblocked at the end of the block.
The SignalBlockGuard returned from .block () must be stored in a variable.
Otherwise, it will immediately destruct and unblock.
This example shows how to change the value of a spin button without invoking a value-changed handler:
#include <peel/Gtk/Gtk.h>
#include <peel/signal.h>
using namespace peel;
inline void
some_function (Gtk::SpinButton *spin_button)
{
g_print ("The value of the spin button is %d.\n",
spin_button->get_value_as_int ());
}
RefPtr<Gtk::SpinButton> spin_button = /* ... */;
SignalConnection connection = spin_button->connect_value_changed (some_function);
{
SignalBlockGuard guard = connection.block ();
spin_button->set_value (42.0); /* some_function is not called. */
} /* The handler is unblocked when guard is destructed. */
Detailed signals
Detailed signals allow handlers to selectively respond to some events but not
others. When connecting to a detailed signal, a handler can optionally be
associated with a detail. When the signal is emitted, a detail
is also emitted, and the handler will only run if the two details match.
In peel, detailed signals support passing in the detail as the first argument
to .connect_signal_name (). The type of the detail
depends on the signal.
The most common detailed signal is GObject::notify. It is emitted when a
property changes on a GObject. The detail argument is a Property<T>
representing the property which changed.
This example shows how to track when the selected entry of a Gtk::DropDown changes:
#include <peel/Gtk/Gtk.h>
#include <peel/signal.h>
#include <peel/GObject/Object.h>
#include <peel/GObject/ParamSpec.h>
using namespace peel;
RefPtr<Gtk::DropDown> dropdown = /* ... */;
dropdown->connect_notify (
Gtk::DropDown::prop_selected (), /* Only run the callback when the */
/* property named "selected" changes. */
[] (Object *obj, peel::GObject::ParamSpec *pspec)
{
int selected = obj->cast<Gtk::DropDown> ()->get_selected ();
g_print ("The index of the selected item is %d.\n", selected);
});
Dynamic signal connection
Dynamic signal connection allows you to connect to a signal by its name.
You call .connect_signal () passing in the name
of the signal as a string in kebab-case and the handler.
#include <peel/Gtk/Gtk.h>
#include <peel/GLib/MainContext.h>
#include <peel/class.h>
RefPtr<Gtk::Button> button = /* ... */;
button->connect_signal ("clicked",
[] (Gtk::Button *)
{
g_print ("The button was clicked.\n");
});
Passing a misspelled signal name or a handler with an incorrect signature
will cause runtime errors rather than compile time errors. Unless you have a
specific reason to use .connect_signal (), you should use the static
.connect_signal_name ()functions instead.
-
GObject signals are similar to Qt signals and slots. ↩
-
Michael Catanzaro’s blog post titled “Common GLib Programming Errors” explains more about this problem. ↩
RefPtr
Among peel smart pointer types, RefPtr<T> is the one that you’ll encounter
early on and most often.
A RefPtr<T> owns a single reference to an instance of a reference-counted
type (most commonly a subclass of GObject::Object). A RefPtr, while it
exists, will keep the object alive, and once all RefPtrs that refer to an
object (and any other references to the same object) go out of scope, the
object will be destroyed.
A RefPtr can be made from a nullptr, or a plain pointer to an object:
#include <peel/Gtk/Gtk.h>
using namespace peel;
Gtk::Button *my_button = /* ... */;
RefPtr<Gtk::Widget> widget = nullptr;
/* Keep an additional reference on my_button */
widget = my_button;
A RefPtr can be dereferenced with -> just like a plain pointer:
#include <peel/Gtk/Gtk.h>
using namespace peel;
RefPtr<Gtk::Button> button = /* ... */;
button->set_label ("My Button");
RefPtr compared to std::shared_ptr
RefPtr is in many ways similar to std::shared_ptr. There is however an
important difference:
std::shared_ptradds its own layer of reference counting around a type that itself doesn’t implement reference-counting (so it is possible to usestd::shared_ptr<int>for example);RefPtrexpects the object type to implement its own reference counting mechanism (forGObject::Objectsubclasses, that isg_object_refandg_object_unref), and wraps that mechanism into a C++ smart pointer type.
This also enables RefPtr to be constructed directly from a plain pointer,
without needing something like std::enable_shared_from_this.
Using RefPtr
The RefPtr class has reasonable implementations of various members,
including copy and move constructors and assignment operators, that manage the
reference count of the referenced object as expected. This means that various
operations involving plain pointers and RefPtrs (and other peel pointer
types like FloatPtr and WeakPtr) “just work” and manage the reference
count of the object automatically and correctly.
For example, assume there are two functions, one of which accepts an object by
a plain pointer, and another by RefPtr. You can just call both of them
whether you have a plain pointer or a RefPtr:
#include <peel/RefPtr.h>
using namespace peel;
void takes_plain_ptr (Object *object);
void takes_ref_ptr (RefPtr<Object> object);
Object *my_plain_ptr = /* ... */;
RefPtr<Object> my_ref_ptr = /* ... */;
/* All of these work: */
takes_plain_ptr (my_plain_ptr);
takes_plain_ptr (my_ref_ptr);
takes_ref_ptr (my_plain_ptr);
takes_ref_ptr (my_ref_ptr);
takes_ref_ptr (std::move (my_ref_ptr));
When a RefPtr is used on an API boundary, meaning it is passed as an argument
to a function or returned from one, it signifies transfer of ownership, also
known as trasnfer full in GObject world. For instance,
Gio::ListModel::get_object returns an owned reference to an object, and has
the following signature in peel:
RefPtr<GObject::Object>
Gio::ListModel::get_object (unsigned position) noexcept;
Sometimes, it makes sense to create temporary RefPtrs in order to make sure
an object is kept alive over some manipulation. For example, here’s how you
would move a button between two boxes:
#include <peel/Gtk/Gtk.h>
using namespace peel;
Gtk::Box *box1 = /* ... */;
Gtk::Box *box2 = /* ... */;
Gtk::Button *button = /* ... */;
/* button is a child of box1, to be moved into box2 */
RefPtr<Gtk::Button> button_ref = button;
box1->remove (button);
/* box1 has dropped its reference on button here! */
box2->append (button);
/* box2 has added a reference on button,
* so our RefPtr can be safely dropped.
*/
A much more common pattern is keeping a RefPtr as a member variable in a
class, in order to make sure that the referenced object stays alive for as
long as an instance of this class does:
#include <peel/Gtk/Gtk.h>
#include <peel/class.h>
using namespace peel;
class MyWidget : public Gtk::Widget
{
/* ... */
private:
RefPtr<Gio::ListModel> model;
RefPtr<Gtk::Adjustment> h_adjustment, v_adjustment;
};
Copy and move semantics
Note that copy and move semantics apply to RefPtr as usual in C++. Passing a
RefPtr into a function by copy, like here:
void takes_ref_ptr (RefPtr<Object> object);
RefPtr<Object> my_ref_ptr = /* ... */;
takes_ref_ptr (my_ref_ptr);
will result in:
- A fresh new instance of
RefPtrbeing created for the function argument, initialized by copying the existingmy_ref_ptrinstance. This increments the reference count of the object by callingg_object_refon it. - The function gets called, and consumes the temporary
RefPtr. - Later, the original
RefPtris deallocated, and its destructor decrements the reference count of the object by callingg_object_unrefon it.
This may be what you wanted if you plan to keep using the object (and the
RefPtr) after the function returns. On the other hand, if you pass the
RefPtr “by move” (typically using std::move), like this:
void takes_ref_ptr (RefPtr<Object> object);
RefPtr<Object> my_ref_ptr = /* ... */;
takes_ref_ptr (std::move (my_ref_ptr));
the temporary RefPtr for the argument will “steal” the reference from
my_ref_ptr. As the result, my_ref_ptr will be set to nullptr after the
call, and there will be no extra g_object_ref/g_object_unref calls at
runtime.
Casting
Upcasting means converting a pointer to a derived type into a pointer to its base type.
Upcasting should for the most part “just work” with RefPtr. In particular, it
should be possible to pass an instance of RefPtr<DerivedType> (or a plain
DerivedType * pointer) in places where an instance of RefPtr<BaseType> is
expected.
Downcasting means converting a pointer to a base type into a pointer to a derived type, when we know that the dynamic type of the object te pointer points to actually matches the derived type.
The usual way to perform downcasting in peel is with the
GObject::TypeInstance::cast method, like this:
#include <peel/Gtk/Gtk.h>
using namespace peel;
Gtk::Widget *widget = /* ... */;
/* We know that it's actually a button */
Gtk::Button *button = widget->cast<Gtk::Button> ();
When used with a RefPtr, this will dereference the RefPtr and call the
usual cast method, which will return a plain pointer. To downcast the
RefPtr itself (moving it), use the RefPtr::cast method. This method is
defined on RefPtr itself rather than on the type it references, so in order
to call it, make sure to use a dot (.) and not an arrow (->):
#include <peel/Gtk/Gtk.h>
using namespace peel;
RefPtr<Gtk::Widget> widget = /* ... */;
/* We know that it's actually a button */
RefPtr<Gtk::Button> button = std::move (widget).cast<Gtk::Button> ();
Non object-derived types
Even though RefPtr is most commonly used with types derived from
GObject::Object, it also works with other types that support GObject-style
reference counting semantics. For example:
RefPtr<Gtk::Expression>manages an instance ofGtk::Expression, usinggtk_expression_refandgtk_expression_unref;RefPtr<Gsk::RenderNode>manages an instance ofGsk::RenderNode, usinggsk_render_node_refandgsk_render_node_unref;RefPtr<GLib::Variant>manages an instance ofGLib::Variant, usingg_variant_refandg_variant_unref. This also demonstrates that a type doesn’t even necessarily have to be a “class” (aGObject::TypeInstance) to be usable withRefPtr.
Under the hood, reference counting for various types is implemented with an
extensible mechanism using the “traits” design pattern (specializations of a
RefTraits<T> template). If you’re getting compile errors mentioning
“incomplete type RefTraits<Something>”, it’s likely the case that you’re
trying to use RefPtr with some type that isn’t reference-counted. For
example, here’s what happens if you try to use RefPtr<Gdk::RGBA>:
peel/RefPtr.h: In instantiation of ‘peel::RefPtr<T>::~RefPtr() [with T = peel::Gdk::RGBA]’:
test.cpp:8:28: required from here
8 | RefPtr<Gdk::RGBA> rgba = nullptr;
| ^~~~~~~
peel/RefPtr.h:115:27: error: incomplete type ‘peel::RefTraits<peel::Gdk::RGBA, void>’ used in nested name specifier
115 | RefTraits<T>::unref (ptr);
| ~~~~~~~~~~~~~~~~~~~~^~~~~
FFI
When using plain C APIs that have no peel wrappers, or when wrapping your C++
API into a plain C interface, you may need to “bridge” between RefPtr and
plain pointers, whether transfer full or transfer none.
For transfer none pointers, just extract or assign a plain pointer using
normal means such as static_cast (or C-style cast) and operator =:
/**
* frob_button:
* @button: (transfer none): a button to frob
*/
void
frob_button (GtkButton *button);
/**
* find_button:
*
* Returns: (nullable) (transfer none) a button, or NULL if not found
*/
GtkButton *
find_button (void);
RefPtr<Gtk::Button> button = /* ... */;
GtkButton *c_button = GTK_BUTTON ((Gtk::Button *) button);
frob_button (c_button);
c_button = find_button ();
button = reinterpret_cast<Gtk::Button *> (c_button);
For trasnfer full pointers, use the RefPtr::adopt_ref static method and the
release_ref method. (Note that release_ref must be called on an rvalue,
for example the result of std::move).
/**
* consume_button:
* @button: (transfer full): a button to consume
*/
void
consume_button (GtkButton *button);
/**
* produce_button:
*
* Returns: (transfer full) a produced button
*/
GtkButton *
produce_button (void);
RefPtr<Gtk::Button> button = /* ... */;
Gtk::Button *owned_plain_button = std::move (button).release_ref ();
GtkButton *owned_c_button = GTK_BUTTON (owned_plain_button);
consume_button (owned_c_button);
owned_c_button = produce_button ();
owned_plain_button = reinterpret_cast<Gtk::Button *> (owned_c_button);
button = RefPtr<Gtk::Button>::adopt_ref (owned_plain_button);
You can even use adopt_ref and release_ref to intentionally leak a
reference owned by a RefPtr, and to intentionally make up a reference when
there is none (perhaps to recreate a reference you’ve leaked earlier).
Note that these APIs are less “safe” then typical RefPtr usage. You take
responsibility for maintaining the reference count and not messing it up.
UniquePtr
UniquePtr<T> represents a unique owned reference to an instance of type T.
When the UniquePtr goes out of scope, the instance will be deallocated.
UniquePtr is used for types that need memory management, but are not
reference-counted (for types that that are reference-counted, RefPtr is
used instead). The most common type by far it is used with is GLib::Error,
where UniquePtr will automatically call g_error_free when the pointer
goes out of scope.
UniquePtr is similar to std::unique_ptr with a custom deleter that calls
the appropriate function to properly free the referenced GObject type.
Here’s a typical example of using UniquePtr<GLib::Error> to call a function
that can potentially fail:
#include <peel/Gtk/Gtk.h>
using namespace peel;
Gtk::ColorDialog *dialog = /* ... */;
Gio::AsyncResult *res = /* ... */;
UniquePtr<GLib::Error> error;
UniquePtr<Gdk::RGBA> color = dialog->choose_rgba_finish (res, &error);
if (error)
g_print ("Failed to pick color: %s\n", error->message);
Here, Gtk::ColorDialog::choose_rgba_finish both returns an instance of
Gdk::RGBA structure allocated on the heap, and on error, sets an
out-parameter describing the error.
There’s also an array variant of UniquePtr, which is described on
its own page.
FloatPtr
WeakPtr
String
A String represents an owned UTF-8 string, allocated using GLib heap API.
When a String goes out of scope, the owned string on the heap is deallocated.
A String is nullable and therefore might not reference any string.
The underlying const char * can be accessed by casting the string to
const char *, or calling the .c_str () method.
A String can be directly created from a const char *, a nullptr, or
another String. They are also returned from functions which return an owned
string, like GLib::DateTime::format and Gtk::Editable::get_chars.
Most functions that take a string as a parameter accept unowned strings, which
in peel are represented simply with const char * (same as in C). You can
pass a string literal or a String, among other things. For example, a
Gtk::Label’s label can be set using a const char *, a String, or even
an std::string:
#include <string>
#include <peel/String.h>
#include <peel/Gtk/Label.h>
using namespace peel;
RefPtr<Gtk::Label> label = /* ... */;
label->set_label ("string literal");
String peel_string = "peel::String";
label->set_label (peel_string); // A String will implicitly convert to const char *.
std::string std_string = "std::string";
label->set_label (std_string.c_str ());
peel::String compared to std::string
String is not meant to be the canonical ultimate string type. Instead, it
should be thought of as a smart pointer to an owned char[] string on the
heap, comparable to std::unique_ptr<char[]>.
For this reason, String only provides a rather minimal API compared to
std::string. A lot more of string utilities are available in GLib, and work
on both owned and unowned strings. Examples of such utilities are
GLib::strrstr, GLib::str_has_suffix, GLib::strjoin.
Here are a few differences between String and std::string:
Stringis nullable,std::stringis not;- A default constructed
Stringis null, but a default constructedstd::stringis empty; Stringdoes not track its length and capacity unlikestd::string;Stringusesg_malloc/g_freefor allocation/deallocation whereasstd::stringusesoperator new/operator delete;Stringis in UTF-8 butstd::stringmight not be.
While neither peel nor STL depend on each other, so there’s no direct
conversion between the two string types, you can easily convert between them by
going through const char *:
#include <string>
#include <peel/String.h>
using namespace peel;
String peel_string_1 = "peel::String";
std::string std_string = peel_string_1.c_str ();
String peel_string_2 = std_string.c_str ();
ArrayRef
ArrayRef<T> references an array of a dynamic size. It is a simple non-owning
pointer that contains a base pointer of type T * and a size. ArrayRef is
analogous to C++20 std::span, or a Rust slice.
ArrayRef doesn’t do much by itself, and is mostly used on API boundaries to
make it ergonomic to pass “an array” as an argument in various ways:
#include <peel/ArrayRef.h>
using namespace peel;
void takes_int_array (ArrayRef<int> arr);
/* a C array (size deduced) */
int my_array[5] = { 1, 2, 3, 4, 5 };
takes_int_array (my_array);
/* base pointer + count */
int *my_ptr = /* ... */;
size_t my_count = /* ... */;
takes_int_array ({ my_ptr, my_count });
/* nullptr */
takes_int_array (nullptr);
/* ArrayRef instance */
ArrayRef<int> another_array = /* ... */;
takes_int_array (another_array);
To take apart an ArrayRef, getting a base pointer and a size, you can use
the data and the size methods respectively. This is also compatible with
C++17 std::data and std::size.
ArrayRef supports common C++ idioms, for example it can be indexed and
iterated over. This also means large parts of the C++ algorithm library
should work with it.
ArrayRef<Gtk::Widget *> widgets = /* ... */;
Gtk::Widget *first_widget = widgets[0];
for (Gtk::Widget *widget : widgets)
g_print ("%s\n", widget->get_name ());
ArrayRef can also be sliced, which produces a new ArrayRef referring to a
part (slice) of the original array. This is useful when working with data, for
instance here’s what the body of a read-write echo loop might look like:
Gio::InputStream *input_stream = /* ... */;
Gio::OutputStream *output_stream = /* ... */;
/* read some data */
unsigned char buffer[1024];
auto n_read = input_stream->read (buffer, nullptr, nullptr);
/* deal with errors... */
/* write back what we have read */
ArrayRef<unsigned char> to_write { buffer, n_read };
while (to_write)
{
auto n_written = output_stream->write (to_write, nullptr, nullptr);
/* deal with errors... */
to_write = to_write.slice (n_written, to_write.size () - n_written);
}
Mutability
ArrayRef<T> is a mutable version, in that it’s possible to mutate the values
of type T that it references through the ArrayRef. ArrayRef<const T> is
the constant version which does not allow mutation. This is very similar to the
difference between T * and const T *.
The following two functions from libdex demonstrate the difference:
RefPtr<Dex::Future>
Dex::input_stream_read (Gio::InputStream *self, ArrayRef<uint8_t> buffer, int io_priority) noexcept;
RefPtr<Dex::Future>
Dex::output_stream_write (Gio::OutputStream *self, ArrayRef<const uint8_t> buffer, int io_priority) noexcept;
Dex::input_stream_read’s buffer argument is a mutable array of bytes that
the call will fill with read data, whereas Dex::output_stream_write’s
buffer argument is a read-only array of bytes.
See examples/libdex.cpp in the peel repository for an example of using
ArrayRef and libdex APIs.
Fixed size arrays
ArrayRef is used for arrays whose size is only known dynamically. For
fixed-size arrays, peel instead uses C++ references to C arrays1. For
example, Gsk::BorderNode::create accepts two arrays, both of them having
fixed size of four, and the Gsk::BorderNode::get_widths getter returns the
widths:
RefPtr<Gsk::BorderNode>
Gsk::BorderNode::create (const Gsk::RoundedRect *outline, const float (&border_width)[4], const Gdk::RGBA (&border_color)[4]) noexcept;
const float
(&Gsk::BorderNode::get_widths () const noexcept)[4];
While the declaration syntax may look somewhat intimidating2, this enables you to pass a C array of a matching size in a very natural way:
Gsk::RoundedRect outline = /* ... */;
float border_width[4] = /* ... */;
Gdk::RGBA border_color[4] = /* ... */;
auto node = Gsk::BorderNode::create (&outline, border_width, border_color);
On the other hand, attempting to pass an array of a wrong size will result in a type error. (You can use an explicit cast to silence the error if you think you know what you’re doing.)
Using a returned fixed-size array is similarly straightforward:
Gsk::BorderNode *node = /* ... */;
auto &widths = node->get_widths ();
g_print ("%f\n", widths[3]);
Note the & on widths declaration; if you omit it, a local array will be
declared instead of a reference to one, and the returned array will be
immediately copied out into the local array3.
Thanks to the type being a reference to an array and not a plain pointer, the compiler should be able to emit a warning if you use an out-of-bounds index:
warning: array index 42 is past the end of the array (that has type 'const float[4]') [-Warray-bounds]
-
this is a somewhat unusual combination, leading to a farily weird syntax, and also about the only place where C++ references show up in peel APIs (other than in special members like copy constructors) ↩
-
to humans and machines alike: the peel bindings generator required significant enhancements to be able to produce this syntax ↩
-
considering that fixed-size arrays typically have a fairly small size (in this case, 4), this may be just fine too ↩
UniquePtr to array
Custom GObject Classes
While it’s possible, to an extent, to use GTK by solely combining existing classes, idiomatic GTK usage involves writing one’s own GObject classes, such as custom widgets (typically, composite widget templates). Implementing a GObject class in peel is largely the same as implementing any other C++ class, but there are some important specifics.
You should include <peel/class.h>, derive your class from a
single1 GObject base class (for example, GObject::Object
or Gtk::Widget). Unless you’re actively planning for your class to be
derivable, you should declare it final, both as a general design principle,
and because it enables small optimizations inside GLib.
Then use the PEEL_SIMPLE_CLASS (or PEEL_CLASS) macro2 as the
first thing inside the class body. The arguments to the macro are:
- the unqualified name of class itself,
- the parent class (only for
PEEL_SIMPLE_CLASS).
#include <peel/GObject/Object.h>
#include <peel/class.h>
namespace Demo
{
class Gizmo final : public peel::GObject::Object
{
PEEL_SIMPLE_CLASS (Gizmo, Object)
};
} /* namespace Demo */
In the implementation file (.cpp), use the corresponding PEEL_CLASS_IMPL
macro3. The arguments to the macro are:
- the qualified name of the class,
- GObject type name for the class, as a string4,
- the parent class.
#include "Demo/Gizmo.h"
PEEL_CLASS_IMPL (Demo::Gizmo, "DemoGizmo", peel::GObject::Object)
Alternatively, it is possible to use PEEL_CLASS_IMPL inside a namespace,
like so:
#include "Demo/Gizmo.h"
namespace Demo
{
PEEL_CLASS_IMPL (Gizmo, "DemoGizmo", peel::GObject::Object)
} /* namespace Demo */
You should then implement, at a minimum, a class initializer, which in the simplest case can be empty:
inline void
Demo::Gizmo::Class::init ()
{ }
This is enough for the class to compile. You should now be able to create an
instance of the class using Object::create:
#include "Demo/Gizmo.h"
using namespace peel;
int
main ()
{
RefPtr<Demo::Gizmo> gizmo = Object::create<Demo::Gizmo> ();
g_print ("%s\n", gizmo->get_type_name ());
}
-
similar to languages like Java, C#, and Objective-C, and unlike C++, the GObject type system supports single inheritance, as well as implementing multiple interfaces ↩
-
PEEL_SIMPLE_CLASSis peel’s counterpart ofG_DECLARE_FINAL_TYPEandQ_OBJECT↩ -
PEEL_CLASS_IMPLis peel’s counterpart ofG_DEFINE_TYPE↩ -
see Naming conventions ↩
Naming Conventions
Your class has two names: the C++ name, and the GObject type name. The GObject
type name is the one you’d see in various debug output, in the GTK inspector,
and use in Gtk.Builder files (including Blueprints).
In order for types defined by various libraries not to clash, the well
established convention is for each project to pick a namespace, and use it as
a type name prefix (among other things). Conventionally, this prefix is short,
and, should it consist of multiple words, in UpperCamelCase.
Examples of prefixes used by various projects are:
- GTK uses
Gtk - libadwaita uses
Adw - Pango uses
Pango - GStreamer uses
Gst - WebKitGTK uses
WebKit - libdex uses
Dex - libsoup uses
Soup - libportal uses
Xdp(for “XDG desktop portal”) - libdazzle uses
Dzl
Then, peel’s convention is to map these namespaces to C++ namespaces; for
instance, the GObject type name of GtkButton corresponds to
peel::Gtk::Button in peel.
We recommend that you follow these same conventions. Pick a short namespace
for your project, and use it both as a prefix for GObject type names, and as a
C++ namespace (though not under peel::). If you pick Foo for a namespace
and have a class named Frobnicator, its GObject type name should be
FooFrobnicator, a the C++ name should be Foo::Frobnicator.
In this documentation, the namespace used for examples is Demo.
Initialization
Unlike regular C++ classes, GObject classes do not have C++ constructors. Instead, they have GObject intializers.
Generally, GObject construction/initialization flow is somewhat complex, and explaining it is outside of the scope of this documentation. Please see “A gentle introduction to GObject construction” for one explanation of this process, and “Object construction” section of the GObject Tutorial for another one. This page documents the parts that are specific to using C++ and peel.
Instance initializer
When an instance of a GObject class is created, memory for the instance is
allocated and zero-initialized. Then, the instance initializer is invoked.
The instance inializer has the signature of void MyType::init (Class *) (you
can ignore the class pointer most of the time). Note that unlike a C++
constructor, an instance initializer cannot accept any parameters (other
than the class pointer); you should use properties for that use case instead.
Things you might want to do in an instance initializer of a GTK widget include:
- Explicitly initializing any fields that should not be zero-initialized;
- Setting up any child widgets;
- Setting up event controllers.
A typical implementation of an instance initializer for a widget (in this case, a window) looks like this:
#include <peel/Gtk/Gtk.h>
using namespace peel;
class MyWindow final : public Gtk::Window
{
PEEL_SIMPLE_CLASS (MyWindow, Gtk::Widget)
inline void
init (Class *);
int last_index;
};
PEEL_CLASS_IMPL (MyWindow, "MyWindow", Gtk::Window)
inline void
MyWindow::init (Class *)
{
/* fields that need initialization other than to 0 */
last_index = -1;
/* child widget */
FloatPtr<Gtk::Button> button = Gtk::Button::create_with_label ("Click me");
button->connect_clicked (this, &MyWindow::on_button_clicked);
set_child (std::move (button));
/* misc set-up */
set_default_size (500, 300);
action_set_enabled ("win.undo-close-tab", false);
}
Class initializer
The first time an instance of a class is created (or the class otherwise
referenced), the initializer for the class itself is invoked. The class
initializer has signature of void MyType::Class::init (), and is declared
automatically by the PEEL_SIMPLE_CLASS macro.
In the class initializer, you set up the class structure (the
MyType::Class) that is shared among all instances of the class and stores
per-class data. Specifically:
-
Override any relevant virtual functions;
-
Set up any signals;
-
Set up your base class, typically using its class methods.
For instance, in a GTK widget class you might want to use
set_css_nameto set the CSS name for instances of the class,set_layout_manager_typeto set the type the layout manager to be used with instances of the class, and install some actions usinginstall_action. -
Initialize your own per-class data, if any.
A typical implementation of a class initializer for a widget looks like this:
#include <peel/Gtk/Gtk.h>
using namespace peel;
namespace Demo
{
class Gizmo final : public Gtk::Widget
{
PEEL_SIMPLE_CLASS (Gizmo, Gtk::Widget)
friend class Gtk::Widget::Class;
inline void
vfunc_dispose ();
inline void
vfunc_snapshot (Gtk::Snapshot *);
};
PEEL_CLASS_IMPL (Gizmo, "DemoGizmo", Gtk::Widget)
inline void
Gizmo::Class::init ()
{
/* vfunc overrides */
override_vfunc_dispose<Gizmo> ();
override_vfunc_snapshot<Gizmo> ();
/* widget class setup */
set_css_name ("gizmo");
set_layout_manager_type (Type::of<Gtk::BinLayout> ());
install_action ("gizmo.frob", nullptr, /* ... */);
}
} /* namespace Demo */
Note that the C++ this pointer inside the class initializer refers to an
instance of the class structure, which inherits from the base type’s class
structure (here, Gizmo::Class inherits from Gtk::Widget::Class). This is
what enables usage of the concise syntax for invoking class methods, such as
Gtk::Widget::Class::set_css_name.
Constructors
Constructors (or constructor methods) in the GObject world are simply
thin convenience wrappers over creating an instance of a type using
Object::create (for Object-derived classes), while passing the values of
the most relevant properties. Conventionally, constructors are named
create, create_somehow, or
create_with_something.
For example, a constructor for a label widget might look like this:
#include <peel/Gtk/Gtk.h>
using namespace peel;
namespace Demo
{
class Label final : public Gtk::Widget
{
PEEL_SIMPLE_CLASS (Label, Gtk::Widget)
/* ... */
public:
PEEL_PROPERTY (String, text, "text")
static FloatPtr<Label>
create (const char *text);
};
FloatPtr<Label>
Label::create (const char *text)
{
return Object::create<Label> (prop_text (), text);
}
} /* namespace Demo */
Most importantly, please note that such a constructor is not a single,
canonical way to create an instance of the type. Object::create is such a
way, and the constructor methods are merely convenient wrappers over calling
Object::create. Indeed, in many cases, such as when constructing an instance
using Gtk::Builder, the constructor will not be called. For this reason, you
should avoid putting any logic into constructors.
Destruction
GObject destruction happens in two phases: dispose, then finalize. In plain C, these are both virtual functions, but peel bridges finalize to the C++ destructor. Therefore, while GObject classes in peel do not have C++ constructors, they do have C++ destructors.
Disposing
Dispose is a virtual function defined in the base GObject::Object class
(GObject::Object::dispose), and you can override it much like any other vfunc:
inline void
Gizmo::Class::init ()
{
override_vfunc_dispose<Gizmo> ();
}
inline void
Gizmo::vfunc_dispose ()
{
/* ...tear things down... */
parent_vfunc_dispose<Gizmo> ();
}
Generally, in dispose you should release (let go of, disconnect yourself
from) any other objects that your object references, so your object becomes
inert. But note that vfunc_dispose may, potentially, be invoked multiple
times for the same object, so you should not crash or misbehave if it is
invoked repeatedly.
If you’re keeping a reference to another object using RefPtr, you can just
assign nullptr to it, which will release the object if the pointer wasn’t
already null1:
class Gizmo final : public peel::GObject::Object
{
PEEL_SIMPLE_CLASS (Gizmo, Object)
inline void
vfunc_dispose ();
RefPtr<Gio::InputStream> input_stream;
};
inline void
Gizmo::vfunc_dispose ()
{
/* release the input stream, if any */
input_stream = nullptr;
parent_vfunc_dispose<Gizmo> ();
}
If your class is a GTK widget that holds another widget as a direct child (and
not as a template child), you should remember to unparent the child in
your dispose, but only do so the first time vfunc_dispose is invoked, like
this:
inline void
Gizmo::vfunc_dispose ()
{
if (child)
{
child->unparent ();
child = nullptr;
}
parent_vfunc_dispose<Gizmo> ();
}
Finalization
Eventually (typically, immediately following disposal), your object instance
will be finalized. peel bridges finalization to the C++ destructor of your
class, which generally makes things “just work”, as the destructor
automatically tears down any remaining data that your object owns, such as any
strings or vectors (or any RefPtr you forget to nullify in dispose).
Note that even though you don’t (and can’t) declare the C++ destructor
virtual, peel will properly invoke the correct destructor of the actual
dynamic type of the object, much like a virtual destructor behaves in
regular C++.
Although it’s rarely needed, you can still provide an explicit destructor using the regular C++ syntax for destructors:
class Gizmo final : public peel::GObject::Object
{
PEEL_SIMPLE_CLASS (Gizmo, Object)
protected:
~Gizmo ();
};
inline
Gizmo::~Gizmo ()
{
g_print ("Finalizing a Gizmo object\n");
/* implicitly chains up to the parent destructor here */
}
-
assigning
nullptrto aRefPtris peel’s counterpart ofg_clear_object↩
Defining Signals
You can define signals on classes
using a Signal<Instance, Ret (Args...)>.
Here is a breakdown of what each template parameter means:
Instanceis the type of the object involved in the event.Ret (Args...)is the signature of any handler connected to the signal without the firstInstance *parameter.Retis the return type of a handler. Usuallyvoid.Args...are parameters passed to handlers that inform about an event.
Here are the steps to add a Signal to a class:
- Declare a
Signalas a static member of your class. - Use the
PEEL_SIGNAL_CONNECT_METHODmacro in your class to declare the.connect_signal_name ()method. - In your class initializer, initialize the
Signalstatic member with a call toSignal::create ().
GObject signal names are in kebab-case except when appearing in a function
name. Signal::create () takes in the signal name as a string in kebab-case.
The first PEEL_SIGNAL_CONNECT_METHOD argument is the signal name in
snake_case. The second argument is the Signal static member.
When an event occurs, you can call .emit () on your
Signal, and pass the Instance * and any other parameters.
Here is an example of a custom MySwitch class that emits
a signal when switched on or off.
#include <peel/GObject/Object.h>
#include <peel/GObject/ParamSpec.h>
#include <peel/class.h>
#include <peel/signal.h>
using namespace peel;
class MySwitch final : public Object
{
PEEL_SIMPLE_CLASS (MySwitch, Object)
bool state;
/* The signature of a handler is void (MySwitch *, bool) */
static Signal<MySwitch, void (bool)> sig_switched;
public:
/* Declares the connect_switched function. */
PEEL_SIGNAL_CONNECT_METHOD (switched, sig_switched)
void
set_enabled (bool new_state);
};
/* C++ requires us to explicitly define storage for a static member. */
Signal<MySwitch, void (bool)> MySwitch::sig_switched;
PEEL_CLASS_IMPL (MySwitch, "MySwitch", Object)
void
MySwitch::Class::init ()
{
sig_switched = Signal<MySwitch, void (bool)>::create ("switched");
}
void
MySwitch::set_enabled (bool new_state)
{
/* Don't emit if the state doesn't change. */
if (new_state == state)
return;
state = new_state;
sig_switched.emit (this, new_state);
}
int
main ()
{
RefPtr<MySwitch> my_switch = Object::create<MySwitch> ();
my_switch->set_enabled (false);
my_switch->connect_switched (
[] (MySwitch *, bool is_on)
{
g_print ("The switch is %s.\n", is_on ? "on" : "off");
});
my_switch->set_enabled (true);
my_switch->set_enabled (false);
}
GTK Widget Templates
A common way to implement composite GTK widgets is by using widget templates.
To start using widget templates, you should:
- Write the template in a
.uifile (perhaps using Blueprint), - Call
Gtk::Widget::Class::set_template_from_resourcein your widget’s class initializer, - Call
Gtk::Widget::init_templatein your widget’sinit, - Call
Gtk::Widget::dispose_templatewith your widget’s type in your widget’svfunc_dispose.
Here’s how it might look:
inline void
MyWidget::Class::init ()
{
override_vfunc_dispose<MyWidget> ();
set_template_from_resource ("/org/example/my-widget.ui");
}
inline void
MyWidget::init (Class *)
{
init_template ();
}
inline void
MyWidget::vfunc_dispose ()
{
dispose_template (Type::of<MyWidget> ());
parent_vfunc_dispose<MyWidget> ();
}
Referencing objects defined in the template, as well as providing callbacks
referenced in the template in plain C is done by using the
gtk_widget_class_bind_template_child and
gtk_widget_class_bind_template_callback macros. peel doesn’t provide direct
wrappers for these macros (so it’s not possible to just write e.g.
bind_template_child () inside your Class::init). Instead, peel provides its
own analogs to them in the form of PEEL_WIDGET_TEMPLATE_BIND_CHILD and
PEEL_WIDGET_TEMPLATE_BIND_CALLBACK macros, defined in
<peel/widget-template.h>:
#include <peel/widget-template.h>
class MyWidget : public Gtk::Widget
{
/* ... */
/* template child */
Gtk::Button *button;
/* template callback */
void
button_clicked_cb (Gtk::Button *);
};
inline void
MyWidget::Class::init ()
{
override_vfunc_dispose<MyWidget> ();
set_template_from_resource ("/org/example/my-widget.ui");
PEEL_WIDGET_TEMPLATE_BIND_CHILD (MyWidget, button);
PEEL_WIDGET_TEMPLATE_BIND_CALLBACK (MyWidget, button_clicked_cb);
}
The first macro argument must be the widget class (commonly just a name, but it
could be namespaced, or referenced through a typedef). The second argument must
be a name of an instance data member (for BIND_CHILD) or a method (for
BIND_CALLBACK) with a matching type.
When using the two-argument versions of the macros, the name of the C++ member
must match the ID/name used in the template. There are also three-argument
versions that let you pass a name explicitly. This can be useful when your C++
code follows one naming convention (such as prefixing all data member names
with m_) and your .ui files follow a different one:
#include <peel/widget-template.h>
class MyWidget : public Gtk::Widget
{
/* ... */
/* template children */
Gtk::Button *m_button;
Gtk::GestureClick *m_gesture_click;
};
inline void
MyWidget::Class::init ()
{
override_vfunc_dispose<MyWidget> ();
set_template_from_resource ("/org/example/my-widget.ui");
PEEL_WIDGET_TEMPLATE_BIND_CHILD (MyWidget, m_button, "button");
PEEL_WIDGET_TEMPLATE_BIND_CHILD (MyWidget, m_gesture_click, "gesture-click");
}
Note that binding a template child only works when the data member is a
plain pointer, not a RefPtr or another smart pointer type! This is fine,
however, since each widget already keeps internal references to all child
objects bound this way.
Gotchas and Pitfalls
using namespace peel
It is often convenient to write
using namespace peel;
and refer to the APIs by their natural-sounding names such as Gtk::Button
(instead of peel::Gtk::Button). We indeed recommend that you do so, and
examples in this documentation are written assuming using namespace peel.
There is an issue with this however, in that the peel::GObject namespace has
the same name as the C GObject type (which is known as GObject::Object in
peel), so any reference to GObject in the code following
using namespace peel becomes ambiguous. This is not a huge issue by itself,
since you can just use qualified names: namely, refer to the C type as
::GObject (and peel itself takes care to always do so), and to the peel
namespace as peel::GObject.
However, while your code may do this, the C headers of libraries you include
will still refer to the C type as simply GObject. So any #include of a C
header (perhaps itself coming from a peel header) following
using namespace peel is likely to break.
So in implementation files (.cpp), you should include the headers first, and
only then write using namespace peel. And in headers, you cannot use
using namespace peel at all (at least, not in the root namespace), because
some other header can always be included after yours.
C++ Core Guidelines also warn about this:
SF.7: Don’t write
using namespaceat global scope in a header file
Capturing WeakPtr
For complex reasons, C++ currently lacks a way to reliably detect trivially relocatable types, that is, types whose values can be safely “moved” around in memory without having to invoke their move constructor and destructor. (Technically, without the standard’s blessing, only primitive types are trivially relocatable, but in practice many others are as well.)
peel sometimes uses trivial relocations when passing C++ callbacks (e.g.
lambdas) to C APIs. Specifically, if the captured data is pointer-sized (or
less) and certain other conditions are met, peel will pack the captured data
into the gpointer user_data argument of C functions, instead of making a
heap allocation and storing the captured data there. This is an important
optimization, and it matches what a C programmer would do when using the C API
directly. However, it relies on trivial relocation being valid for the
captured data.
Given that containg self-references (pointers to other parts of the same
object) is the primary reason for a type to not be trivially relocatable,
peel is usually justified in treating pointer-sized types as trivially
relocatable: indeed, there’s just no place in a pointer-sized type to contain
both the pointer and the pointee. However, there is an important type in peel
itself that is pointer-sized, yet not trivially relocatable for a different
reason, and that is WeakPtr.
This means that code that only captures a single WeakPtr in a callback
lambda is typically broken, because of this (optimistic, but in this case,
incorrect) assumption that peel makes:
#include <peel/GLib/functions.h>
using namespace peel;
Gtk::Button *button = /* ... */;
GLib::idle_add_once (
[button = WeakPtr (button)] /* <--- broken! */
{
if (!button)
return;
button->set_label ("New label");
});
The workaround is to add another capture, perhaps of a dummy variable:
#include <peel/GLib/functions.h>
using namespace peel;
Gtk::Button *button = /* ... */;
GLib::idle_add_once (
[button = WeakPtr (button), workaround = true]
{
if (!button)
return;
button->set_label ("New label");
});
Note also that Microsoft’s MSVC compiler has a bug where it might parse
references to this inside the lambda capture list (e.g.
self = WeakPtr (this)) as referring to the lambda object and not an
instance of the containing class.
Complex fields in GObject classes
GObject classes don’t have a C++ constructor, instead, they have the GObject
initializer, spelled MyType::init (Class *) in peel. Before the initializer
is run, the body of the class is initialized to zero bytes, as if with
memset. This is enough to initialize many types to their default values (and
in fact similiar to how global variables located in .bss are initialized). In
particular:
- integers get set to 0,
- booleans get set to
false, - floating-point numbers get set to 0.0,
- raw pointers get set to
nullptr, - as an additional guarantee, peel smart pointers (such as
RefPtr,WeakPtr,UniquePtr,String) get set tonullptr.
However, for more complex types (for example, std::vector), the C++
language requires the actual constructor to be invoked. This happens implicitly
when you do use a C++ constructor in your own class:
class MyClass
{
public:
MyClass ();
int m_int;
std::vector<int> m_vector;
};
MyClass::MyClass ()
: m_int (42)
/* m_vector implicitly constructed here */
{
std::cout << "My C++ constructor" << std::endl;
}
In peel, as your class doesn’t have a C++ constructor, you have to instead explicitly use placement new in your initializer to construct the values of complex types:
class MyWidget : public Gtk::Widget
{
/* ... */
int m_int;
std::vector<int> m_vector;
inline void
init (Class *);
};
inline void
MyWidget::init (Class *)
{
m_int = 42;
new (&m_vector) std::vector<int>;
}
On the other hand, peel classes do have a C++ destructor, which is mapped to
the GObject finalize vfunc. For this reason, destructing the object will
properly destruct all of its fields, invoking their C++ destructors. You don’t
have to do anything explicitly to make it happen.
Errors You May Encounter
These page lists the various error messages you could get when using peel (from
peel-gen, from your C++ compiler, or at runtime), and describes what they
mean and what to do about them.
-
RuntimeError: GIR file for
Something-42.0not foundThis happens when you try to run
peel-gento generate the bindings based on a GIR file, but it cannot find that GIR file. You should check that you do have the relevant GIR file installed (typically in/usr/share/gir-1.0/). If it’s installed in an unusual location, you may have to setGI_GIR_PATHto letpeel-gen(and other tools) know that it should look for GIR files there.In particular, on Debian 13 (trixie), they have decided to move
GLib-2.0.gir(provided by thegir1.2-glib-2.0-devpackage) from its classic location at/usr/share/gir-1.0/into an architecture-specific directory, such as/usr/lib/x86_64-linux-gnu/gir-1.0/. peel doesn’t currently have any built-in logic to handle that. So if you’re running Debian 13 or a distribution derived from it, you should setGI_GIR_PATHappropriately.
-
undefined reference to
Something::_peel_get_type()or undefined reference topeel::GObject::Type::of<Something>()These methods are implemented by the
PEEL_CLASS_IMPL,PEEL_ENUM_IMPL, andPEEL_FLAGS_IMPLmacros. So either you are forgetting to compile and link in the.cppfile containing the use of one of these macros, or you forgot to use the macro in the first place. -
no declaration matches
Something::_peel_get_type()This internal method is declared by the
PEEL_CLASS/PEEL_SIMPLE_CLASSmacros, perhaps you have forgotten to use them. -
specialization of
template<class T> static peel::GObject::Type peel::GObject::Type::of()in different namespaceC++ disallows specializing a template outside of its namespace, which means that
PEEL_ENUM_IMPLandPEEL_FLAGS_IMPLmacros cannot be used inside of your namespace. You really need to use them at the root scope. Note thatPEEL_CLASS_IMPLis implemented differently, and doesn’t have this limitation.
-
reference to
GObjectis ambiguousThis happens when you put
using namespace peelbefore including some C header.
-
g_object_something: object classSomethinghas no property namedsome-propertyThis happens when you declare that your class has a property using
PEEL_PROPERTY, but forget to define it usingdefine_properties. -
SOME_VARIANTis not a member ofpeel::Some::EnumThis happens when the compiler only sees a forward declaration of the enum, and not the full one. Make sure you’re including the relevant
<peel/Some/Enum.h>header. -
use of deleted function
peel::FloatPtr<T>::FloatPtr(const peel::FloatPtr<U>&)FloatPtrintentionally has a deleted copy constructor. Most likely, you should move the floating reference instead, usingstd::move ().
-
use of deleted function
T* peel::RefPtr<T>::operator->() &&or use of deleted functionpeel::RefPtr<T>::operator T*() &&These happen when you try to use a temporary
RefPtr(e.g. when aRefPtris returned from some method, perhaps a constructor of some type) as a plain pointer. Unfortunately, this is not supported.There is a simple workaround: just store the
RefPtrinto a variable, so it’s no longer temporary. If you’re trying to call->cast (), call.cast ()instead, so you cast theRefPtritself, and not the plain pointer it decays to. -
no matching function for call to
peel::internals::PspecTraits<Something>::PspecTraitsThis happens when you pass a wrong set of arguments to the
.prop ()call inside yourdefine_properties. The correct set of arguments to pass depends on the type of the property you’re declaring; it commonly is either:- Nothing (such as for object and boxed record properties),
- Default value (such as for string and boolean properties),
- Minimum, maximum, and default values (such as for numeric properties).
-
Invalid object type
SomethingYou may see this at runtime when trying to instantiate an object using
GtkBuilder. This may mean one of two things:-
You spelled the GObject type name of the class differently in the
.ui(or Blueprint) file and in thePEEL_CLASS_IMPLmacro. See the page on Naming Conventions. -
You forgot to ensure the type is at all registered with GObject type system.
Before you instantiate your interface from
.uior Blueprint (so, either before callingGtk::BuilderAPIs, orGtk::Widget::init_templateif using widget templates), callGObject::Type::ensure ()for the relevant types that the template uses. For example, ifDemo::Window’s template refers to a type namedDemo::Page(which would likely be spelled asDemoPageinside the template itself), its initializer might look like this:inline void Demo::Window::init (Class *) { Type::of<Demo::Page> ().ensure (); init_template (); }
-
Local copies
For out and inout parameters, we can frequently pass through the user-provided pointer directly to the C function, perhaps casting the pointer type:
peel_arg_out (1) peel_arg_out (2)
static void
foo (int *out_integer, GObject::Object **out_object) noexcept
{
::GObject **_peel_out_object = reinterpret_cast<::GObject **> (out_object);
c_foo (out_integer, _peel_out_object);
}
In some more complex cases, it is not possible to pass through the same pointer, and we have to create a local copy variable, into which we copy the “in” value of the argument (for inout params), pass the pointer to this local copy into the C function, and copy out the “out” value to user-provided pointer:
peel_arg_inout (1) peel_nonnull_args (1)
static void
bar (bool *inout_bool) noexcept
{
gboolean _peel_inout_bool = static_cast<gboolean> (*inout_bool);
c_bar (&_peel_inout_bool);
*inout_bool = !!_peel_inout_bool;
}
In the example above, we need the local copy because bool and gboolean
differ in ABI/layout.
Note that this is still zero-overhead in practice: an optimizing compiler
would not actually materialize two distinct variables of types bool and
gboolean. Instead, the inout_bool “pointer” will be a synthetic one, and
subsequent code in the caller code will branch on the gboolean value
directly.
We also use local copies for owned reference transfer:
peel_arg_out (1) peel_nonnull_args (1)
static void
baz (peel::RefPtr<GObject::Object> *out_object) noexcept
{
::GObject *_peel_out_object;
c_baz (&_peel_out_object);
*out_object = peel::RefPtr<GObject::Object>::adopt_ref (reinterpret_cast<GObject::Object *> (_peel_out_object));
}
This way, setting the value to *out_object goes through the C++ operator =,
which properly unsets the previous value, and is “less UB” compared to what
would happen if we simply casted the pointer to ::GObject ** and let the C
code overwrite it, without using a local copy. Again, the hope here is that
the compiler will see through this and merge the two variables, especially
when the *out_object points to a fresh, null-initialized RefPtr variable.
Another reason for needing to make a local copy is when the parameter is
optional, but we need to pass non-null into the C function even when null is
passed into the C++ wrapper. Notably, this happens with the GError **
argument, because peel needs to know for sure whether or not there had been an
error, because of other local copies which we only copy out when the C
function succeeds:
peel_arg_out (1) peel_arg_out (2) peel_nonnull_args (1)
static void
read_some_string (peel::String *out_string, peel::UniquePtr<GLib::Error> *error) noexcept
{
gchar *_peel_out_string;
::GError *_peel_error = nullptr;
c_read_some_string (&_peel_out_string, &_peel_error);
if (_peel_error)
{
/* An error happened, _peel_out_string contains garbage */
if (error) /* The caller wants to see the error */
*error = peel::UniquePtr<GLib::Error>::adopt_ref (reinterpret_cast<GLib::Error *> (_peel_error));
else
g_error_free (_peel_error);
}
else
{
/* No error happened, copy out _peel_out_string */
if (error)
*error = nullptr;
*out_string = peel::String::adopt_string (_peel_out_string);
}
}