Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.


  1. “modern” here relates to “GObject bindings”, not “C++”

  2. 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

  3. 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.

Building 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],
)

  1. See also Primitive Recursive Functions For A Working Programmer.

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_* 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 ([&times_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 ([&times_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

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_ptr adds its own layer of reference counting around a type that itself doesn’t implement reference-counting (so it is possible to use std::shared_ptr<int> for example);
  • RefPtr expects the object type to implement its own reference counting mechanism (for GObject::Object subclasses, that is g_object_ref and g_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 RefPtr being created for the function argument, initialized by copying the existing my_ref_ptr instance. This increments the reference count of the object by calling g_object_ref on it.
  • The function gets called, and consumes the temporary RefPtr.
  • Later, the original RefPtr is deallocated, and its destructor decrements the reference count of the object by calling g_object_unref on 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:

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

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]

  1. 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)

  2. to humans and machines alike: the peel bindings generator required significant enhancements to be able to produce this syntax

  3. 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 ());
}

  1. 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

  2. PEEL_SIMPLE_CLASS is peel’s counterpart of G_DECLARE_FINAL_TYPE and Q_OBJECT

  3. PEEL_CLASS_IMPL is peel’s counterpart of G_DEFINE_TYPE

  4. 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:

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:

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 */
}

  1. assigning nullptr to a RefPtr is peel’s counterpart of g_clear_object

GTK Widget Templates

A common way to implement composite GTK widgets is by using widget templates.

To start using widget templates, you should:

  1. Write the template in a .ui file (perhaps using Blueprint),
  2. Call Gtk::Widget::Class::set_template_from_resource in your widget’s class initializer,
  3. Call Gtk::Widget::init_template in your widget’s init,
  4. Call Gtk::Widget::dispose_template with your widget’s type in your widget’s vfunc_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 namespace at 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 to nullptr.

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.0 not found

    This happens when you try to run peel-gen to 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 set GI_GIR_PATH to let peel-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 the gir1.2-glib-2.0-dev package) 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 set GI_GIR_PATH appropriately.

  • undefined reference to Something::_peel_get_type() or undefined reference to peel::GObject::Type::of<Something>()

    These methods are implemented by the PEEL_CLASS_IMPL, PEEL_ENUM_IMPL, and PEEL_FLAGS_IMPL macros. So either you are forgetting to compile and link in the .cpp file 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_CLASS macros, perhaps you have forgotten to use them.

  • specialization of template<class T> static peel::GObject::Type peel::GObject::Type::of() in different namespace

    C++ disallows specializing a template outside of its namespace, which means that PEEL_ENUM_IMPL and PEEL_FLAGS_IMPL macros cannot be used inside of your namespace. You really need to use them at the root scope. Note that PEEL_CLASS_IMPL is implemented differently, and doesn’t have this limitation.

  • reference to GObject is ambiguous

    This happens when you put using namespace peel before including some C header.

  • g_object_something: object class Something has no property named some-property

    This happens when you declare that your class has a property using PEEL_PROPERTY, but forget to define it using define_properties.

  • SOME_VARIANT is not a member of peel::Some::Enum

    This 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>&)

    FloatPtr intentionally has a deleted copy constructor. Most likely, you should move the floating reference instead, using std::move ().

  • use of deleted function T* peel::RefPtr<T>::operator->() && or use of deleted function peel::RefPtr<T>::operator T*() &&

    These happen when you try to use a temporary RefPtr (e.g. when a RefPtr is 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 RefPtr into a variable, so it’s no longer temporary. If you’re trying to call ->cast (), call .cast () instead, so you cast the RefPtr itself, and not the plain pointer it decays to.

  • no matching function for call to peel::internals::PspecTraits<Something>::PspecTraits

    This happens when you pass a wrong set of arguments to the .prop () call inside your define_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 Something

    You 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 the PEEL_CLASS_IMPL macro. 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 .ui or Blueprint (so, either before calling Gtk::Builder APIs, or Gtk::Widget::init_template if using widget templates), call GObject::Type::ensure () for the relevant types that the template uses. For example, if Demo::Window’s template refers to a type named Demo::Page (which would likely be spelled as DemoPage inside 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);
    }
}