Skip to content

Commit 9f09665

Browse files
docs: recommend a single project Io.h/Io.c pair for user IOFMT
Adds a "Recommended Layout" section that names the one-pair pattern as the default and explains the maintenance pitfalls of splitting (per-TU macro drift, forward-decl sprawl across TUs, include-order auditing). Splitting is still supported; the guide now spells out the trade-off so users don't accidentally adopt the harder layout.
1 parent b53ac87 commit 9f09665

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

Docs/content/english/guides/extending-io-with-user-types.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,52 @@ bool _write_Point2D(Str *o, FmtInfo *info, Point2D *p) { … }
152152

153153
Pattern A is the safe default once you have more than one user type.
154154

155+
## Recommended Layout: One `Io.h` / `Io.c` Pair per Project
156+
157+
The mechanism above is per-TU, but the natural unit of organisation is the
158+
project: every TU that formats your types needs the same `IOFMT_USER_CASES_`
159+
list visible, every `_write_T` / `_read_T` symbol needs to be linkable from
160+
those TUs, and every user type in the list needs its struct declaration in
161+
scope so the `_Generic` arm can mention it.
162+
163+
The cleanest way to satisfy all three constraints is to keep your IO
164+
extension in a single project-internal pair:
165+
166+
```
167+
MyApp/Io.h -- forward-declares every user type's _write_T / _read_T,
168+
defines IOFMT_USER_CASES_, then #include <Misra/Std/Io.h>
169+
MyApp/Io.c -- defines the bodies for every _write_T / _read_T
170+
```
171+
172+
Every TU in your project then has exactly one extra include:
173+
174+
```c
175+
#include "MyApp/Io.h" // pulls in everything: types, hook, Misra/Std/Io.h
176+
```
177+
178+
You can absolutely split the writers/readers across multiple `.c` files, or
179+
keep them next to each type's own module, and it will compile and link.
180+
But maintenance gets harder fast:
181+
182+
- **Macro drift.** Each TU that calls `WriteFmt(..., my_t)` needs
183+
`IOFMT_USER_CASES_` defined with `my_t`'s arm. If one TU's hook lags
184+
behind, that TU silently fails to match (compile error if you're lucky,
185+
wrong-arm dispatch if a built-in coincidentally matches the type).
186+
- **Forward-declaration sprawl.** Pattern A (forward-declare every
187+
user-type writer/reader before any body) becomes "every TU has to
188+
forward-declare every user type's writer/reader" once nested writers
189+
cross TU boundaries. Centralising the forward decls in one header
190+
removes the bookkeeping.
191+
- **Include-order auditing.** The "define hook before `<Misra/Std/Io.h>`"
192+
rule is easy to violate when the IO header is pulled transitively. A
193+
single project IO header that wraps the rule once eliminates the audit
194+
surface entirely.
195+
196+
If you do split, treat `MyApp/Io.h` as authoritative for the hook + forward
197+
decls and treat the per-module `.c` files purely as homes for the bodies.
198+
That keeps the macro discipline in one place and lets the bodies live next
199+
to their data.
200+
155201
## Composing Across Libraries
156202

157203
`IOFMT_USER_CASES_` is a single preprocessor symbol. If two libraries both want to publish IO-able types, the consumer's chain has to thread them together. The usual idiom is *rename-then-extend*:

0 commit comments

Comments
 (0)