Platform and Ownership

ClaFi Framework Developer Manual

Introduction

The platform is the object an application is built against. It is the one place the framework meets the operating system, and it is where everything that is process-wide lives: the connection to the display server, the timers, the clipboard, the directory watches. An application holds exactly one, builds it first and destroys it last, and everything else in the process - the context, the forms, every control - stands on it.

Module Import:
import ClaFi.Platform.Windows;   // Win32Platform, Win32Application
import ClaFi.Platform.Wayland;   // WaylandPlatform, WaylandApplication

Most code never names the platform. A control reaches what it needs through its context - the clipboard through formContext().clipboard(), the animation controller through appContext().animator() - and the framework's own services reach their owner through one of two seams described in Reaching the Platform From Below. This manual is for the reader who wants to know what stands where, in what order it is built and torn down, and how to add a service of their own without breaking that order.

One rule is worth carrying from the start: nothing framework-wide is a static. Every object that serves the whole process has an owner, the owner is the platform or the application's context, and the object is built and destroyed at the moment its owner's member order says. A service that cannot hold a reference to its owner - a hook with no object in hand - reaches it through a standing pointer the owned object sets and clears, and a hook that finds none standing has found a broken order and says so.

Basic Usage Example

An application is Application<Platform, GpuBackend>, or a class derived from it, built from the platform's Params. The second argument names the GPU backend the application offers and may be left out, which offers none. The entry point is the whole of what a program knows about its platform:

// Win32 - Direct2D beside the CPU backend
int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE, wchar_t*, int)
{
    using namespace ClaFi;

    Themes_App::ThemesApplication<Win32Platform, Direct2DBackend> app{ { hInstance } };
    return app.run();
}

// Wayland - no GPU backend is named, because this platform has none
int main()
{
    using namespace ClaFi;

    Themes_App::ThemesApplication<WaylandPlatform> app{ WaylandPlatform::Params{} };
    return app.run();
}

Two aliases name the common pairings: Win32Application is Application<Win32Platform, Direct2DBackend> and WaylandApplication is Application<WaylandPlatform>. The CPU backend is the core's own and every application has it, so naming no GPU backend is an answer rather than a gap: Application<Win32Platform> draws on the CPU and offers nothing to turn on. One that names Direct2D carries both and starts on whichever its config holds - see The Backend.

A platform that cannot start throws. WaylandPlatform's constructor connects to the compositor, and where there is none it throws a std::runtime_error naming what WAYLAND_DISPLAY held. Left uncaught, that ends the process with the message on stderr, which is what a client started outside a session should do. There is no probe to call first: building the application is the attempt.

The Two Halves of a Platform

A platform is two classes in a base-derived pair. Platform, declared in the Context module, is what the rest of the framework sees and is the same on every platform. Win32Platform and WaylandPlatform derive from it, one per build, and are what the entry point names. The base is the contract; the derived class is the owner.

Platform, the Neutral Half

Platform lives in ClaFi.Core.Context.FormContext and carries three kinds of member:

KindMembersWho answers
StaticssetApplicationId, createFormsConfigSchema, restoreFormPlacement, storeFormPlacement, addFormToSession, keyName, keyModifiers, beep, globalScaleFactor, caretBlinkTime, processCpuTime, setCursor, cursor, getCursorInfo, shellExecute, appDataPath, documentsPath, executableDirectory, debugOutput, systemColorMode, events Each platform supplies the bodies in its own unit - Wayland.Platform.cpp, an implementation unit of the Context module, and Windows_Facade.cppm. A static that needs the display server reaches it through a standing pointer - see The Standing Pointer.
Two virtualscreateWindow(IForm&, WindowRole, IForm* parentForm), diagnosticLines() The derived platform, which holds what a window is made from. A form calls createWindow through appContext().platform() when it builds its window. diagnosticLines answers what the platform found the display server offering, one line each, and nothing unless a platform says otherwise; the application writes them into the diagnostic log as it starts. Wayland names the optional protocols present and missing.
One owned object, by referenceclipboard() The derived platform owns the Transfer::Clipboard as a member and names it to the base at construction: Platform(Transfer::Clipboard&).

The base holds a reference rather than the clipboard itself because of order: a base is built before any member of the derived class, and on Win32 the clipboard has to come after the OLE apartment the windows manager opens. So the derived class declares the clipboard where the order needs it and hands the base its address - before the clipboard is built, which a reference may be, nothing reading through it until the platform stands whole.

The Platform Built for the Display Server

Win32Platform and WaylandPlatform are the owners. Each derives from Platform and from one further interface that says what its platform layer offers to services standing below the context - the subject of The Empty Name and What Stands Behind It:

export struct Win32Platform
    :
    public Platform,
    public PlatformImplementation::Windows::IMessageWindowFactory
{
    ...
};

export struct WaylandPlatform
    :
    public Platform,
    public PlatformImplementation::Wayland::IDisplayAccess
{
    ...
};

What each owns, and in what order, is the next section. The rule they share: a platform member is declared after everything it needs and before everything that needs it, and what the platform owns is reached through the platform - or, where a hook has no object in hand, through the standing pointer the owned object publishes.

Params

Each platform declares a Params struct, and IsPlatform asks for nothing more than that a platform is constructible from one. It is what the entry point has and the platform wants:

PlatformParamsMeaning
Win32PlatformHINSTANCE appInstance The instance handle wWinMain is given, which every window class and window is registered under.
WaylandPlatformbool watchesClipboard{ false } Whether the application watches the clipboard rather than pastes from it. A compositor announces a selection to the client holding the keyboard focus and to nobody else, so a viewer of the clipboard asks here for the clipboard manager's channel, where the compositor offers one.

What the Platform Owns

C++ builds members in declaration order and destroys them in reverse, and that is the whole of the lifetime machinery here. Each platform lists its members in the order they depend on each other, the comment on the list says why, and nothing else states an order anywhere.

Win32Platform

private:
    Params m_params;
    PlatformImplementation::Windows::WindowsManager m_manager{ m_params.appInstance };
    PlatformImplementation::Windows::TimerManager m_timers{ m_manager };
    Transfer::Clipboard m_clipboard{ *this };
MemberWhat it isWhy here
WindowsManagerRegisters the window classes, opens the OLE apartment, and closes both on the way out. Every window is made from it. First: everything below is made from it or lives inside what it opened.
TimerManagerA message-only window and its WM_TIMER, which every UiTimer ticks through. After the manager, whose class it is made from.
Transfer::ClipboardThe clipboard, with its Windows half behind it - the IDataObject OLE holds and the listener window. Last: its destructor renders the package onto the clipboard, which wants the OLE apartment still open.

WaylandPlatform

private:
    PlatformImplementation::Wayland::DisplayManager m_display{};
    PlatformImplementation::Linux::TimerManager m_timers{};
    PlatformImplementation::Linux::DirWatchManager m_dirWatches{};
    PlatformImplementation::Wayland::PollSource m_timerSource{ m_display, m_timers.eventFd(), ... };
    PlatformImplementation::Wayland::PollSource m_dirWatchSource{ m_display, m_dirWatches.notifyFd(), ... };
    Transfer::Clipboard m_clipboard{ *this };
MemberWhat it isWhy here
DisplayManagerThe connection, the globals and the event loop. Its constructor connects and throws where it cannot. First: it is the loop everything else is dispatched from.
TimerManagerA thread that waits out the delays and an eventfd it raises, which every UiTimer ticks through. Before its poll source, which names its descriptor.
DirWatchManagerOne inotify descriptor serving every DirWatch. Same.
Two PollSourcesEach puts a descriptor in the display's loop with the call to make when it is readable, and takes it out again in its destructor. After the display and the manager each polls for, so the descriptor is in the loop exactly while both stand.
Transfer::ClipboardThe clipboard, with its Wayland half - the wl_data_source behind the selection. Last: on the way out it hands the selection and its change handler back to a display that is still up.

Order Is the Design

Two things follow from letting member order carry the lifetime, and both are deliberate.

First, there is no initialize() or finalize() on a platform. A member's constructor is its initialisation and its destructor its finalisation, and a member declared in the right place needs neither called. A separate finalisation step is for the case where something must happen while a later member is still standing, and no member of either platform has that case: the Win32 clipboard flushes in its own destructor, which runs before the manager's, and that is the order it wants.

Second, the loop names none of what it polls. On Wayland the display would otherwise have to know the timer and the watch managers, which would put them under it in the module graph and above it in the ownership order at the same time. Instead the display offers addPollSource and removePollSource, and the platform - which owns both the display and the managers - registers each descriptor through a PollSource whose lifetime is the registration.

Declaring a platform member in the wrong place is the one mistake this design cannot catch at compile time. The list in each facade carries a comment stating the order and its reasons; read it before adding to it, and add to it rather than around it.

Where the Platform Stands in an Application

First Up, Last Down

Application<Platform, GpuBackend> holds its platform in a base, not a member:

template <IsPlatform PlatformType>
class PlatformHolder
{
protected:
    PlatformType m_platform;
};

export template <IsPlatform PlatformType, IsOptionalGpuBackend GpuBackend = void>
class Application : private PlatformHolder<PlatformType>, public ApplicationBase
{
    ...
};

Bases are built in declaration order and destroyed in reverse, so the platform is built before ApplicationBase - and with it the AppContext, the config and the ThemesManager with its directory watch - and destroyed after them. Everything an application owns is built on a standing platform and goes down on one. A UiTimer stopped by a dying member finds the timer manager up; a directory watch removed by a dying themes manager finds its manager up; a form's window is destroyed while the windows manager still holds its class.

The base-from-member idiom is what makes this an order rather than a convention. A platform held as a member behind ApplicationBase would be built after the context and destroyed before it, and every one of the sentences above would be false.

The Whole Lifetime

From the entry point's braces to their close, in order:

StepWhat happens
1PlatformHolder builds the platform from its Params: the platform's members in their declared order, each on the ones before it. The platform stands whole from here.
2ApplicationBase is built on it: the config path, the AppContext holding a Platform& and the GPU backend's factory where the application named one, the ThemesManager.
3Application's constructor body calls ApplicationBase::initialize(): the application id is stated to the platform, the standard actions and the transfer formats are registered - the paste action connecting to the platform's clipboard - the app menu is connected, the diagnostic log is built and given the platform's diagnosticLines, the themes are connected, the config is read.
4The application's own members and its run(): forms are built, each asking the platform for its window and the context for a backend to draw it, and the main form's loop runs on the platform's event loop.
5The application's own members go. A form's root stops its animations while it can still name its form; a container stops its subtree's before detaching it - see Teardown.
6~Application calls finalize(): the config is saved where the user allowed it, the diagnostic log is released. It runs once, however many times it is called, so an application whose own members the save reaches calls it from its own destructor first.
7ApplicationBase's members go: the themes manager and its watch, then the context, whose destructor stops the theme crossing.
8The platform goes, its members in reverse: the clipboard flushes or hands its selection back, the timers stop, the display or the windows manager closes.

AppContext

AppContext is what a form or a control means by "the application". It holds the platform by reference and hands it out as platform(), and it owns what the application runs for itself rather than what the platform makes from the operating system: the AnimationController, as animator(), and the UpdateCheck, as updateCheck(). The animation controller belongs here rather than to the platform because it is the application's - every control's state fades, every glide, the theme crossing - stepped by one UiTimer that the platform's timer manager serves. Its owner has to stand below the platform and above every control, which is exactly where the context stands.

The update check holds a WebFetch while it asks: one request on a thread of its own, answered on the UI thread through a UiTimer. A fetch cancels its request and joins its thread in its destructor, and the context goes before the platform, so the timer it stops on the way out finds the timer manager up.

A FormContext holds a const Platform& and hands out the one thing a control asks the platform for directly, the clipboard, as clipboard(). It is not const: the clipboard is the process's state rather than the context's, and a copy made from a const context is still a copy.

The Backend

The platform makes a form's window; a backend draws into it. A form asks the context for one as it is built - AppContext::createBackend(window, size) - and is handed the GPU backend the application named while GPU acceleration is on, the CPU backend otherwise. The two kinds are built from different things, and a concept states each:

ConceptBuilt fromWhy
IsGpuBackend(IPlatformWindow&, IntSize) It draws into a window the platform owns, and reads the native object it needs out of that window.
IsCpuBackend(IntSize) It owns its pixel buffer, which the window presents.

Besides its size, a backend is told one thing about its window: whether the window carries an alpha channel, so that a frame starts from nothing where the pixels outside a rounded corner are read.

The choice is made in the application's type. Application is a template and a form is not, so it travels down as a function pointer - gpuBackendFactory<GpuBackend>(), null where none was named - and AppContext::gpuAvailable() is that pointer's answer. Where there is one, the config's Performance section holds a GpuAcceleration value, on by default, and the Settings page shows the check that writes it; a change reaches every window at the top of its next frame, and the window's canvas lets go of everything the old backend held. Where there is none, neither the section nor the check exists.

Nothing else in the platform depends on which backend draws, and nothing in a control does. What the two draw, and where they still differ, is the Graphics manual's.

Reaching the Platform From Above

Code above the context - forms, controls, an application's own pages - never names Win32Platform or WaylandPlatform. It reaches what it needs through the context it already has, and the context knows only Platform.

Windows

A form asks for its window in its own member initialiser, through the context's platform:

std::unique_ptr<IPlatformWindow> m_window{
    m_appContext.platform().createWindow(*this, m_windowRole, popupTargetForm())
};

createWindow is the virtual every platform answers. The derived platform answers with a FormWindow made from what it holds - the windows manager on Win32, the display on Wayland - and hands that to the window, so no window reaches for it by any other route. A popup is handed its parent's window the same way, through the parent form. WindowRole says what kind of window: Dialog, Menu or Tooltip, and nothing else.

Window Opacity

A window's opacity is a byte on its IPlatformWindow - alpha() and setAlpha(ColorByte) - applied to the whole window at once. A tooltip fades in and out this way, stepping the byte on AnimationSlots::formAlpha. Each platform applies the value where it costs least:

PlatformWhat applies it
Win32The window's composition visual. The shadow window the form casts its shadow from takes the same value, so the two fade together. Nothing is repainted.
Wayland, where the compositor offers wp_alpha_modifier_v1The compositor, as a multiplier stated on the surface. A change is one commit - no paint, no pixel pass, no new buffer - and it applies to a buffer without an alpha channel too, so an opaque window fades the same way. While the window is off the screen the value waits in the surface's pending state for the commit that maps it.
Wayland, where it offers noneThe frame, where the window's buffer carries an alpha channel. Every pixel is written at the window's opacity, and a change repaints the window. Premultiplied colour scales with its own alpha, so the four channels take one factor, the result is still premultiplied, and a rounded corner keeps the coverage it was drawn with.

Zero takes a Wayland window off the screen on either path: a frame nobody can see is not worth drawing, and an unmapped window is how a tooltip says it is not there.

The Clipboard

The clipboard is formContext().clipboard(), a Transfer::Clipboard&. A copy puts a package on it with the input event that asked; a paste asks it for an offer:

// a copy, from a handler that has the click's stamp
Transfer::Source source{};
source.add<Transfer::PlainText>(selectedText());
formContext().clipboard().set(std::move(source), stamp);

// a paste
if (Transfer::Offer* offer = formContext().clipboard().offer())
{
    if (const std::optional<std::wstring> text = Transfer::take<Transfer::PlainText>(*offer))
        insert(*text);
}

// what to connect to instead of polling - scoped, because the clipboard outlives the listener
ScopedEventConnection watch{ formContext().clipboard().events()
    .connect<Transfer::ClipboardChangeEvent>([&](Transfer::ClipboardChangeEvent&) {
        refresh();
    }) };

Where only a form is at hand, form->context().clipboard() is the same object. The clipboard's own manual is the Transfer footnote under Source/.seedocs/footnotes; what matters here is its place: one per process, the platform's member, alive from before the first window to after the last.

Animations

A control runs its animations through Control::animate, which reaches the application's controller through the control's form:

animate(AnimationSlots::expander, m_expandedFactor, target, [this](AnimateParams& params) {
    m_expandedFactor = params.value;
    invalidateChevron();
});

stopAnimation(AnimationSlots::scroll);   // one slot
stopAnimations();                           // every slot of this control

animator() answers the controller, or null while the control stands in no form - there being no application to reach then. animate on a control in no form applies the end value at once through the same callback: nobody sees an unattached control, and the end value is what it is drawn from once attached. A form reaches the controller as appContext().animator(); something that is not a control - the grid's cell highlights - takes it from the control it belongs to and answers the null the same way.

What the Platform Reports

The platform reports what the user does to the form whose window it happened in, through IForm: a pointer move, a press, a wheel, a key, a side button. The form turns each report into the framework's input state, which lives in Input - one per application rather than one per form, since the focus leaves one window as it arrives in another.

The Input Controllers

The mouse and the keyboard are each an InputController: whether it drives the input, and a factor that follows that through a fade of its own. Code outside Input reads them and never writes them:

const InputController& mouse = Input::mouse();
const InputController& keyboard = Input::keyboard();

bool byKey = !mouse.active();         // whether a key did what is being handled
float ringShare = keyboard.factor();  // active() through its fade, from 0 to 1

What the form is told switches them:

ReportMouseKeyboard
A pointer move, a press or a release, a wheel, a side button, a menu asked for at a pointOnOff
A key, or a menu asked for from the keyboardOffOn
A bare modifier - Shift, Ctrl, AltAs it isOn

So both are on only after a modifier, until the next pointer report or key, and the one state that cannot occur is both off. A modifier names no destination - it qualifies the click or the key that follows, and the pointer is still where the user left it. Two reports switch nothing: a held key's repeats, and the first move a form hears since the pointer came in, which only hit-tests - Win32 sends a window that appears under a still pointer a move, and a Wayland enter arrives as one.

A click, or a menu the pointer raised, always finds the mouse on. A key other than a modifier finds it off, unless the pointer moved while that key was held down and repeating.

The fades run on the application's animator, reached through the AppContext the form reporting the input hands in, so Input owns nothing that has to be released. The two slots, AnimationSlots::keyboardActive and mouseActive, mirror each other, so a switch from one device to the other is one crossfade. Each step of either fade raises InputSwitchEvent on AppContext::events(), and every visible form repaints whole on it: the colour rules read the two factors as RuleInput::Keyboard and RuleInput::Mouse, and nothing knows which controls of which windows do. The Colour manual says what the rules do with them.

Side Buttons

The two side buttons most mice carry are reported as SideButton::Back and SideButton::Forward - named by what the system takes each to mean, not by where it sits on the mouse:

PlatformBackForward
Win32XBUTTON1XBUTTON2
LinuxBTN_SIDE, or BTN_BACK on the few mice that report itBTN_EXTRA, or BTN_FORWARD

A side button acts as it goes down, and a second press inside the double-click time is one more press. On Win32 the window answers every side button message itself, so the system raises no WM_APPCOMMAND for the same press.

The form hands the press to the control under the pointer as a SideClickEvent, carrying the button, and walks it up the parents until one stops it. A control answers with an OnSideClick handler, or, where it answers for everything inside it, by overriding nestedSideClick:

// Whatever the pointer stands on inside the browser, a side button is its Back or Forward.
void BrowserControl::nestedSideClick(SideClickEvent& event)
{
    Panel::nestedSideClick(event);   // emits; a handler may stop it
    if (event.propagationStopped())
        return;

    event.stopPropagation();
    const std::ptrdiff_t offset = event.button == SideButton::Back ? -1 : 1;
    travel(offset, historyButton(offset));
}

Reaching the Platform From Below

Some of the framework's own services stand below the context in the module graph - the clipboard in Transfer, the timer hooks in System - and cannot import the module that declares Platform, because Context imports them. They still need the platform: the clipboard wants a listener window on Win32 and the display on Wayland, and a UiTimer wants the timer manager. Two seams serve them, and which one applies depends on whether the service has an object in hand.

The Empty Name and What Stands Behind It

IPlatformServices, declared in ClaFi.Core.System.UiTypes, is a class with a virtual destructor and nothing else. It is the platform as a service below the context can name it, and a service that is handed the platform holds it under this name:

export class Clipboard
{
public:
    explicit Clipboard(IPlatformServices&);
    ...
private:
    IPlatformServices& m_platform;
};

What stands behind the name is each platform layer's own statement, in its own module, and the platform built for the display server derives from it:

PlatformInterfaceDeclared inWhat it offers
Win32IMessageWindowFactoryClaFi.Platform.Windows.Window createMessageWindow() - a MessageWindow made from the windows manager, for a service that needs an HWND to listen on.
WaylandIDisplayAccessClaFi.Platform.Wayland.Display display() - the DisplayManager, for the data device, the selection and the change handler.

The service's platform half - the unit compiled for that platform alone - casts the name to what its platform stands behind it:

// Windows.Transfer.cpp
IMessageWindowFactory& platform = static_cast<IMessageWindowFactory&>(owner.m_platform);
listener = platform.createMessageWindow();

// Wayland.Transfer.cpp
return static_cast<IDisplayAccess&>(owner.m_platform).display();

The cast is sound because a build has one platform: the object behind an IPlatformServices& in a Windows build is a Win32Platform, which derives from IMessageWindowFactory, and there is no other. The cast is also what keeps the neutral name empty. Nothing is declared on IPlatformServices that only one platform can answer, so no platform is made to answer for a thing it has no notion of - Wayland has no message window, and Win32 has no display object - and the neutral module carries no method that half the builds would stub.

Platform itself does not derive from IPlatformServices; the derived platform does, beside Platform. That keeps the conversion from either platform to the name unambiguous, and keeps Context out of the business of naming the seam at all.

The Standing Pointer

A hook has no object in hand. PlatformTimer::start and stop are free functions a UiTimer calls; a DirWatch's platform half is built with nothing but the watch; a Platform static is a static. These reach the platform's object through a pointer that object sets as it is built and clears as it goes:

export class TimerManager
{
public:
    [[nodiscard]] static TimerManager* standing() { return s_standing; }
    ...
private:
    inline static TimerManager* s_standing{ nullptr };
};

// the hook
void start(UiTimer* timer, MilliSeconds delay)
{
    TimerManager* manager = TimerManager::standing();
    if (manager == nullptr)
        unreachable("PlatformTimer: no timer manager stands - a UiTimer outlives the platform");
    ...
}
PointerSet byRead by
TimerManager::standing() (Win32 and Linux)The platform's timer managerPlatformTimer::start and stop, the two hooks ClaFi.Core.System.Timer declares and each platform defines.
DirWatchManager::standing() (Linux)The platform's watch managerDirWatch::Impl, the Linux half of a directory watch.
DisplayManager::standing() (Wayland)The platform's display The Platform statics that need the display server: the cursor, the modifiers, the application id, the session.

This is not a singleton. The object is a platform member with a declared place and a declared lifetime; the pointer is a non-owning view of it, and it is null exactly while the platform holds no such object. That null is the whole of the difference: a singleton built on first use would answer a hook called after the platform has gone by building a new manager, silently and at process exit. A standing pointer answers it with unreachable, because the platform stands before anything that owns a timer or a watch and after it - see First Up, Last Down - and a hook finding none standing has found a broken order.

Do:

  • Hand the platform's object to whatever you build from it - a window its manager, a buffer its wl_shm - and hold it as a reference member.
  • Use the empty name and a cast when the caller is a service below the context that is handed the platform once.
  • Use a standing pointer only for a hook with no object in hand, and make none standing unreachable.

Don't:

  • Don't add a method to IPlatformServices. A method there is one every platform must answer, and the name exists to be empty.
  • Don't write a static X& instance() or a function-local static holding framework state. It has no place in the order and dies at process exit, after everything that would have released it.
  • Don't answer a missing standing pointer by doing nothing. The silence hides the order that is wrong.

The Platform's Own Objects

What each platform layer builds for its owner, and the two or three rules each carries.

On Win32

WindowsManager is the root of the Windows layer. Its constructor sets the process DPI awareness, opens the OLE apartment the clipboard lives in, and registers the four window classes; its destructor closes the apartment, posts the quit message and unregisters the classes. Every window is handed the manager by whoever makes it - the platform to a FormWindow, the platform to the timer's and the clipboard's message windows - and the manager has no static accessor. The one static state it keeps is the desktop's colour mode as last read, which a broadcast is compared against.

MessageWindow is a message-only HWND on WindowBase alone - no frame, no placement, no alpha, none of what a platform window has. Whoever holds one names itself as its IMessageSink and receives every message the window does, exactly as a Window subclass would without being one; what the sink leaves unhandled goes to the system's default. IMessageSink is also what the manager's window procedure delivers to for every window it made, by the pointer in the window's user data - a Window and a MessageWindow alike.

TimerManager owns a MessageWindow and is its sink for WM_TIMER. It publishes standing() for the two hooks.

On Wayland

DisplayManager is the connection, the bound globals and the event loop. Its constructor connects and throws where it cannot; waitForEvent sleeps on the connection's descriptor, every registered poll source and the appearance bus, and dispatchPending hands every sink what arrived. It publishes standing() for the Platform statics, and nothing else reaches it by name: windows are handed it, and the clipboard casts to IDisplayAccess.

PollSource is a descriptor's place in the loop, held for as long as the object stands: addPollSource in its constructor, removePollSource in its destructor, with the call to make when the descriptor is readable. The platform declares one per manager, after the display and the manager it polls for.

Linux::TimerManager is a thread that waits out the delays and an eventfd it raises; Linux::DirWatchManager is one inotify descriptor. Both live in Platform/Linux because they are the operating system's, not the display server's; both publish standing(); neither knows the loop that polls it.

The Clipboard's Two Halves

Transfer::Clipboard is the worked example of a service that is one object with a neutral half and a platform half. The neutral half - the package this process stands behind, the held reader, the change dispatcher, the generation - is the class's own members, declared in ClaFi.Core.Transfer.Clipboard. The platform half is NativeClipboard: a struct the interface declares without a body and each platform's implementation unit of the same module defines, held through a unique_ptr so the interface names no platform type.

Win32Wayland
NativeClipboard holdsThe IDataObject OLE keeps a reference to, the clipboard sequence numbers, the retry flag, the listener MessageWindow. The wl_data_source behind the selection and the offer handle the reader was built for.
Reaches the platform byCasting to IMessageWindowFactory on the first watch, for the listener window; it is the window's sink. Casting to IDisplayAccess, for the data device, the selection and the change handler it installs on the first watch.
On the way outRenders the package onto the clipboard where OleIsCurrentClipboard says it is still there, so a copy outlives the process; removes the format listener. Destroys the standing source and clears the display's change handler.

The constructor and destructor of Clipboard are defined in the platform unit, where NativeClipboard is complete. Nothing above the platform layer sees the difference.

Teardown

Construction order is the easy half; the framework's own objects are built by whoever owns them, in the order the owner declares. Teardown is where a design of this shape earns its keep, because the things going down still reach for the platform, and the platform has to be there.

What goesWhat it reaches forWhy it finds it
A control destroyed in placeThe animation controller, to stop what it was running (Control::~Control calls stopAnimations). It is still attached, so its form and the context are reachable.
A container's childrenThe same. They go down detached, so releaseChildren stops the whole subtree's animations first, while the container can still reach the controller.
A form's rootThe same. A root has no parent, so FormControlBase's destructor stops its animations while getForm still names its form - by the time Control's destructor runs, getForm walks the parents and finds none.
Any UiTimerThe platform's timer manager, through PlatformTimer::stop. Every UiTimer is owned by something the application or its context owns - a control, a form's tooltip, the animation controller, a directory watch, a web fetch - and the platform outlives all of them. The hook finding no manager is unreachable.
The themes manager's DirWatchThe platform's watch manager. ApplicationBase's members go before the platform.
The update check's WebFetchIts own thread, and the timer manager for the timer it answers through.Its destructor cancels the request and joins the thread while the timer manager stands: the check is the context's member, and the context goes before the platform.
AppContextIts own animation controller, to stop the theme crossing.The controller is its member, stopped in its destructor body before the members go.
The clipboardThe OLE apartment, or the display. Declared last in the platform, so it goes first.
An animation left running into freed memory is the failure this order prevents. The controller ticks through a callback that captures the control; a control that died without stopping its animations leaves that callback to run on the next tick. Every route a control can die by stops them - in place, as a released child, as a root - and each does so at the one moment the controller is still reachable from where it stands.

What Stays Static, and Why

The rule is not "no statics"; it is that no static holds anything that must be released in an order. A timer, a handle, a thread, a protocol object, a listener on something else - each of those has an owner with a declared place. What remains static holds none of them:

StaticWhat it holds
The Platform staticsFunctions, and one value: the cursor last set. A static that needs the display server on Wayland reaches it through DisplayManager::standing().
Platform::events()One dispatcher for the process, where SystemColorModeEvent is raised. It stands before the first window and after the last, which is the safe direction for a scoped connection.
InputWhich control has the focus, which is hovered, whether a button is down, and the two input controllers: pointers to controls that clear themselves on the way out, and values. Each controller's fade runs on the application's animator, reached through the AppContext the form reporting the input hands in - see The Input Controllers.
The Transfer tablesRegistrations filled by calls from ApplicationBase::initialize, never at static initialisation - a static library drops a translation unit nothing references.
The text engine's cachesLayouts, glyphs, draw buffers, the spare buffers on Linux. They are caches: what they hold is rebuilt on demand and released in no order.
The Wayland appearance portalA lazily connected session bus, read for the desktop's colour mode; its descriptor is polled by the display's loop while it is open.
libcurl, on LinuxNothing a variable holds: the library is opened with dlopen when a fetch runs and never closed. A transfer aborted while a name resolves leaves libcurl's resolver thread running its code, and unloading the library would take that code from under it.
WindowsManager::appsColorModeThe desktop's mode as last read, which a broadcast is compared against.
The diagnostic logA form, held in a static unique_ptr in the Diagnostic module. It is the one static whose contents need an order, and ApplicationBase::finalize is what keeps it: the log is released there, ahead of the context and the platform going.

Adding a Platform Service

The questions to answer, in order:

  1. Who owns it? A service the platform layer makes from the operating system - a window, a descriptor, an apartment - belongs to the platform. A service the application runs for itself - the animations - belongs to AppContext. Nothing belongs to a namespace.
  2. Where in the list? After everything it is made from, before everything that is made from it. On Wayland, a descriptor the loop must poll gets a PollSource declared after both the display and the service.
  3. Who reaches it, and from where? Code above the context: give the context an accessor, as clipboard() and animator(). A service below the context that is handed the platform once: add a method to the platform layer's IPlatformServices derivative - IMessageWindowFactory or IDisplayAccess - never to the neutral name. A hook with no object in hand: a standing() pointer on the service, set and cleared by the service, unreachable when null.
  4. Where do the bodies go? A class member is defined in the module that declared its class, so a platform-neutral interface with platform bodies gets one implementation unit per platform of the same module - Windows.Transfer.cpp and Wayland.Transfer.cpp are both module ClaFi.Core.Transfer.Clipboard;. Its platform state goes behind a struct the interface declares and the platform unit defines, held through a unique_ptr.
  5. What does it do on the way out? Its destructor, and only that. If it must act while a later platform member still stands, the order is wrong; move it.

Reference

The platform's neutral half, ClaFi.Core.Context.FormContext:

MemberWhat it is
Platform(Transfer::Clipboard&)Names the clipboard the derived platform owns as a member.
clipboard()The one clipboard, a Transfer::Clipboard&.
createWindow(IForm&, WindowRole, IForm* parentForm)A form's window, answered by the derived platform.
diagnosticLines()What the platform found the display server offering, one diagnostic log line each. Empty unless the derived platform answers.
The staticsSee Platform, the Neutral Half.

The two platforms and their facades:

NameModuleNotes
Win32Platform, Win32ApplicationClaFi.Platform.Windows Params{ HINSTANCE appInstance }. Derives from IMessageWindowFactory. Win32Application is Application<Win32Platform, Direct2DBackend>.
WaylandPlatform, WaylandApplicationClaFi.Platform.Wayland Params{ bool watchesClipboard }. Derives from IDisplayAccess. The constructor throws std::runtime_error with no compositor. WaylandApplication is Application<WaylandPlatform>.

The seams:

NameModuleWhat it is
IPlatformServicesClaFi.Core.System.UiTypes The platform as a service below the context names it. Empty.
IMessageWindowFactory, MessageWindow, IMessageSink ClaFi.Platform.Windows.Window What Win32 stands behind the name: a message-only window and the sink it delivers to.
IDisplayAccess, PollSourceClaFi.Platform.Wayland.Display What Wayland stands behind the name: the display, and a descriptor's place in its loop.
TimerManager::standing()ClaFi.Platform.Windows.Timer, ClaFi.Platform.Linux.TimerFor PlatformTimer::start and stop.
DirWatchManager::standing()ClaFi.Platform.Linux.DirWatch For the Linux DirWatch::Impl.
DisplayManager::standing()ClaFi.Platform.Wayland.Display For the Platform statics.

What the context hands out:

AccessorAnswers
AppContext::platform()The Platform&.
AppContext::animator()The application's AnimationController.
AppContext::updateCheck()The application's UpdateCheck.
AppContext::createBackend(window, size)A form's backend: the GPU backend while one was named and GPU acceleration is on, the CPU backend otherwise.
AppContext::gpuAvailable(), usingGpu()Whether the application named a GPU backend, and whether the windows draw through it now.
FormContext::clipboard()The platform's Transfer::Clipboard.
Control::animator()The controller through the form, or null in no form.
Control::animate(slot, current, end, onAnimate)Runs an animation through it, or applies the end value in no form.
Control::stopAnimation(slot), stopAnimations()The stops, by the same route.
AppContext::events()Where InputSwitchEvent is raised, at every step of either input controller's fade.

What the platform reports, as the framework holds it:

NameModuleWhat it is
Input::mouse(), Input::keyboard()ClaFi.Core.Foundation The two input controllers, read-only.
InputController::active(), factor()ClaFi.Core.Foundation Whether the device drives the input, and that through its fade.
InputSwitchEventClaFi.Core.Context.AppContext The controllers' factors have moved. Carries nothing.
SideButtonClaFi.Core.System.UiTypes Back or Forward.
SideClickEvent, Control::OnSideClick, nestedSideClick ClaFi.Core.FoundationA side button pressed over a control, walked up its parents.
IPlatformWindow::alpha(), setAlpha(ColorByte) ClaFi.Core.Context.FormContextThe window's opacity - see Window Opacity.
All manuals Top