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_atomicandend_atomiccastMyPaintSurface*toMyPaintTiledSurface*, and so does itsdraw_dab. That is the library downcasting on its own; it never learns Ariandel exists. - Ariandel's
tile_request_startandtile_request_endreceive aMyPaintTiledSurface*and cast it toArdSurface*to reach the context and the callbacks. - Ariandel's
surface_destroy, installed in the base'sdestroyslot, receives aMyPaintSurface*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.