Skip to content
C.W.K.
Stream
Lesson 01 of 05 · published

The First-Member Trick

~12 min · c, libmypaint, struct-layout, ffi

Level 0Blank Canvas
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Three Types, One Address

libmypaint is C99. It has no classes, and yet it has a class hierarchy, and Ariandel joins that hierarchy as a subclass. The base is MyPaintSurface: a struct of function pointers (draw_dab, get_color, begin_atomic, end_atomic, destroy, save_png) plus a reference count. That struct is a hand-built virtual table. The tiled subclass, MyPaintTiledSurface, puts a whole MyPaintSurface first and then adds what tiling needs: the two tile-request callbacks, the symmetry state, an operation queue. Ariandel's ArdSurface puts a whole MyPaintTiledSurface first and then adds what Ariandel needs: a context pointer, two callbacks into Swift, two tiles of its own, and the saved round dab.

Stack those and one pointer answers to three names. The address of an ArdSurface is the address of its parent, which is the address of that struct's parent. ard_surface_base returns &surface->parent.parent, the MyPaintSurface* Swift hands to mypaint_brush_stroke_to, and numerically it is the very pointer ard_surface_create returned.

Why the Casts Are Legal C

The C standard says a pointer to a structure, suitably converted, points to its initial member, and that there is never padding before the first member. That one sentence is the entire inheritance mechanism, and every cast in the chain leans on it:

  • libmypaint's own default begin_atomic and end_atomic cast MyPaintSurface* to MyPaintTiledSurface*, and so does its draw_dab. That is the library downcasting on its own; it never learns Ariandel exists.
  • Ariandel's tile_request_start and tile_request_end receive a MyPaintTiledSurface* and cast it to ArdSurface* to reach the context and the callbacks.
  • Ariandel's surface_destroy, installed in the base's destroy slot, receives a MyPaintSurface* and casts it two levels down in one step.

This is not a trick Ariandel invented. libmypaint ships a subclass of its own, MyPaintFixedTiledSurface, built exactly this way: a MyPaintTiledSurface parent first, then its fields, including a single null tile it lends for out-of-range requests and wipes afterward. And mypaint_tiled_surface_init carries the note "Only intended to be called from subclasses". The first-member layout is the library's extension contract. Ariandel signs it rather than working around it.

Swift Never Sees the Layout

The public header declares typedef struct ArdSurface ArdSurface; and nothing more, so the struct is opaque everywhere outside its own C file. Swift imports a pointer to it as OpaquePointer, and BrushSurface stores exactly that, as handle. Swift cannot read a field, reorder one, or depend on the struct's size. The layout is C's private business, decided in one file, which is precisely where a layout rule should live.

The Rule That Breaks It

Put anything before parent and every downcast in the bridge becomes a lie. Say someone adds a field to the top of ArdSurface: a debug counter, a magic number, a lock. At first nothing notices. ard_surface_base names its field, so it still returns the right MyPaintSurface*. libmypaint's own casts to MyPaintTiledSurface* stay true, because that layout is upstream's and did not move. The brush's call through draw_dab still lands in Ariandel's replacement. The lie is one step further down: shaped_draw_dab, tile_request_start, tile_request_end and surface_destroy cast that pointer to ArdSurface*, and it now points the new field's size past where the struct really begins. Every field they read is shifted. self->acquire is whatever bytes happen to sit at that offset, the bridge calls it as a function and jumps into garbage, and free(self) frees an address calloc never returned. The compiler says nothing, because every one of those casts is explicit, and an explicit cast is you telling the compiler to stop checking.

Two quieter variants fail the same way. Embedding by pointer (MyPaintTiledSurface *parent) is a reference, not a first member. Allocating the parent separately and storing it is the same mistake with more steps. The parent has to be inside, and first.

Ariandel guards the rule with a comment on the field: "Must stay first." That comment has held, but it is a rule made of tokens, and a rule made of tokens only works when someone reads it at the right moment. The sketch below shows the structural version: a _Static_assert on offsetof(..., parent) == 0 turns a reorder into a build error instead of a jump into garbage. If you adopt the pattern, bring the assert with it.

Inheritance in C is a layout promise, not a language feature. The language gives you exactly one guarantee, that the first member sits at offset zero, and you build the hierarchy on top of it. Keep the promise in one file, keep the struct opaque to everyone else, and make the compiler check the one thing the casts cannot.

Code

Three structs, one address: libmypaint's base and tiled surfaces (verbatim) and Ariandel's ArdSurface (trimmed)·c
/* libmypaint, include/mypaint-surface.h (verbatim) */
struct MyPaintSurface {
    MyPaintSurfaceDrawDabFunction draw_dab;
    MyPaintSurfaceGetColorFunction get_color;
    MyPaintSurfaceBeginAtomicFunction begin_atomic;
    MyPaintSurfaceEndAtomicFunction end_atomic;
    MyPaintSurfaceDestroyFunction destroy;
    MyPaintSurfaceSavePngFunction save_png;
    int refcount;
};

/* libmypaint, include/mypaint-tiled-surface.h (verbatim) */
struct MyPaintTiledSurface {
    MyPaintSurface parent;
    /* private: */
    MyPaintTileRequestStartFunction tile_request_start;
    MyPaintTileRequestEndFunction tile_request_end;
    MyPaintSymmetryData symmetry_data;
    struct OperationQueue *operation_queue;
    int num_bboxes;
    int num_bboxes_dirtied;
    MyPaintRectangle *bboxes;
    MyPaintRectangle default_bboxes[NUM_BBOXES_DEFAULT];
    gboolean threadsafe_tile_requests;
    int tile_size;
};

/* Ariandel, CAriandelSurface/ariandel_surface.c (trimmed: the shape, grain, dual
   and held-tile fields that follow are left out) */
struct ArdSurface {
    /* Must stay first: libmypaint casts MyPaintSurface* to MyPaintTiledSurface*,
       and this bridge casts that to ArdSurface*. */
    MyPaintTiledSurface parent;
    void *context;
    ArdTileAcquire acquire;
    ArdTileRelease release;
    uint16_t *empty;   /* lent to readonly requests for absent tiles; never written */
    uint16_t *scratch; /* lent to a writable request the store could not satisfy */

    /* libmypaint's own round dab, used whenever no shape is set. */
    MyPaintSurfaceDrawDabFunction round_dab;
    /* ... */
};

MyPaintSurface *
ard_surface_base(ArdSurface *surface)
{
    return &surface->parent.parent;
}
A minimal sketch: first-member embedding with the layout checked at compile time (compiled and run with Apple clang, -std=c11)·c
// A minimal sketch of first-member embedding, the shape ArdSurface uses.
// clang -std=c11 -Wall -Wextra first_member.c -o first_member && ./first_member
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct Surface Surface;
struct Surface {                      /* the library's base "class" */
    int (*draw_dab)(Surface *self, float x, float y);
};

typedef struct {                      /* the library's tiled "subclass" */
    Surface parent;                   /* first: a TiledSurface* is a Surface* */
    int tile_size;
} TiledSurface;

typedef struct {                      /* the host's own "subclass" */
    TiledSurface parent;              /* first again: an AppSurface* is both */
    void *context;
    int dabs;
} AppSurface;

/* C11 guarantees a pointer to a struct, suitably converted, points to its
   first member. Make the layout a compile-time fact, not a hope. */
_Static_assert(offsetof(AppSurface, parent) == 0, "parent must stay first");
_Static_assert(offsetof(TiledSurface, parent) == 0, "parent must stay first");

static int app_draw_dab(Surface *surface, float x, float y) {
    AppSurface *self = (AppSurface *)surface;     /* the downcast the library never sees */
    self->dabs += 1;
    printf("dab %d at (%.0f, %.0f), tile size %d\n", self->dabs, x, y, self->parent.tile_size);
    return 1;
}

/* Library code: it only knows Surface. */
static void library_stroke(Surface *surface) {
    surface->draw_dab(surface, 10, 20);
    surface->draw_dab(surface, 11, 22);
}

int main(void) {
    AppSurface *app = calloc(1, sizeof *app);
    app->parent.tile_size = 64;
    app->parent.parent.draw_dab = app_draw_dab;

    Surface *base = &app->parent.parent;          /* what ard_surface_base returns */
    printf("same address: %d\n", (void *)base == (void *)app && (void *)(TiledSurface *)base == (void *)app);
    library_stroke(base);
    printf("dabs counted by the host: %d\n", app->dabs);
    free(app);
    return 0;
}
/* Output:
   same address: 1
   dab 1 at (10, 20), tile size 64
   dab 2 at (11, 22), tile size 64
   dabs counted by the host: 2

   Move the context field above the parent field and the build stops:
   error: static assertion failed ... "parent must stay first" */

External links

Exercise

Find a C library in your own dependency tree that builds a hierarchy by first-member embedding. Common ones: a custom SQLite VFS (its file struct starts with the base sqlite3_file), CPython extension types (PyObject_HEAD first), GObject instance structs. Write the one-line _Static_assert with offsetof that would catch a reorder of your subclass, compile it, then move a field above the parent and watch the build stop.
Hint
SQLite's VFS hands xOpen a block sized by the VFS's szOsFile and expects you to treat it as your own struct; that cast only works because the base sqlite3_file is your first member. If you cannot find the cast, search for the base type's name inside the subclass's functions: the downcast is usually the first line.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.