Skip to content

Commit 8a58783

Browse files
zstr: inline char * synonym arm on every _Generic Zstr dispatch
MSVC's C `_Generic` follows the C standard, which types string literals as `char[N]` decaying to `char *`. The codebase had been relying on gcc/clang's `-Wwrite-strings` (a non-standard extension) to retype literals as `const char *` so the `Zstr` arms would match; on MSVC pure-C the dispatch fails. `/Zc:strictStrings` is a C++-only flag with no effect in C mode. Add an inline `char *:` synonym arm next to every `Zstr:` arm across 17 headers (49 sites). Both arms route through the same backend with a `(Zstr)` cast: `Zstr` is `const char *`, so the input cannot be mutated through the parameter and the library's read-only invariant on `Zstr` arguments is preserved. No wrapper macro hides the `_Generic` -- inlining both arms at every site is the project convention. The convention is documented in CODING-CONVENTIONS.md and the Zstr.h doc comment. Docs: - CODING-CONVENTIONS.md: rule rewritten; StrStartsWith and FileGetSize skeleton examples updated to show the Zstr + char * pair; the Compiler-flags section corrected to document /Zc:strictStrings as C++-only with no effect in C mode. - Zstr.h: typedef doc comment updated. - meson.build: drop the no-op /Zc:strictStrings from the MSVC branch; inline NOTE explaining why it is intentionally absent. Also folded in: - Tests/Std/MachO.c: bind _NSGetExecutablePath's char path[4096] to a `Zstr path` local before passing to `MachoOpen` so the Darwin test builds under the same Zstr dispatch. Verified: - Linux gcc: 104/104 tests pass. - Mac clang: 99/99 tests pass (MachO suite exercises both the path binding and the StrPushBackMany("\\x") hex-escape shape). - MSVC: pending CI.
1 parent d29c1ff commit 8a58783

19 files changed

Lines changed: 196 additions & 124 deletions

File tree

CODING-CONVENTIONS.md

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,28 @@ of the codebase to see them in action.
2323
`Str`, `Vec`, `Elf` — not `MisraBuf`, not `ElfFile`.
2424
- **Tool binaries** ship with a single short word as their name
2525
(see `Bin/Beam.c`, `Bin/Resolve.c`).
26-
- **C-strings everywhere use `Zstr`**, not `char *` / `const char *`.
27-
`Zstr` (`<Misra/Std/Zstr.h>`) is the project's name for a
28-
NUL-terminated C-string. This applies to internal helpers too — `Zstr`
29-
is the *only* C-string type in the codebase, *including* in `_Generic`
30-
dispatch arms. There is no `char *` / `const char *` carve-out. The
31-
build sets `-Wwrite-strings` (gcc/clang/clang-cl) and
32-
`/Zc:strictStrings` (msvc/clang-cl) so string literals carry type
33-
`const char *` (= `Zstr`), which lets `_Generic((literal), Zstr: ...)`
34-
match literals directly without a bare-`char *` arm. See the
35-
*Compiler flags* section below.
26+
- **C-strings everywhere use `Zstr`**, not `char *` / `const char *`,
27+
in declarations / parameters / return types / fields. `Zstr`
28+
(`<Misra/Std/Zstr.h>`) is the project's name for a NUL-terminated
29+
C-string. The build sets `-Wwrite-strings` (gcc/clang/clang-cl) so
30+
string literals carry type `const char *` (= `Zstr`) and `char *`
31+
variables get rejected at the call site.
32+
- **`_Generic` dispatch arms have a `char *` synonym next to every
33+
`Zstr` arm.** This is the one carve-out, and it exists because
34+
MSVC's C `_Generic` follows the C standard (string literals are
35+
`char[N]` decaying to `char *`); `/Zc:strictStrings` is a C++-only
36+
flag with no effect in C mode. Every site that dispatches on a
37+
string-shaped argument inlines both arms with identical bodies:
38+
39+
_Generic((s),
40+
Zstr: foo_zstr((Zstr)(s)),
41+
char *: foo_zstr((Zstr)(s)))
42+
43+
Inline the pair at every site; do NOT introduce a wrapper macro
44+
that hides the `_Generic`. The library's read-only invariant for
45+
`Zstr` parameters is preserved -- the `char *` arm casts to `Zstr`
46+
before dispatching, and `Zstr` is `const char *`, so the input
47+
cannot be mutated through the parameter.
3648
- **`Cstr` is a naming-suffix, not a type.** The `Cstr` form of an API
3749
takes `(Zstr, size)` — a non-NUL-terminated view of memory, or a
3850
NUL-terminated string truncated at an explicit length. See
@@ -139,12 +151,16 @@ of the codebase to see them in action.
139151
#define StrStartsWith(...) MISRA_OVERLOAD(StrStartsWith, __VA_ARGS__)
140152
#define StrStartsWith_2(s, prefix) \
141153
_Generic((prefix), \
142-
Str *: str_starts_with_str ((s), (const Str *)(prefix)), \
143-
Zstr: str_starts_with_zstr((s), (Zstr)(prefix)))
154+
Str *: str_starts_with_str ((s), (const Str *)(prefix)), \
155+
Zstr: str_starts_with_zstr((s), (Zstr)(prefix)), \
156+
char *: str_starts_with_zstr((s), (Zstr)(prefix)))
144157
#define StrStartsWith_3(s, prefix, prefix_len) \
145158
str_starts_with_cstr((s), (Zstr)(prefix), (prefix_len))
146159
```
147160
161+
The `char *` arm is the MSVC-portability synonym for `Zstr` — see
162+
the "C-strings everywhere use `Zstr`" rule above.
163+
148164
The user-facing surface is just `StrStartsWith` — callers never type
149165
`*Cstr` / `*Zstr` suffixes. Same applies whenever a `Zstr` parameter
150166
shows up: add the `Str` overload (or surface that the function
@@ -273,20 +289,24 @@ of the codebase to see them in action.
273289
`default:` arm that silently casts a wrong type to the function's
274290
expected one. Any other input type should trigger a compile-time
275291
`_Generic` mismatch.
276-
- Path / string dispatch: `Str *` and `Zstr` only — no bare `char *`
277-
or `const char *` arms. The mandatory `-Wwrite-strings` /
278-
`/Zc:strictStrings` build flags make string literals match the
279-
`Zstr` arm directly.
292+
- Path / string dispatch: `Str *` plus the `Zstr` + `char *` pair
293+
(the two share a body — see the "C-strings everywhere use `Zstr`"
294+
rule for why MSVC C requires the `char *` synonym). No
295+
`const char *` arms.
280296
- Container-out dispatch: `Buf *`, `Str *`.
281297

282298
Skeleton:
283299
```c
284300
#define FileGetSize(path) \
285301
_Generic((path), \
286-
Str *: file_get_size(StrBegin((Str *)(path))), \
287-
Zstr: file_get_size((Zstr)(path)))
302+
Str *: file_get_size(StrBegin((Str *)(path))), \
303+
Zstr: file_get_size((Zstr)(path)), \
304+
char *: file_get_size((Zstr)(path)))
288305
```
289306
307+
The `char *` arm is the MSVC-portability synonym for `Zstr` — see
308+
the "C-strings everywhere use `Zstr`" rule.
309+
290310
For functions that already have an arg-count variant of the API
291311
(`*Cstr` form taking `(Zstr, size)`), combine the type dispatch
292312
above with `MISRA_OVERLOAD` for arg count — see the
@@ -431,23 +451,34 @@ These are baked into `meson.build`'s `common_c_args`; downstream consumers
431451
that bypass the meson build must enable equivalents themselves or the
432452
`_Generic` dispatch the convention relies on will silently mis-match.
433453
434-
- **`-Wwrite-strings`** (gcc, clang, clang-cl) and
435-
**`/Zc:strictStrings`** (msvc, clang-cl) — gives string literals the
436-
type `const char *` (= `Zstr`) instead of bare `char *`. Required so
437-
`_Generic` dispatch on string types can list `Zstr` as a match arm
438-
and still accept literal callers. Without these flags, `"foo"` has
439-
type `char *` and the `Zstr`-only dispatch misses.
454+
- **`-Wwrite-strings`** (gcc, clang, clang-cl) — gives string literals
455+
the type `const char *` (= `Zstr`) on these toolchains so a `char *`
456+
variable or parameter cannot be silently initialised from a literal.
457+
This catches code-smell ("I have a mutable buffer where a `Zstr`
458+
belongs") at the call site rather than at runtime.
459+
- **`/Zc:strictStrings` is NOT useful in C mode.** Microsoft's docs are
460+
explicit: the flag enforces C++ const qualifications on string
461+
literals and has no effect on C compilation. Pure-C MSVC therefore
462+
follows the C standard, which types literals as `char[N]` decaying
463+
to `char *`. To stay portable across gcc/clang/MSVC, every
464+
`_Generic` dispatch on a string-shaped argument inlines a `char *`
465+
synonym arm next to the `Zstr` arm — see the "C-strings everywhere
466+
use `Zstr`" rule.
440467
- **`-Wuninitialized` / `-Wmaybe-uninitialized` / `-Werror=` on both** —
441468
enforces the "initialise at declaration" convention. Listed for
442469
completeness; not new in this section.
443470
444-
Verified compiler behaviour at the time these flags were locked in:
445-
446-
- GCC and Clang: empirically tested. `_Generic((literal), Zstr: ...)`
447-
matches under `-Wwrite-strings` and does not match without it.
448-
- MSVC: docs-confirmed (`/Zc:strictStrings`) — Windows-CI smoke test
449-
is what locks this in for production. clang-cl honours both `-W`
450-
and `/Zc` spellings; we pass both in the MSVC branch.
471+
Verified compiler behaviour:
472+
473+
- GCC and Clang: empirically tested. With `-Wwrite-strings`,
474+
`_Generic((literal), Zstr: ...)` matches; without it, the literal
475+
is `char *` and only the `char *` arm matches.
476+
- MSVC (cl) in C mode: empirically observed. String literal is
477+
`char *` regardless of `/Zc:strictStrings`. The `char *` arm is the
478+
only one that matches; the `Zstr` arm is unreachable for literals
479+
but still useful for `Zstr`-typed locals / parameters.
480+
- clang-cl honours both `-W` and `/Zc` spellings; the codebase passes
481+
both for parity.
451482
452483
## Commits and pre-commit
453484

Include/Misra/Parsers/Dns.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ extern "C" {
119119
bool dns_build_query_zstr(DnsWireBuf *out, u16 id, Zstr name, DnsType type);
120120
bool dns_build_query_str(DnsWireBuf *out, u16 id, const Str *name, DnsType type);
121121
#define DnsBuildQuery(out, id, name, type) \
122-
_Generic((name), Str *: dns_build_query_str, Zstr: dns_build_query_zstr)( \
122+
_Generic((name), Str *: dns_build_query_str, Zstr: dns_build_query_zstr, char *: dns_build_query_zstr)( \
123123
(out), \
124124
(id), \
125125
(name), \

Include/Misra/Parsers/Elf.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,15 @@ bool elf_open(Elf *out, Zstr path, Allocator *alloc);
208208
_Generic( \
209209
(path), \
210210
Str *: elf_open((out), (Zstr)StrBegin((Str *)(path)), MisraScope), \
211-
Zstr: elf_open((out), (Zstr)(path), MisraScope) \
211+
Zstr: elf_open((out), (Zstr)(path), MisraScope), \
212+
char *: elf_open((out), (Zstr)(path), MisraScope) \
212213
)
213214
#define ElfOpen_3(out, path, alloc) \
214215
_Generic( \
215216
(path), \
216217
Str *: elf_open((out), (Zstr)StrBegin((Str *)(path)), ALLOCATOR_OF(alloc)), \
217-
Zstr: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \
218+
Zstr: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \
219+
char *: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \
218220
)
219221

220222
///

Include/Misra/Parsers/Http.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ bool http_header_init_copy(void *dst, const void *src, const Allocator *alloc);
9393
HttpHeader *http_headers_find_zstr(HttpHeaders *headers, Zstr key);
9494
HttpHeader *http_headers_find_str(HttpHeaders *headers, const Str *key);
9595
#define HttpHeadersFind(headers, key) \
96-
_Generic((key), Str *: http_headers_find_str, Zstr: http_headers_find_zstr)( \
96+
_Generic((key), Str *: http_headers_find_str, Zstr: http_headers_find_zstr, char *: http_headers_find_zstr)( \
9797
(headers), \
9898
(key) \
9999
)
@@ -248,7 +248,7 @@ typedef struct HttpRequest {
248248
Zstr http_request_parse_zstr(HttpRequest *req, Zstr in);
249249
Zstr http_request_parse_str(HttpRequest *req, const Str *in);
250250
#define HttpRequestParse(req, in) \
251-
_Generic((in), Str *: http_request_parse_str, Zstr: http_request_parse_zstr)( \
251+
_Generic((in), Str *: http_request_parse_str, Zstr: http_request_parse_zstr, char *: http_request_parse_zstr)( \
252252
(req), \
253253
(in) \
254254
)

Include/Misra/Parsers/KvConfig.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg);
179179
Str kvconfig_get_zstr(KvConfig *cfg, Zstr key);
180180
Str kvconfig_get_str(KvConfig *cfg, const Str *key);
181181
#define KvConfigGet(cfg, key) \
182-
_Generic((key), Str *: kvconfig_get_str, Zstr: kvconfig_get_zstr)( \
182+
_Generic((key), Str *: kvconfig_get_str, Zstr: kvconfig_get_zstr, char *: kvconfig_get_zstr)( \
183183
(cfg), \
184184
(key) \
185185
)
@@ -196,7 +196,7 @@ Str kvconfig_get_str(KvConfig *cfg, const Str *key);
196196
Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key);
197197
Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key);
198198
#define KvConfigGetPtr(cfg, key) \
199-
_Generic((key), Str *: kvconfig_get_ptr_str, Zstr: kvconfig_get_ptr_zstr)( \
199+
_Generic((key), Str *: kvconfig_get_ptr_str, Zstr: kvconfig_get_ptr_zstr, char *: kvconfig_get_ptr_zstr)( \
200200
(cfg), \
201201
(key) \
202202
)
@@ -213,7 +213,7 @@ Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key);
213213
bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key);
214214
bool kvconfig_contains_str(KvConfig *cfg, const Str *key);
215215
#define KvConfigContains(cfg, key) \
216-
_Generic((key), Str *: kvconfig_contains_str, Zstr: kvconfig_contains_zstr)( \
216+
_Generic((key), Str *: kvconfig_contains_str, Zstr: kvconfig_contains_zstr, char *: kvconfig_contains_zstr)( \
217217
(cfg), \
218218
(key) \
219219
)
@@ -233,7 +233,7 @@ bool kvconfig_contains_str(KvConfig *cfg, const Str *key);
233233
bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value);
234234
bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value);
235235
#define KvConfigGetBool(cfg, key, value) \
236-
_Generic((key), Str *: kvconfig_get_bool_str, Zstr: kvconfig_get_bool_zstr)( \
236+
_Generic((key), Str *: kvconfig_get_bool_str, Zstr: kvconfig_get_bool_zstr, char *: kvconfig_get_bool_zstr)( \
237237
(cfg), \
238238
(key), \
239239
(value) \
@@ -252,7 +252,7 @@ bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value);
252252
bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value);
253253
bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value);
254254
#define KvConfigGetI64(cfg, key, value) \
255-
_Generic((key), Str *: kvconfig_get_i64_str, Zstr: kvconfig_get_i64_zstr)( \
255+
_Generic((key), Str *: kvconfig_get_i64_str, Zstr: kvconfig_get_i64_zstr, char *: kvconfig_get_i64_zstr)( \
256256
(cfg), \
257257
(key), \
258258
(value) \
@@ -271,7 +271,7 @@ bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value);
271271
bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value);
272272
bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value);
273273
#define KvConfigGetF64(cfg, key, value) \
274-
_Generic((key), Str *: kvconfig_get_f64_str, Zstr: kvconfig_get_f64_zstr)( \
274+
_Generic((key), Str *: kvconfig_get_f64_str, Zstr: kvconfig_get_f64_zstr, char *: kvconfig_get_f64_zstr)( \
275275
(cfg), \
276276
(key), \
277277
(value) \

Include/Misra/Parsers/MachO.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,15 @@ bool macho_open(Macho *out, Zstr path, Allocator *alloc);
127127
_Generic( \
128128
(path), \
129129
Str *: macho_open((out), (Zstr)StrBegin((Str *)(path)), MisraScope), \
130-
Zstr: macho_open((out), (Zstr)(path), MisraScope) \
130+
Zstr: macho_open((out), (Zstr)(path), MisraScope), \
131+
char *: macho_open((out), (Zstr)(path), MisraScope) \
131132
)
132133
#define MachoOpen_3(out, path, alloc) \
133134
_Generic( \
134135
(path), \
135136
Str *: macho_open((out), (Zstr)StrBegin((Str *)(path)), ALLOCATOR_OF(alloc)), \
136-
Zstr: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \
137+
Zstr: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \
138+
char *: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \
137139
)
138140

139141
///

0 commit comments

Comments
 (0)