// lesson: the-platform-seam
The Platform Seam
Before touching an OS API, decide where portability lives. The classic
mistake is to sprinkle #ifdef _WIN32 through the whole program until every
file knows about three operating systems. The fix is one deliberate seam: a
small set of types the editor is written against, with one implementation
of that seam per platform. Get the seam right and the editor core โ the
document, layout, selection, undo, everything you'll build from lesson 6
onward โ compiles and runs on a machine with no display at all. (That's not
hypothetical: it's exactly how this course's grader runs your code.)
A workable seam for an editor is tiny:
// platform.h โ the only header the editor core sees.
struct PlatformWindow {
virtual ~PlatformWindow() = default;
virtual void set_title(const std::string& title) = 0;
virtual void invalidate(Rect dirty) = 0; // "please send me a Paint for this"
virtual Size size() const = 0;
virtual void blit(const Framebuffer& fb) = 0; // pixels -> screen
};
struct Platform {
virtual ~Platform() = default;
virtual std::unique_ptr<PlatformWindow> create_window(int w, int h) = 0;
virtual std::optional<Event> wait_event() = 0; // nullopt = shutting down
virtual std::string clipboard_read() = 0;
virtual void clipboard_write(const std::string& text) = 0;
};
Two C++ decisions are hiding in those dozen lines, and they're worth making consciously:
- Abstract base vs
std::variant. You could model the seam asstd::variant<X11Platform, Win32Platform, CocoaPlatform>andstd::visityour way through. But only one alternative can ever exist per build โ you'll never hold "an X11 or Win32 platform" at runtime on one OS โ so the variant buys closed-set exhaustiveness you don't need and costs you a compile-time dependency on every platform's headers everywhere. An abstract base with exactly one derived class linked per platform keeps each backend's headers quarantined in its own.cpp(or.mm) file. Variants shine for events (a closed set of small value types); virtual dispatch shines for backends (open set, one alive at a time, holding OS resources). - Pimpl by construction. Because the editor only ever sees
PlatformWindow*, thexcb_connection_t*andxcb_window_tmembers live inX11Window : PlatformWindowinsideplatform_x11.cpp. The interface is the pimpl โ no separate idiom needed, no OS header ever leaks into the core's translation units.
Value types for geometry
The seam traffics in points, sizes, and rectangles constantly: mouse
positions, dirty regions, window sizes. These should be value types โ
small structs passed by value, compared field by field, copied freely โ not
objects with identity. There is nothing to encapsulate in an x and a y;
wrapping them in getters adds friction and nothing else. This is the "rule
of zero" end of the design spectrum: no destructor, no copy control,
aggregate initialization (Rect{0, 0, 800, 600}) just works.
The one place geometry earns real code is rectangle algebra. Editors compute with rects all day: "clip this fill to the window", "what part of the damage is visible", "merge these two dirty regions". Two operations carry almost all of it:
intersect(a, b)โ the overlapping region (used for clipping). If the rects don't overlap, the result is empty.union_of(a, b)โ the smallest rect containing both (used for merging damage). An empty rect is the identity: union with it returns the other rect unchanged, because "no damage" plus "this damage" is just "this damage".
A rect with w <= 0 or h <= 0 is empty. Treat all empty rects as
interchangeable โ code that checks r.empty() before using r never cares
which degenerate coordinates it holds.
One convention to fix now and never revisit: rects are half-open. A
rect at x=0 with w=10 covers pixel columns 0 through 9 โ x + w is
one-past-the-end, exactly like STL iterators. Half-open ranges make
adjacency tests (a.x + a.w == b.x) and width math (right - left == width) come out without the ยฑ1 fudging that plagues inclusive ranges.
Handles: what the OS actually hands you
Everything a windowing system gives you is a handle: an opaque token
standing for a resource the OS owns on your behalf. On X11 a window is a
uint32_t ID valid only within one connection; on Win32 it's an HWND; on
macOS an NSWindow*. The same goes for graphics contexts, shared-memory
segments, and the file-watch descriptors we'll meet in the final lesson.
Each has a matching destroy call (xcb_destroy_window, DestroyWindow,
close), and each one leaks โ or worse, dangles โ if some error path
misses the cleanup.
C programs handle this with discipline and goto cleanup. C++ has a better
answer, and it's the most important idiom in this course's platform layer:
RAII โ acquire the resource in a constructor, release it in the
destructor, let scope do the bookkeeping. Once a handle lives inside an
RAII wrapper there is no code path that leaks it: early returns,
exceptions, and error branches all run the destructor.
The subtlety is copying. If a wrapper owning window #42 gets copied, both copies' destructors destroy window #42 โ a double-free. So an owning handle wrapper must be move-only: copying is deleted (there can't be two owners), and moving transfers ownership, leaving the source holding nothing so its destructor does nothing. This is the "rule of five" in its most common practical form: you wrote a destructor, so you must decide all five special members. (The geometry structs above follow the rule of zero: no resources, so no special members at all. Most types should be one or the other โ a type with a destructor but default-generated copies is the red flag.)
You already know this shape: it's std::unique_ptr, generalized. A
unique_ptr with a custom deleter can wrap handles, but an int-valued
handle where -1 means "nothing" fits awkwardly into a pointer-shaped API,
so platform layers routinely write a small dedicated wrapper. Three fine
points the second challenge will hold you to:
- Moved-from means empty. After
b = std::move(a),amust hold the invalid handle so its destructor is a no-op. Forgetting this is the #1 RAII bug: the resource dies twice. - Move-assignment releases the old resource first. If
balready owned something, that something must be closed beforebtakes overa's handle โ otherwise it leaks silently. - Self-move must not destroy the resource.
x = std::move(x)is legal C++ (it arises in generic code) and must leavexusable.
release() (give the handle up without closing โ for handing ownership to
an API that takes it) and reset() (close now, optionally adopt a new
handle) round out the interface, mirroring unique_ptr deliberately:
familiar names make your code cheaper to read.
โบ Points and Rects
10 ptsImplement the geometry kit the rest of the course leans on. Every later rendering challenge hands you this code back pre-written, so make it right once, here.
Semantics:
Rect::empty()โ true iffw <= 0orh <= 0.Rect::contains(Point)โ half-open on both axes:{0,0,10,10}contains (0,0) and (9,9) but not (10,10). Empty rects contain nothing.intersect(a, b)โ the overlap; when there is none, return somethingempty()(returning{0,0,0,0}is the tidy choice). If either input is empty, the result is empty. The tests only inspect exact fields of non-empty results.union_of(a, b)โ bounding box of both; if one input is empty, return the other unchanged; if both are empty, return an empty rect.
Log in to submit a solution and earn points.
โบ A Move-Only Handle
12 ptsImplement UniqueHandle, the RAII wrapper the platform backends use for
every OS resource. The handle is an int where -1 means "none"; the
close function is supplied at construction (in the real backend it's a
function calling xcb_destroy_window, close(2), and so on).
Required behavior:
- the destructor closes a valid handle exactly once; it never calls close
for handle
-1or when the close function is null; - move construction and move assignment transfer ownership and leave the source empty;
- move assignment closes the destination's previous handle first;
- self-move-assignment leaves the object unchanged and closes nothing;
release()returns the handle and empties the wrapper without closing;reset(h, fn)closes any current handle, then adoptshandfn.
Log in to submit a solution and earn points.