Device extensions and host services

CNA snapshot 009d40f5  ·  Development › Module internals  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Test names were located by reading and none was executed. Real host-service behaviour, permission behaviour on any operating system and every platform implementation beyond the SDL3 camera, dialog and tray code that was read remain unverified.

modules/devices-ext/ exposes the CNA-specific CNA::Devices services: camera, clipboard, file and message dialogs, system tray, display information, locale, power, system information and URL launching. Its classes are mostly thin, but not interchangeable, translations over the selected platform's capability and service interfaces, so the maintenance risk is not native code (the platform owns that) but truthful capability reporting, callback completion, borrowed-service lifetime and the crossing from a camera frame into a graphics texture. This page is for maintainers who change one of these wrappers or add a host capability; the user-level material is the CNAEXT engine layer guide and Tutorial 117. The XNA and Windows Phone shaped sensors are a different module, described in Devices and sensor lifetime.

Physical boundary and selection

modules/devices-ext/CMakeLists.txt builds cna_devices_ext (alias CNA::DevicesExt) from every source under src/, linking cna_runtime, cna_graphics_core, cna_core, cna_math and cna_platform plus Sharp Runtime Core.Base. It does not link the XNA device base module; the split is by API responsibility, not by which native hardware is involved, and the dependency direction is deliberately one way. The module has no native-library link of its own: every native system surface reaches the host through the platform contract, and the selected platform implementation owns any library or handle it needs.

Every header, source and test file in the module is wrapped in #ifdef CNA_DEVICES (checked by reading each file at this snapshot). The root option CNA_DEVICES defaults to OFF and, when ON, adds the definition to the shared build-config target. A default build therefore ships an empty cna_devices_ext library and none of its tests, and a test or binary built without the option cannot establish extension behaviour even though the physical target exists. This is the only thing the option gates: Microsoft::Devices is built either way (see the devices page), which is why the sanitizer presets and the devices workflow set the option explicitly. The user-facing side of the same rule is in the CNAEXT guide, and a caller that builds with and without the option must guard its own use of the headers.

CNA::Devices static / value API
  -> GetCurrentPlatform() + PlatformCapabilities
  -> IPlatformCameraProvider | IPlatformDialogs | IPlatformClipboard | IPlatformTray
     | IPlatformDisplays | IPlatformSystemInfo
  -> selected platform backend -> OS service

Camera::TryAcquireFrame -> PlatformCameraFrame RGBA bytes -> Texture2D::SetDataRGBA

The static wrappers borrow their service from the ambient platform at each call. Camera and SystemTray instead own a unique_ptr to a session or icon that a borrowed provider created, and they must be destroyed before their platform backend. No common lock or off-thread dispatch layer exists in this module, so each platform service's own callback and thread rules remain relevant, and a change to IPlatform service availability or to a capability bit can alter all of these APIs without changing this module. One correction to a common assumption: the wrappers do not install a platform, but they do not need one installed either. CurrentPlatform.hpp documents that GetCurrentPlatform() never returns null and, on first use with nothing set, creates and installs the build-time default platform, throwing PlatformException only if that default cannot be created. The rule the tests state is the ownership one: a platform must outlive every use of the reference GetCurrentPlatform() returns.

Which platforms provide which service

The capability bits in PlatformCapabilities follow a stated contract: unsupported means refused, never silently ignored. The platform conformance suite pins the companion rule as PlatformConformance.EveryServiceIsNullExactlyWhenItsCapabilityIsFalse: each service pointer is non-null exactly when its capability bit is true, except that the dialog service is non-null when either the message-box or the file-dialog capability is true. Read from the platform sources at 009d40f5, the services these wrappers ask for are returned as follows; the capability values behind them were traced only where stated.

ServiceNon-null onAlways nullptr on
Camera providerSDL3 only, and only when SDL reports a camera driver and the camera subsystem startsX11, WAYLAND, WIN32, SDL2, HEADLESS, TERMINAL
DialogsSDL3 and WIN32 (both dialog capabilities true); X11 and WAYLAND return their owned dialog object, with X11 reporting the file-dialog capability only when it also has a portal object and WAYLAND asking its dialog object which of the two it hasSDL2, HEADLESS, TERMINAL
TraySDL3 when its tray reports support; X11 when its tray object was createdWIN32, WAYLAND, SDL2, HEADLESS, TERMINAL
ClipboardSDL3, X11, WIN32; WAYLAND when its data-device manager existsSDL2, HEADLESS, TERMINAL
System informationevery platform (the interface documents it as never null)none

How the tests reach the platform

There are no per-class SetBackendForTesting hooks any more. Tests install a platform that records instead of acting: CannedDialogPlatform (CannedDialogs.hpp) and CannedCameraPlatform (CannedCamera.hpp), each a PlatformTestDecorator around a real platform, installed with ScopedCurrentPlatform. The dialog test scaffolding exists because a real message box or file dialog blocks on a human, and this project has already had an automated run leave orphaned zenity processes. Two properties of the canned dialog service shape what the tests can show: it fires a file-dialog callback before the call returns, which the SDL3 and portal-based X11 and Wayland services do not, though the native WIN32 service does (its file dialogs are modal and call back before Show* returns, Win32SystemServices.cpp, contrary to the platform contract: CNA-BUG-182), and it throws PlatformException for an empty callback for the same reason the SDL3 service does.

Camera: capability, session and texture upload

Camera.cpp and Camera.hpp translate the platform camera contract in IPlatformCamera.hpp. Camera::getIsSupportedProperty reads the platform's camera capability: it means a backend is offered, not that a physical camera is present or permission was granted. getAvailableCamerasProperty calls IPlatformCameraProvider::GetCameras and maps name and position (front, back or unknown) without opening a device or requesting permission. The constructor opens only the first enumerated camera, keeps its IPlatformCamera session in a unique_ptr, and is neither copyable nor movable; there is no selection API and no callback-based capture. getStateProperty returns NotSupported when there is no session (no provider, no camera, or an open that failed) and otherwise the session's state, mapped one to one from Opening, Denied, Ready and Lost. The public CameraState also declares Closed, which this wrapper never returns. The session destructor closes the device, so changing platform lifetime while a Camera exists is unsafe even though its C++ owner is a unique_ptr.

Behind the wrapper the only provider at this snapshot is SDL3's (Sdl3Camera.cpp). Its IsSupported requires both a compiled-in camera driver and a successful start of the SDL camera subsystem, which it starts lazily and never quits again because a camera id is only meaningful inside the session that issued it. Its session requests RGBA32 frames, latches Denied, Ready and Lost, resolves the negotiated format only after the permission state is approved (reporting Lost if the format is not RGBA32), and returns a frame only when the session is Ready. It copies each acquired surface row by row into a tightly packed vector, releasing the SDL frame lease automatically, so no consumer needs a pitch value. Its own comments note that SDL may keep returning blank frames after physical removal, so Lost is best effort.

TryAcquireFrame polls that session, non-blocking, once per Update or Draw. It returns false, changing nothing, in this order:

  1. there is no session;
  2. the platform reports no new frame, or a frame with a non-positive width or height;
  3. the destination texture's width or height differs from the frame's;
  4. the row byte count (width times four) or the total would overflow size_t, or the pixel vector's size is not exactly width times height times four;
  5. the pixel count would not fit an int.

Only then does it call Texture2D::SetDataRGBA with the pixel count. It does not allocate or recreate the destination texture, convert formats or select another camera; the caller must construct the texture with the camera's frame size. The header also says a call returns false when the state is not Ready, but the wrapper does not test the state itself: that answer comes from the platform session refusing to produce a frame. A camera-frame problem crosses the platform service, this wrapper, graphics upload and the chosen renderer, so capture which boundary first diverged before editing: a bad byte count belongs at the bridge, a stride or format error can belong to the native provider, and an upload failure after valid data reaches SetDataRGBA belongs to graphics and the renderer.

CameraTests (13 tests) uses the canned platform for capability reflection, enumeration without opening (EnumerationMapsNamesAndPositionsWithoutOpeningADevice), an inert instance on an unsupported platform, opening the first camera, open failure, state mapping, frame dimensions, the destructor closing the session, no frame ready, not-ready, dimension mismatch (TryAcquireFrameRejectsTextureDimensionMismatch), a malformed pixel count and a successful RGBA upload. The texture tests create a graphics device, so the devices workflow runs that suite under Xvfb. None of it proves live camera permission behaviour on any operating system, and no test drives the SDL3 provider itself; the design record is docs/cna-devices-camera-design.md.

Dialogs: three distinct failure contracts

The two interfaces are not a generic dialog. Message boxes are synchronous and return a choice; file and folder dialogs take a callback and are asynchronous. The platform contract in IPlatformSystemServices.hpp says the callback fires exactly once, after the Show* call has returned, on the thread that pumps events, with an empty list when the user cancelled (the native WIN32 service does not keep this promise: its file dialogs are modal and call back before Show* returns, CNA-BUG-182); the wrappers add three different failure behaviours on top.

SurfaceGateService missingPlatform throws PlatformException
MessageBox::ShowSimple (MessageBox.cpp)messageBox capabilityreturns silentlyswallowed; the call returns silently
MessageBox::Show with buttonsmessageBox capabilityreturns -1swallowed; returns -1, the same value as a dismissed dialog
FileDialog::ShowOpenFile, ShowSaveFile, ShowOpenFolder (FileDialog.cpp)nativeFileDialog capabilityinvokes the callback once with an empty list, so a waiting continuation never hangsswallowed; the callback is not invoked, because it was already moved into the platform call

The last cell is narrower than a guarantee that the callback always completes. The wrapper passes std::move(onResult) into a by-value parameter, so when the platform throws, the wrapper's own callback is empty and cannot be called; the source comment says the request was rejected rather than shown, and the caller may observe a dialog that never opened. Preserve the distinction in caller code and characterise the failure before changing this seam. Three further points follow from reading the code. First, FileDialog::getIsSupportedProperty applies target policy (Web and iOS report false, Desktop and Android answer with the nativeFileDialog capability), but the three Show* functions gate only on the capability, so the policy affects what the query says, not whether a call reaches the service. Second, every call passes a null parent window, so dialogs are never parented. Third, the SDL3 service (Sdl3SystemServices.cpp) throws PlatformException for an empty callback and for a message box with no buttons, and the wrapper turns both into silence or -1; the SDL3 trampoline reports an SDL failure to the callback as an empty list, the same answer as a cancel.

A host-specific dialog can still impose main-thread or native-window requirements; the wrapper does not marshal to an owning UI thread. The tests for these surfaces are FileDialogTest.* and MessageBoxTest.* (parameter forwarding, every severity forwarded distinctly, dismissed dialog reporting no choice), plus FileDialogWithoutAServiceTest.TheCallbackStillFiresSoACallerIsNotLeftWaiting and MessageBoxWithoutAServiceTest.ReportsUnsupportedAndDoesNothingRatherThanFailing. No test in this module makes the dialog service throw, so the swallow paths in the table are read from source, not pinned by a test, and the canned service's synchronous callback means no test in this module shows the asynchronous timing; at platform level the X11 and Wayland portal tests do (X11DesktopPortalBus.AnOpenDialogCarriesItsOptionsAndItsAnswerArrivesFromPump asserts the answer never arrives inside the call, and WaylandPortal.TheGameKeepsRunningWhileADialogIsOpen).

Other service translations and ownership

  • Clipboard (Clipboard.cpp) borrows IPlatformClipboard. setTextProperty returns false when the service is absent or throws PlatformException; the getters return an empty string or false when the service is absent.
  • SystemTray (SystemTray.cpp) reads the tray capability, throws PlatformNotSupportedException when there is no service and PlatformException when the service returns no icon, and owns the returned IPlatformTrayIcon. Entry callbacks are forwarded into that icon; the SDL3 implementation keeps their storage alive for the life of the tray and invokes them from SDL's tray trampoline, and this module does not specify the OS callback thread, so destruction order and handler captures must be reviewed in the backend. Unknown entry indices are documented as ignored by the platform contract and read false (UnknownEntryIndicesAreSafeAndReadFalse).
  • DisplayInfo (DisplayInfo.cpp) reads a GameWindow's borrowed platform window. Content scale is the maximum of the window's display scale and its display's content scale, not their product, because a platform states high density one way or the other (as pixel density on macOS and Wayland, as display scale on Windows and X11) and a product would double-count a backend that reports both; it returns 0 when the window has no platform window. Safe area is empty when the window or the display service is unavailable.
  • Locale, PowerInfo, SystemInfo and UrlLauncher call IPlatformSystemInfo and translate returned values without any per-object cache. They do not test the service for null, which is consistent with the contract that GetSystemInfo() is never null.

Failure representation intentionally differs across these classes, and the differences are contracts callers rely on:

Unavailable or failing serviceWhat the wrapper does
Cameraan inert instance whose state is NotSupported; enumeration returns an empty list
System traythe constructor throws
Clipboard setreturns false
Message boxsilently refuses, or returns -1
File dialogcalls back with an empty result when unavailable; silent when the platform throws
System queriesreturn the platform service's data

Do not simplify these paths to one blanket exception or capability check without considering the callers and the tests, and do not read the absence of a lock in a small wrapper as a whole-service thread-safety contract.

Worked maintenance decision: add a host capability

  1. Define the user-visible contract in CNA::Devices first: a synchronous value, an owned session or an asynchronous callback. Decide how an unsupported platform and a thrown platform operation are reported before editing.
  2. Find the narrow IPlatform* service that owns the OS operation. Add capability metadata only when its meaning is stable across backends. The conformance rule above means a new service needs both halves, a capability bit and a pointer that is non-null exactly when the bit is true, and one service can carry two capabilities (X11 can have a message box without file dialogs).
  3. Implement or explicitly refuse the operation in each relevant platform backend. Review the acquired subsystems, native handles, the callback thread, and whether a session or icon must die before platform destruction.
  4. Add fake-service tests beside modules/devices-ext/tests/CNA/Devices for argument translation, the unsupported result, lifetime and the exception path (the last is the one the current suite lacks for dialogs), using a canned platform rather than a hook on the public class. Add a native-host smoke for the real platform. If a graphics texture is involved, run a representative renderer upload and readback test too.
  5. Configure with CNA_DEVICES=ON, confirm the owning tests are present in the build, and run the focused CnaDevicesExtTests target (an EXCLUDE_FROM_ALL developer target, not a separate CTest registration) plus the aggregate CnaTests. A default-OFF unit build is not evidence for this module. Review the C API and binding exposure only if the new operation is exported there: devices.h exports the camera, dialog, tray and other routes, with test-backend hooks for some, and whether the C API library builds was not verified for these pages.

For a concrete camera bug, trace Camera::TryAcquireFrame backward to the provider's PlatformCameraFrame and forward to Texture2D::SetDataRGBA, following the boundary rule in the camera section. The dedicated CI workflow builds the devices-ubsan preset with CNA_DEVICES=ON, runs the CNA::Devices suites its filter names in a separate step under Xvfb (only eight of the twelve suites match: the filter still names FileDialogTests and MessageBoxTests, so FileDialogTest, MessageBoxTest and the two WithoutAService suites do not run), and every test there exercises a canned platform rather than real camera, dialog or tray hardware, so it is desktop and headless evidence only.

⚠

What remains unverified. Real host-service behaviour (native dialogs, trays, clipboards and cameras on real desktops), every platform implementation beyond the SDL3 camera, dialog and tray code read here, and permission behaviour on any operating system were not exercised. This page reports the source and test files at 009d40f5; nothing was built or run for it.

Source and test reading route

  1. devices-ext/CMakeLists.txt and Camera.hpp: establish the build gate, the public scope and which objects own sessions.
  2. Camera.cpp and IPlatformCamera.hpp: follow frame validation and provider and session ownership; then CameraTests.cpp to separate fake proof from real-device behaviour.
  3. FileDialog.cpp, MessageBox.cpp and their tests: compare callback versus synchronous choice, capability refusal and the caught platform failure, then read the SDL3 dialog service beside them.
  4. SystemTray.cpp and DisplayInfo.cpp: learn owned icon versus borrowed window and the scale combination rule.
  5. For any particular change, finish by reading the selected platform's matching service implementation and the canned platform in CannedDialogs.hpp that the tests use in its place.

Related reading: Platform architecture for the service and capability model, SDL3 platform internals for the only camera provider, the ownership map and the thread and callback map, the module index and the Maintainer Handbook.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.