Skip to content

Commit d6290fb

Browse files
io extension: align with coding conventions
Three convention-compliance fixes that don't change behaviour: 1. Rename IOFMT_USER_CASES_ -> IOFMT_USER_CASE_ to match the singular sibling pattern (IOFMT_BITVEC_CASE_, IOFMT_INT_CASE_, IOFMT_FLOAT_CASE_). 2. Add SUCCESS:/FAILURE: lines to the IOFMT_USER_CASE_ doc block per the "every public function and macro" rule -- they describe the compile- time behaviour of the hook (registers user arms in IOFMT's _Generic; mis-typed arms surface as compile/link errors). 3. Rewrite _read_Point2D / _read_Bounds / _read_Region to use StrIter with the peek-then-Must idiom from the conventions doc (matching the in-tree _read_u8 / _read_Str shape) instead of bare Zstr pointer walks. The guide example is updated to match so users copy the conventional shape. The Log.h include moves below Io.h (it transitively pulls Io.h, which would lock in the empty-fallback IOFMT_USER_CASE_ before our override). This constraint is now documented inline in the test and in the guide's "A Minimal Example" section. 105-test suite still green.
1 parent 9f09665 commit d6290fb

3 files changed

Lines changed: 145 additions & 77 deletions

File tree

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

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Extending IO with User-Defined Types"
33
date: 2026-06-03
4-
description: "Plug your own types into WriteFmt / StrReadFmt / FReadFmt through the IOFMT_USER_CASES_ extension hook, with no call-site wrapper and full compile-time type safety."
4+
description: "Plug your own types into WriteFmt / StrReadFmt / FReadFmt through the IOFMT_USER_CASE_ extension hook, with no call-site wrapper and full compile-time type safety."
55
authors:
66
- siddharth-mishra
77
tags:
@@ -19,7 +19,7 @@ For built-ins (`i32`, `f64`, `Str`, `Zstr`, `BitVec`, `Int`, `Float`, …) those
1919
WriteFmt("got {}", IO_WRAP(my_widget)); // not what we want
2020
```
2121
22-
`IOFMT_USER_CASES_` removes that wrapper. After a one-time hook in your project header, every call site reads exactly the same as if the type were built in:
22+
`IOFMT_USER_CASE_` removes that wrapper. After a one-time hook in your project header, every call site reads exactly the same as if the type were built in:
2323
2424
```c
2525
WriteFmt("got {}", my_widget); // dispatches through your writer
@@ -32,9 +32,14 @@ Dispatch stays compile-time, type-safe (unknown types fail to match), and zero-o
3232
The TU layout has four blocks, in this order:
3333

3434
```c
35-
// 1. Headers that introduce your type's dependencies.
35+
// 1. Headers that introduce your type's dependencies and the iterator
36+
// helpers your reader will use. NOTHING that transitively pulls in
37+
// <Misra/Std/Io.h> belongs here -- if Io.h's empty-fallback for
38+
// IOFMT_USER_CASE_ runs before our override, the override silently
39+
// loses (the redefinition warning is the friendly outcome).
3640
#include <Misra/Std/Allocator/Default.h>
3741
#include <Misra/Std/Container/Str.h>
42+
#include <Misra/Std/Utility/StrIter.h>
3843
#include <Misra/Std/Zstr.h>
3944
#include <Misra/Types.h>
4045

@@ -46,29 +51,51 @@ typedef struct {
4651

4752
// 3. The extension hook -- one arm per user type, comma-terminated.
4853
// MUST be defined before <Misra/Std/Io.h> is processed.
49-
#define IOFMT_USER_CASES_(x, addr) \
54+
#define IOFMT_USER_CASE_(x, addr) \
5055
Point2D : TO_TYPE_SPECIFIC_IO(Point2D, addr),
5156

52-
// 4. The IO header, then your writer / reader definitions.
57+
// 4. The IO header (and any Io.h-pulling headers such as Log.h), then
58+
// your writer / reader definitions.
5359
#include <Misra/Std/Io.h>
60+
#include <Misra/Std/Log.h>
5461

5562
bool _write_Point2D(Str *o, FmtInfo *info, Point2D *p) {
5663
(void)info;
5764
return StrAppendFmt(o, "({}, {})", p->x, p->y);
5865
}
5966

67+
// Readers walk the input through a StrIter rather than bare pointer
68+
// arithmetic -- matches the in-tree convention (`_read_u8`, `_read_Str`)
69+
// and means whitespace / delimiter handling can lean on the peek-then-
70+
// Must idiom. When delegating to a built-in reader like `_read_i32`,
71+
// hand it `StrIterDataAt(&si, StrIterIndex(&si))` and rebuild the
72+
// iterator from the Zstr it returns.
6073
Zstr _read_Point2D(Zstr i, FmtInfo *info, Point2D *p) {
6174
(void)info;
62-
while (*i == ' ' || *i == '\t') i++;
63-
if (*i != '(') return i;
64-
i++;
75+
StrIter si = StrIterFromZstr(i);
76+
char c = 0;
77+
78+
while (StrIterPeek(&si, &c) && IS_SPACE(c)) StrIterMustNext(&si);
79+
if (!StrIterPeek(&si, &c) || c != '(') {
80+
return StrIterDataAt(&si, StrIterIndex(&si));
81+
}
82+
StrIterMustNext(&si);
83+
6584
FmtInfo inner = {0};
66-
i = _read_i32(i, &inner, &p->x);
67-
while (*i == ' ' || *i == '\t' || *i == ',') i++;
68-
i = _read_i32(i, &inner, &p->y);
69-
while (*i == ' ' || *i == '\t') i++;
70-
if (*i == ')') i++;
71-
return i;
85+
Zstr rest = _read_i32(StrIterDataAt(&si, StrIterIndex(&si)),
86+
&inner, &p->x);
87+
88+
si = StrIterFromZstr(rest);
89+
while (StrIterPeek(&si, &c) && (IS_SPACE(c) || c == ',')) {
90+
StrIterMustNext(&si);
91+
}
92+
rest = _read_i32(StrIterDataAt(&si, StrIterIndex(&si)),
93+
&inner, &p->y);
94+
95+
si = StrIterFromZstr(rest);
96+
while (StrIterPeek(&si, &c) && IS_SPACE(c)) StrIterMustNext(&si);
97+
if (StrIterPeek(&si, &c) && c == ')') StrIterMustNext(&si);
98+
return StrIterDataAt(&si, StrIterIndex(&si));
7299
}
73100
```
74101
@@ -104,7 +131,7 @@ Zstr _read_T (Zstr i, FmtInfo *info, T *self);
104131
| Field | Meaning |
105132
| --- | --- |
106133
| `Str *o` | Output sink for the writer. Append with `StrAppendFmt(o, …)`, `StrAppend(o, …)`, etc. |
107-
| `Zstr i` | Input cursor for the reader. Treat as `const char *` -- advance with `i++` or by returning a pointer further along. |
134+
| `Zstr i` | Input cursor for the reader. Wrap with `StrIterFromZstr(i)` and walk it through `StrIterPeek` / `StrIterMustNext`; return the post-parse position via `StrIterDataAt(&si, StrIterIndex(&si))`. |
108135
| `FmtInfo *info` | Parsed `{...}` spec (width, precision, flags). Ignore with `(void)info;` if you do not care. |
109136
| `T *self` | Pointer to the user value. The IOFMT machinery passes `&val` of the original argument. |
110137
| **Writer return** | `true` on success, `false` on allocation failure (the IO pipeline propagates). |
@@ -116,11 +143,11 @@ If you only need printing, define `_read_T` as a stub that returns `i` unchanged
116143

117144
## Include-Order Rule
118145

119-
`IOFMT_USER_CASES_` works because of this fragment near the top of `<Misra/Std/Io.h>`:
146+
`IOFMT_USER_CASE_` works because of this fragment near the top of `<Misra/Std/Io.h>`:
120147

121148
```c
122-
#ifndef IOFMT_USER_CASES_
123-
# define IOFMT_USER_CASES_(x, addr) /* empty -- override before include */
149+
#ifndef IOFMT_USER_CASE_
150+
# define IOFMT_USER_CASE_(x, addr) /* empty -- override before include */
124151
#endif
125152
```
126153

@@ -129,11 +156,11 @@ If you `#include <Misra/Std/Io.h>` *before* defining the hook, the empty fallbac
129156
- `WriteFmt("{}", my_widget);` fails to compile with *"controlling expression type not compatible with any generic association"* -- this is the safe failure mode.
130157
- Or, if a built-in arm coincidentally matches (e.g. you wrap a single `i32`), the wrong writer runs silently.
131158

132-
Practical rule: put the `#define IOFMT_USER_CASES_(...)` line in **a single project-wide header**, include nothing else in that header except what your types' declarations need, and include it before any other Misra IO surface in every TU that formats your types.
159+
Practical rule: put the `#define IOFMT_USER_CASE_(...)` line in **a single project-wide header**, include nothing else in that header except what your types' declarations need, and include it before any other Misra IO surface in every TU that formats your types.
133160

134161
## Forward Declarations Inside Writer Bodies
135162

136-
If your writer calls `StrAppendFmt`, `WriteFmt`, etc. with arguments of its own user type *or any other type covered by `IOFMT_USER_CASES_`*, the `_Generic` arms inside that nested expansion need every user `_write_T` / `_read_T` symbol to be visible already. Two patterns work:
163+
If your writer calls `StrAppendFmt`, `WriteFmt`, etc. with arguments of its own user type *or any other type covered by `IOFMT_USER_CASE_`*, the `_Generic` arms inside that nested expansion need every user `_write_T` / `_read_T` symbol to be visible already. Two patterns work:
137164

138165
```c
139166
// Pattern A -- forward-declare both functions before either body.
@@ -155,7 +182,7 @@ Pattern A is the safe default once you have more than one user type.
155182
## Recommended Layout: One `Io.h` / `Io.c` Pair per Project
156183

157184
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_`
185+
project: every TU that formats your types needs the same `IOFMT_USER_CASE_`
159186
list visible, every `_write_T` / `_read_T` symbol needs to be linkable from
160187
those TUs, and every user type in the list needs its struct declaration in
161188
scope so the `_Generic` arm can mention it.
@@ -165,7 +192,7 @@ extension in a single project-internal pair:
165192

166193
```
167194
MyApp/Io.h -- forward-declares every user type's _write_T / _read_T,
168-
defines IOFMT_USER_CASES_, then #include <Misra/Std/Io.h>
195+
defines IOFMT_USER_CASE_, then #include <Misra/Std/Io.h>
169196
MyApp/Io.c -- defines the bodies for every _write_T / _read_T
170197
```
171198

@@ -180,7 +207,7 @@ keep them next to each type's own module, and it will compile and link.
180207
But maintenance gets harder fast:
181208

182209
- **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
210+
`IOFMT_USER_CASE_` defined with `my_t`'s arm. If one TU's hook lags
184211
behind, that TU silently fails to match (compile error if you're lucky,
185212
wrong-arm dispatch if a built-in coincidentally matches the type).
186213
- **Forward-declaration sprawl.** Pattern A (forward-declare every
@@ -200,16 +227,16 @@ to their data.
200227

201228
## Composing Across Libraries
202229

203-
`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*:
230+
`IOFMT_USER_CASE_` 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*:
204231

205232
```c
206233
// LibB/Io.h
207-
#include "LibA/Io.h" // already defined IOFMT_USER_CASES_
234+
#include "LibA/Io.h" // already defined IOFMT_USER_CASE_
208235

209-
#define IOFMT_USER_CASES_PREV_ IOFMT_USER_CASES_
210-
#undef IOFMT_USER_CASES_
211-
#define IOFMT_USER_CASES_(x, addr) \
212-
IOFMT_USER_CASES_PREV_(x, addr) \
236+
#define IOFMT_USER_CASE_PREV_ IOFMT_USER_CASE_
237+
#undef IOFMT_USER_CASE_
238+
#define IOFMT_USER_CASE_(x, addr) \
239+
IOFMT_USER_CASE_PREV_(x, addr) \
213240
LibBType : TO_TYPE_SPECIFIC_IO(LibBType, addr),
214241

215242
#include <Misra/Std/Io.h>
@@ -219,7 +246,7 @@ Each library header in the chain accepts whatever the previous one defined and e
219246

220247
## What You Lose vs Built-Ins
221248

222-
| Aspect | Built-in (`i32`, `Str`, …) | User type via `IOFMT_USER_CASES_` |
249+
| Aspect | Built-in (`i32`, `Str`, …) | User type via `IOFMT_USER_CASE_` |
223250
| --- | --- | --- |
224251
| Call-site syntax | `WriteFmt("{}", x)` | `WriteFmt("{}", x)` (identical) |
225252
| Type checking | Compile-time, no `default` arm | Same |
@@ -228,11 +255,11 @@ Each library header in the chain accepts whatever the previous one defined and e
228255
| Spec parsing (`{:.2}` etc.) | Honored by the runtime, applied by the writer | Same `FmtInfo` reaches your writer; honoring it is your responsibility |
229256
| Type erasure | None | `void *` cast inside the function entry; the macro ensures the cast is correct by construction |
230257

231-
The single cost is that the writer / reader bodies receive `void *` data (the function-pointer cast in `TO_TYPE_SPECIFIC_IO_IMPL` widens to `void *` for storage and the call site re-narrows). The narrowing back to `T *` is type-safe because the `_Generic` arm and the symbol name are bound at the same point: the only way for the wrong pointer to reach `_write_T` is if you mis-spell the type tag in `IOFMT_USER_CASES_`, which the compiler would reject anyway.
258+
The single cost is that the writer / reader bodies receive `void *` data (the function-pointer cast in `TO_TYPE_SPECIFIC_IO_IMPL` widens to `void *` for storage and the call site re-narrows). The narrowing back to `T *` is type-safe because the `_Generic` arm and the symbol name are bound at the same point: the only way for the wrong pointer to reach `_write_T` is if you mis-spell the type tag in `IOFMT_USER_CASE_`, which the compiler would reject anyway.
232259

233260
## Known Limitations
234261

235-
- **One hook per TU.** Multiple `#define IOFMT_USER_CASES_` in the same TU is a redefinition warning unless you use the rename-extend pattern above.
262+
- **One hook per TU.** Multiple `#define IOFMT_USER_CASE_` in the same TU is a redefinition warning unless you use the rename-extend pattern above.
236263
- **No spec-string dispatch.** `WriteFmt("{Widget:.2}", x)` does not look up "Widget" anywhere; the type tag lives at the C-type level. Your writer interprets the spec freely via `FmtInfo`.
237264
- **Header-only.** The hook controls macro expansion; you cannot register a type from a `.c` file alone -- consumers' TUs need the macro visible.
238265
- **No partial implementations.** Even a write-only user type must have a `_read_T` symbol (a trivial stub that returns `i` is fine). The constraint is symbol resolution, not call coverage.

Include/Misra/Std/Io.h

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ Float:
245245
/// `TO_TYPE_SPECIFIC_IO(T, addr)`.
246246
///
247247
/// EXAMPLE (per-TU or per-project header):
248-
/// #define IOFMT_USER_CASES_(x, addr)
248+
/// #define IOFMT_USER_CASE_(x, addr)
249249
/// MyWidget : TO_TYPE_SPECIFIC_IO(MyWidget, addr),
250250
/// OtherT : TO_TYPE_SPECIFIC_IO(OtherT, addr),
251251
/// #include <Misra/Std/Io.h>
@@ -258,9 +258,21 @@ Float:
258258
/// See `Docs/.../extending-io-with-user-types.md` for the full guide,
259259
/// including the multi-library chain-extension pattern.
260260
///
261+
/// SUCCESS: Once defined before `<Misra/Std/Io.h>` is processed in a TU,
262+
/// every listed type becomes an explicit `_Generic` arm in
263+
/// `IOFMT(x)` for that TU; subsequent `WriteFmt` / `StrReadFmt`
264+
/// / `FReadFmt` / `BufReadFmt` calls dispatch user values
265+
/// through the matching `_write_T` / `_read_T` symbols with
266+
/// the same compile-time selection as in-tree types.
267+
/// FAILURE: Cannot fail at the macro-expansion layer. Mis-typed arms or
268+
/// missing `_write_T` / `_read_T` symbols surface as compile or
269+
/// link errors at the call site; a user value whose type is
270+
/// not listed produces the standard `_Generic`
271+
/// "no matching association" diagnostic.
272+
///
261273
/// TAGS: I/O, Generic, Extension, Macro
262-
#ifndef IOFMT_USER_CASES_
263-
# define IOFMT_USER_CASES_(x, addr) /* empty -- override before include */
274+
#ifndef IOFMT_USER_CASE_
275+
# define IOFMT_USER_CASE_(x, addr) /* empty -- override before include */
264276
#endif
265277

266278
///
@@ -285,7 +297,7 @@ Float:
285297
(x), \
286298
TypeSpecificIO: (x), \
287299
Str: TO_TYPE_SPECIFIC_IO(Str, &(x)), \
288-
IOFMT_FLOAT_CASE_(x, &(x)) IOFMT_INT_CASE_(x, &(x)) IOFMT_BITVEC_CASE_(x, &(x)) IOFMT_USER_CASES_(x, &(x)) \
300+
IOFMT_FLOAT_CASE_(x, &(x)) IOFMT_INT_CASE_(x, &(x)) IOFMT_BITVEC_CASE_(x, &(x)) IOFMT_USER_CASE_(x, &(x)) \
289301
Zstr: TO_TYPE_SPECIFIC_IO(Zstr, &(x)), \
290302
char *: TO_TYPE_SPECIFIC_IO(Zstr, &(x)), \
291303
unsigned char: TO_TYPE_SPECIFIC_IO(u8, &(x)), \
@@ -310,7 +322,7 @@ Float:
310322
TypeSpecificIO: (x), \
311323
Str: TO_TYPE_SPECIFIC_IO(Str, (void *)&(x)), \
312324
IOFMT_FLOAT_CASE_(x, (void *)&(x)) IOFMT_INT_CASE_(x, (void *)&(x)) IOFMT_BITVEC_CASE_(x, (void *)&(x)) \
313-
IOFMT_USER_CASES_(x, (void *)&(x)) Zstr: TO_TYPE_SPECIFIC_IO(Zstr, (void *)&(x)), \
325+
IOFMT_USER_CASE_(x, (void *)&(x)) Zstr: TO_TYPE_SPECIFIC_IO(Zstr, (void *)&(x)), \
314326
char *: TO_TYPE_SPECIFIC_IO(Zstr, (void *)&(x)), \
315327
unsigned char: TO_TYPE_SPECIFIC_IO(u8, (void *)&(x)), \
316328
unsigned short: TO_TYPE_SPECIFIC_IO(u16, (void *)&(x)), \

0 commit comments

Comments
 (0)