You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: Docs/content/english/guides/extending-io-with-user-types.md
+59-32Lines changed: 59 additions & 32 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
---
2
2
title: "Extending IO with User-Defined Types"
3
3
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."
5
5
authors:
6
6
- siddharth-mishra
7
7
tags:
@@ -19,7 +19,7 @@ For built-ins (`i32`, `f64`, `Str`, `Zstr`, `BitVec`, `Int`, `Float`, …) those
19
19
WriteFmt("got {}", IO_WRAP(my_widget)); // not what we want
20
20
```
21
21
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:
23
23
24
24
```c
25
25
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
32
32
The TU layout has four blocks, in this order:
33
33
34
34
```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).
36
40
#include<Misra/Std/Allocator/Default.h>
37
41
#include<Misra/Std/Container/Str.h>
42
+
#include<Misra/Std/Utility/StrIter.h>
38
43
#include<Misra/Std/Zstr.h>
39
44
#include<Misra/Types.h>
40
45
@@ -46,29 +51,51 @@ typedef struct {
46
51
47
52
// 3. The extension hook -- one arm per user type, comma-terminated.
48
53
// MUST be defined before <Misra/Std/Io.h> is processed.
49
-
#defineIOFMT_USER_CASES_(x, addr) \
54
+
#defineIOFMT_USER_CASE_(x, addr) \
50
55
Point2D : TO_TYPE_SPECIFIC_IO(Point2D, addr),
51
56
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
while (StrIterPeek(&si, &c) && IS_SPACE(c)) StrIterMustNext(&si);
97
+
if (StrIterPeek(&si, &c) && c == ')') StrIterMustNext(&si);
98
+
return StrIterDataAt(&si, StrIterIndex(&si));
72
99
}
73
100
```
74
101
@@ -104,7 +131,7 @@ Zstr _read_T (Zstr i, FmtInfo *info, T *self);
104
131
| Field | Meaning |
105
132
| --- | --- |
106
133
|`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))`. |
108
135
|`FmtInfo *info`| Parsed `{...}` spec (width, precision, flags). Ignore with `(void)info;` if you do not care. |
109
136
|`T *self`| Pointer to the user value. The IOFMT machinery passes `&val` of the original argument. |
110
137
|**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
116
143
117
144
## Include-Order Rule
118
145
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>`:
120
147
121
148
```c
122
-
#ifndefIOFMT_USER_CASES_
123
-
# define IOFMT_USER_CASES_(x, addr) /* empty -- override before include */
149
+
#ifndefIOFMT_USER_CASE_
150
+
# define IOFMT_USER_CASE_(x, addr) /* empty -- override before include */
124
151
#endif
125
152
```
126
153
@@ -129,11 +156,11 @@ If you `#include <Misra/Std/Io.h>` *before* defining the hook, the empty fallbac
129
156
-`WriteFmt("{}", my_widget);` fails to compile with *"controlling expression type not compatible with any generic association"* -- this is the safe failure mode.
130
157
- Or, if a built-in arm coincidentally matches (e.g. you wrap a single `i32`), the wrong writer runs silently.
131
158
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.
133
160
134
161
## Forward Declarations Inside Writer Bodies
135
162
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:
137
164
138
165
```c
139
166
// 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.
155
182
## Recommended Layout: One `Io.h` / `Io.c` Pair per Project
156
183
157
184
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_`
159
186
list visible, every `_write_T` / `_read_T` symbol needs to be linkable from
160
187
those TUs, and every user type in the list needs its struct declaration in
161
188
scope so the `_Generic` arm can mention it.
@@ -165,7 +192,7 @@ extension in a single project-internal pair:
165
192
166
193
```
167
194
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>
169
196
MyApp/Io.c -- defines the bodies for every _write_T / _read_T
170
197
```
171
198
@@ -180,7 +207,7 @@ keep them next to each type's own module, and it will compile and link.
180
207
But maintenance gets harder fast:
181
208
182
209
-**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
184
211
behind, that TU silently fails to match (compile error if you're lucky,
185
212
wrong-arm dispatch if a built-in coincidentally matches the type).
186
213
-**Forward-declaration sprawl.** Pattern A (forward-declare every
@@ -200,16 +227,16 @@ to their data.
200
227
201
228
## Composing Across Libraries
202
229
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*:
204
231
205
232
```c
206
233
// LibB/Io.h
207
-
#include"LibA/Io.h"// already defined IOFMT_USER_CASES_
234
+
#include"LibA/Io.h"// already defined IOFMT_USER_CASE_
208
235
209
-
#defineIOFMT_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
+
#defineIOFMT_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) \
213
240
LibBType : TO_TYPE_SPECIFIC_IO(LibBType, addr),
214
241
215
242
#include<Misra/Std/Io.h>
@@ -219,7 +246,7 @@ Each library header in the chain accepts whatever the previous one defined and e
219
246
220
247
## What You Lose vs Built-Ins
221
248
222
-
| Aspect | Built-in (`i32`, `Str`, …) | User type via `IOFMT_USER_CASES_`|
249
+
| Aspect | Built-in (`i32`, `Str`, …) | User type via `IOFMT_USER_CASE_`|
| 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
228
255
| Spec parsing (`{:.2}` etc.) | Honored by the runtime, applied by the writer | Same `FmtInfo` reaches your writer; honoring it is your responsibility |
229
256
| Type erasure | None |`void *` cast inside the function entry; the macro ensures the cast is correct by construction |
230
257
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.
232
259
233
260
## Known Limitations
234
261
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.
236
263
-**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`.
237
264
-**Header-only.** The hook controls macro expansion; you cannot register a type from a `.c` file alone -- consumers' TUs need the macro visible.
238
265
-**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.
0 commit comments