Device extensions and host services
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.
| Service | Non-null on | Always nullptr on |
|---|---|---|
| Camera provider | SDL3 only, and only when SDL reports a camera driver and the camera subsystem starts | X11, WAYLAND, WIN32, SDL2, HEADLESS, TERMINAL |
| Dialogs | SDL3 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 has | SDL2, HEADLESS, TERMINAL |
| Tray | SDL3 when its tray reports support; X11 when its tray object was created | WIN32, WAYLAND, SDL2, HEADLESS, TERMINAL |
| Clipboard | SDL3, X11, WIN32; WAYLAND when its data-device manager exists | SDL2, HEADLESS, TERMINAL |
| System information | every 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:
- there is no session;
- the platform reports no new frame, or a frame with a non-positive width or height;
- the destination texture's width or height differs from the frame's;
- 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; - 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.
| Surface | Gate | Service missing | Platform throws PlatformException |
|---|---|---|---|
MessageBox::ShowSimple (MessageBox.cpp) | messageBox capability | returns silently | swallowed; the call returns silently |
MessageBox::Show with buttons | messageBox capability | returns -1 | swallowed; returns -1, the same value as a dismissed dialog |
FileDialog::ShowOpenFile, ShowSaveFile, ShowOpenFolder (FileDialog.cpp) | nativeFileDialog capability | invokes the callback once with an empty list, so a waiting continuation never hangs | swallowed; 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) borrowsIPlatformClipboard.setTextPropertyreturns false when the service is absent or throwsPlatformException; the getters return an empty string or false when the service is absent. - SystemTray (
SystemTray.cpp) reads thetraycapability, throwsPlatformNotSupportedExceptionwhen there is no service andPlatformExceptionwhen the service returns no icon, and owns the returnedIPlatformTrayIcon. 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 aGameWindow'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
IPlatformSystemInfoand translate returned values without any per-object cache. They do not test the service for null, which is consistent with the contract thatGetSystemInfo()is never null.
Failure representation intentionally differs across these classes, and the differences are contracts callers rely on:
| Unavailable or failing service | What the wrapper does |
|---|---|
| Camera | an inert instance whose state is NotSupported; enumeration returns an empty list |
| System tray | the constructor throws |
| Clipboard set | returns false |
| Message box | silently refuses, or returns -1 |
| File dialog | calls back with an empty result when unavailable; silent when the platform throws |
| System queries | return 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
- Define the user-visible contract in
CNA::Devicesfirst: a synchronous value, an owned session or an asynchronous callback. Decide how an unsupported platform and a thrown platform operation are reported before editing. - 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). - 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.
- Add fake-service tests beside
modules/devices-ext/tests/CNA/Devicesfor 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. - Configure with
CNA_DEVICES=ON, confirm the owning tests are present in the build, and run the focusedCnaDevicesExtTeststarget (anEXCLUDE_FROM_ALLdeveloper target, not a separate CTest registration) plus the aggregateCnaTests. 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.hexports 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
devices-ext/CMakeLists.txtandCamera.hpp: establish the build gate, the public scope and which objects own sessions.Camera.cppandIPlatformCamera.hpp: follow frame validation and provider and session ownership; thenCameraTests.cppto separate fake proof from real-device behaviour.FileDialog.cpp,MessageBox.cppand their tests: compare callback versus synchronous choice, capability refusal and the caught platform failure, then read the SDL3 dialog service beside them.SystemTray.cppandDisplayInfo.cpp: learn owned icon versus borrowed window and the scale combination rule.- For any particular change, finish by reading the selected platform's matching service implementation and the canned platform in
CannedDialogs.hppthat 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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Host devices: the optional CNA::Devices layer — The CNA_DEVICES build contract, what each CNA::Devices class really does, its platform reach, the overlap with CNA::Input, the camera polling model and the callback boundary callers must respect.
- Text input and IME composition — The exact TextInputEXT contract in CNA: committed text, IME composition and candidate events, the window-bound text mode, the input-type hint, composition placement and the clipboard classes.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-181: Sdl3Platform reports the nativeFileDialog capability on iOS and web builds, where SDL has no usable file-dialog backend — The SDL3 platform sets nativeFileDialog unconditionally, so on iOS and the web the platform says a dialog can be shown while FileDialog::getIsSupportedProperty says it cannot, and FileDialog::Show* reach SDL, which fails
- CNA-BUG-182: Win32Dialogs file dialogs run modally inside Show* and invoke the callback before returning, contrary to the IPlatformDialogs timing contract — On the native WIN32 platform the three file dialogs run modally on the calling thread and call the result callback before returning, while IPlatformDialogs and FileDialog promise an asynchronous callback after the call r
- CNA-BUG-183: The SDL3 file-dialog and tray-entry trampolines call application callbacks without a try/catch, so a throwing handler unwinds through SDL's C code — ResultTrampoline and EntryClickTrampoline invoke the application's std::function directly from SDL C callbacks, so an exception escaping it crosses SDL frames (undefined behaviour, typically std::terminate).
- CNA-GAP-057: CNA::Devices host dialogs are minimal: message boxes are unparented with no default or cancel button, tray icons have no image API, and UrlLauncher passes any string to the OS — MessageBox passes a null parent window and SDL buttons without RETURNKEY/ESCAPEKEY defaults, SystemTray offers only a tooltip and menu, and UrlLauncher::Open forwards its argument unvalidated to the platform (ShellExecut
- CNA-VGAP-007: The SDL3 camera provider, system-tray icon and file-dialog result path have no test that drives them — Sdl3Camera.cpp, Sdl3Tray.cpp and the SDL3 dialog trampoline are exercised by no test beyond capability presence and empty-callback refusal, and the CNA::Devices suites above them use canned platforms.
- CNA-VGAP-037: CNA::Devices extension tests use canned platforms or check only that the host call does not fail, and exist only in a CNA_DEVICES=ON build — Camera, FileDialog, MessageBox and SystemTray tests drive canned platform services; the Clipboard, DisplayInfo, Locale, PowerInfo, SystemInfo and UrlLauncher tests call the host but mostly assert only that nothing crashe
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- CNAEXT engine layer · Tutorial 117: the CNA::Devices layer · Native platforms: capabilities
- Architecture
- Platform architecture
- Maintainer workflow
- I need to modify a platform backend · I need to add a regression test · Ownership and lifetime master map · Thread and callback map
- Tests and validation
- Test architecture and change recipes · What to test after changing X
- Reference
- Module index · Test target index · CMake option index