An idiom that has shown up in GTK4 development is the idea of immutable objects and builders. The idea behind an immutable object is that you can be sure that it doesn’t change under you, so you don’t need to track changes, you can expose it in your API without having to fear users of the API are gonna change that object under you, you can use it as a key when caching and last but not least you can pass it into multiple threads without requiring synchronization.
Examples of immutable objects in GTK4 are GdkCursor, GdkTexture, GdkContentFormats or GskRenderNode. An example outside of GTK would be GBytes

Sometimes these objects are easy to create using a series of constructors, but oftentimes these objects are more complex. And because those objects are immutable, we can’t just provide setters for the various properties. Instead, we provide builder objects. They are short-lived objects whose only purpose is to manage the construction of an immutable object.
Here’s an example of how that would look:

Sandwich *
make_me_a_sandwich (void)
  SandwichBuilder *builder;

  /* create builder */
  builder = sandwich_builder_new ();
  /* setup the object to create */
  sandwich_builder_set_bread (builder, white_bread);
  sandwich_builder_add_ingredient (builder, cheese);
  sandwich_builder_add_ingredient (builder, ham);
  sandwich_builder_set_toasted (builder, TRUE);
  /* free the builder, create the object and return it */
  return sandwich_builder_free_to_sandwich (builder);

This approach works well in C, but does not work when trying to make the builder accessible to bindings. Bindings need no explicit memory management, so they want to write code like this:

def make_me_a_sandwich():
    # create builder
    builder = SandwichBuilder ()
    # setup the object to create
    builder.set_bread (white_bread)
    builder.add_ingredient (cheese)
    builder.add_ingredient (ham)
    builder.set_toasted (True)
    # create the object and return it
    return builder.to_sandwich ()

We spent the hackfest arguing about how to create a C API that works for both of those use cases and the consensus so far has been to turn builders into refcounted boxed types that provide both the above APIs – but advertises the C-specific parts only to the C APIs and the binding-specific parts only to bindings. So the C header for the above examples would look like this:

SandwichBuilder *sandwich_builder_new (void);
/* (skip) in bindings */
Sandwich *sandwich_builder_free_to_sandwich (SandwichBuilder *builder) G_GNUC_WARN_UNUSED_RESULT;

/* to be used by bindings only */
GType *sandwich_builder_get_type (void) G_GNUC_CONST;
SandwichBuilder *sandwich_builder_ref (SandwichBuilder *builder);
void sandwich_builder_unref (SandwichBuilder *builder);
Sandwich sandwich_builder_to_sandwich (SandwichBuilder *builder);

/* shared API */
void sandwich_builder_set_bread (SandwichBuilder *builder, Bread *bread);
void sandwich_builder_add_ingredient (SandwichBuilder *builder, Ingredient *ingredient);
void sandwich_builder_set_toasted (SandwichBuilder *builder, gboolean should_toast);

/* and in the .c file: */
G_DEFINE_BOXED_TYPE (SandwichBuilder, sandwich_builder, sandwich_builder_ref, sandwich_builder_unref)

And now I’m off to review all our builder APIs so they conform to this idea.


#1 John on 02.03.18 at 14:29

At least for bindings, please consider making setters return the builder itself, so that calls can be chained:


#2 Kristian Høgsberg on 02.03.18 at 18:17

I’ve this pattern before in C, it’s great.

#3 Philip Chimento on 02.04.18 at 07:12

Interesting train of thought!

For comparison, I have worked on something similar:
All properties of EkncQueryObject are construct-only immutable, and to build a new object based on an old object you use eknc_query_object_new_from_object(old_object, new_props…). It’s not very bindable, we have to do an override. I’d be interested to see how the builder pattern shakes out.

Leave a Comment