From 82ce758e433700373e94de3654c7f1ba65302680 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Thu, 23 Apr 2026 11:09:34 -0400 Subject: [PATCH 1/2] libnvme: rename !accessors: member annotation to !access: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the member-level annotation prefix from !accessors: to !access: to decouple it from the !generate-accessors struct-level annotation. Until now, the !accessors: member annotation was consumed only by the accessor generator, and its name reflected that — it read as a natural companion to the struct-level !generate-accessors annotation. That one-to-one relationship is about to change. An upcoming !generate-python struct-level annotation will also need to read the same member annotations, both to drive the generated SWIG layer and to validate consistency between the C and Python sides. The annotation is no longer the private concern of the accessor generator. Keeping the name !accessors: would make that relationship confusing: - The visual similarity to !generate-accessors suggests the member annotation is owned by accessor generation, when in fact multiple generators will consume it. - Readers (and future contributors adding more generators) would reasonably — but wrongly — assume the annotation is accessor specific and only relevant when !generate-accessors is set. Renaming to !access: breaks that false association. The new name also reads more naturally as English: char *name; // !access:readonly char *addr; // !access:none vs. char *name; // !accessors:readonly char *addr; // !accessors:none This patch is a pure rename with no behavioural change: - generate-accessors.py: update has_annotation() lookups and the docstring examples - private.h: rename all 47 member annotations - generate-accessors.md: update every example, table row and note; re-pad the affected table rows to preserve column alignment Generator output (accessors.h / accessors.c / accessors.ld) is byte identical before and after this commit. Signed-off-by: Martin Belanger Assisted-by: Claude Opus 4.7 --- libnvme/src/nvme/private.h | 94 +++++++++---------- libnvme/tools/generator/generate-accessors.md | 46 ++++----- libnvme/tools/generator/generate-accessors.py | 16 ++-- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/libnvme/src/nvme/private.h b/libnvme/src/nvme/private.h index 36c51f4ef1..0df9eed2e9 100644 --- a/libnvme/src/nvme/private.h +++ b/libnvme/src/nvme/private.h @@ -202,20 +202,20 @@ struct libnvme_path { // !generate-accessors struct libnvme_stat stat[2]; /* gendisk I/O stat */ unsigned int curr_idx; /* current index into the stat[] */ - bool diffstat; // !accessors:none + bool diffstat; // !access:none struct libnvme_ctrl *c; struct libnvme_ns *n; char *name; char *sysfs_dir; - char *ana_state; // !accessors:none - char *numa_nodes; // !accessors:none + char *ana_state; // !access:none + char *numa_nodes; // !access:none int grpid; - int queue_depth; // !accessors:none - long multipath_failover_count; // !accessors:none - long command_retry_count; // !accessors:none - long command_error_count; // !accessors:none + int queue_depth; // !access:none + long multipath_failover_count; // !access:none + long command_retry_count; // !access:none + long command_error_count; // !access:none }; struct libnvme_ns_head { @@ -236,12 +236,12 @@ struct libnvme_ns { // !generate-accessors struct libnvme_stat stat[2]; /* gendisk I/O stat */ unsigned int curr_idx; /* current index into the stat[] */ - bool diffstat; // !accessors:none + bool diffstat; // !access:none struct libnvme_transport_handle *hdl; __u32 nsid; char *name; - char *generic_name; // !accessors:none + char *generic_name; // !access:none char *sysfs_dir; int lba_shift; @@ -255,10 +255,10 @@ struct libnvme_ns { // !generate-accessors unsigned char uuid[NVME_UUID_LEN]; enum nvme_csi csi; - long command_retry_count; // !accessors:none - long command_error_count; // !accessors:none - long requeue_no_usable_path_count; // !accessors:none - long fail_no_available_path_count; // !accessors:none + long command_retry_count; // !access:none + long command_error_count; // !access:none + long requeue_no_usable_path_count; // !access:none + long fail_no_available_path_count; // !access:none }; struct libnvme_ctrl { // !generate-accessors @@ -269,38 +269,38 @@ struct libnvme_ctrl { // !generate-accessors struct libnvme_global_ctx *ctx; struct libnvme_transport_handle *hdl; - char *name; // !accessors:readonly - char *sysfs_dir; // !accessors:readonly - char *address; // !accessors:none - char *firmware; // !accessors:readonly - char *model; // !accessors:readonly - char *state; // !accessors:none - char *numa_node; // !accessors:readonly - char *queue_count; // !accessors:readonly - char *serial; // !accessors:readonly - char *sqsize; // !accessors:readonly - char *transport; // !accessors:readonly - char *subsysnqn; // !accessors:readonly - char *traddr; // !accessors:readonly - char *trsvcid; // !accessors:readonly + char *name; // !access:readonly + char *sysfs_dir; // !access:readonly + char *address; // !access:none + char *firmware; // !access:readonly + char *model; // !access:readonly + char *state; // !access:none + char *numa_node; // !access:readonly + char *queue_count; // !access:readonly + char *serial; // !access:readonly + char *sqsize; // !access:readonly + char *transport; // !access:readonly + char *subsysnqn; // !access:readonly + char *traddr; // !access:readonly + char *trsvcid; // !access:readonly char *dhchap_host_key; char *dhchap_ctrl_key; char *keyring; char *tls_key_identity; char *tls_key; - char *cntrltype; // !accessors:readonly - char *cntlid; // !accessors:readonly - char *dctype; // !accessors:readonly - char *phy_slot; // !accessors:readonly - char *host_traddr; // !accessors:readonly - char *host_iface; // !accessors:readonly + char *cntrltype; // !access:readonly + char *cntlid; // !access:readonly + char *dctype; // !access:readonly + char *phy_slot; // !access:readonly + char *host_traddr; // !access:readonly + char *host_iface; // !access:readonly bool discovery_ctrl; bool unique_discovery_ctrl; bool discovered; bool persistent; - long command_error_count; // !accessors:none - long reset_count; // !accessors:none - long reconnect_count; // !accessors:none + long command_error_count; // !access:none + long reset_count; // !access:none + long reconnect_count; // !access:none struct libnvme_fabrics_config cfg; }; @@ -310,15 +310,15 @@ struct libnvme_subsystem { // !generate-accessors struct list_head namespaces; struct libnvme_host *h; - char *name; // !accessors:readonly - char *sysfs_dir; // !accessors:readonly - char *subsysnqn; // !accessors:readonly - char *model; // !accessors:readonly - char *serial; // !accessors:readonly - char *firmware; // !accessors:readonly - char *subsystype; // !accessors:readonly + char *name; // !access:readonly + char *sysfs_dir; // !access:readonly + char *subsysnqn; // !access:readonly + char *model; // !access:readonly + char *serial; // !access:readonly + char *firmware; // !access:readonly + char *subsystype; // !access:readonly char *application; - char *iopolicy; // !accessors:none + char *iopolicy; // !access:none }; struct libnvme_host { // !generate-accessors @@ -326,11 +326,11 @@ struct libnvme_host { // !generate-accessors struct list_head subsystems; struct libnvme_global_ctx *ctx; - char *hostnqn; // !accessors:readonly - char *hostid; // !accessors:readonly + char *hostnqn; // !access:readonly + char *hostid; // !access:readonly char *dhchap_host_key; char *hostsymname; - bool pdc_enabled; // !accessors:none + bool pdc_enabled; // !access:none bool pdc_enabled_valid; /* set if pdc_enabled doesn't have an undefined * value */ }; diff --git a/libnvme/tools/generator/generate-accessors.md b/libnvme/tools/generator/generate-accessors.md index f78822b101..4175d450f7 100644 --- a/libnvme/tools/generator/generate-accessors.md +++ b/libnvme/tools/generator/generate-accessors.md @@ -59,8 +59,8 @@ Place the annotation on a member's declaration line to suppress accessor generat ```c struct nvme_ctrl { // !generate-accessors char *name; - char *state; // !accessors:none - char *subsysnqn; // !accessors:none + char *state; // !access:none + char *subsysnqn; // !access:none }; ``` @@ -71,8 +71,8 @@ Place the annotation on a member's declaration line to generate only a getter (n ```c struct nvme_ctrl { // !generate-accessors char *name; - char *firmware; // !accessors:readonly - char *model; // !accessors:readonly + char *firmware; // !access:readonly + char *model; // !access:readonly }; ``` @@ -85,7 +85,7 @@ Place the annotation on a member's declaration line to generate only a setter (n ```c struct nvme_ctrl { // !generate-accessors:readonly char *name; /* getter only (struct default) */ - char *token; // !accessors:writeonly /* setter only override */ + char *token; // !access:writeonly /* setter only override */ }; ``` @@ -96,8 +96,8 @@ Place the annotation on a member's declaration line to generate both a getter an ```c struct nvme_ctrl { // !generate-accessors:none char *name; /* no accessors (struct default) */ - char *model; // !accessors:readwrite /* both getter and setter */ - char *firmware; // !accessors:readonly /* getter only */ + char *model; // !access:readwrite /* both getter and setter */ + char *firmware; // !access:readonly /* getter only */ }; ``` @@ -140,7 +140,7 @@ struct nvme_ctrl { // !generate-lifecycle }; ``` -This annotation has no effect on accessor generation. Combine with `// !accessors:none` if both should be suppressed. +This annotation has no effect on accessor generation. Combine with `// !access:none` if both should be suppressed. ### Member defaults — `default:VALUE` @@ -168,10 +168,10 @@ The value is emitted verbatim, so any valid C expression — integer literals, m | `// !generate-accessors:readonly` | struct brace | Include struct, default: getter only | | `// !generate-accessors:writeonly` | struct brace | Include struct, default: setter only | | `// !generate-lifecycle` | struct brace | Generate constructor + destructor | -| `// !accessors:none` | member line | Skip this member entirely (accessors only) | -| `// !accessors:readonly` | member line | Generate getter only | -| `// !accessors:writeonly` | member line | Generate setter only | -| `// !accessors:readwrite` | member line | Generate getter and setter | +| `// !access:none` | member line | Skip this member entirely (accessors only) | +| `// !access:readonly` | member line | Generate getter only | +| `// !access:writeonly` | member line | Generate setter only | +| `// !access:readwrite` | member line | Generate getter and setter | | `// !lifecycle:none` | member line | Exclude member from destructor free logic | | `// !default:VALUE` | member line | Set field to VALUE in `init_defaults()` | | `const` qualifier on member | member type | Suppress setter; suppress free in destructor | @@ -187,8 +187,8 @@ struct person { // !generate-accessors char *name; int age; const char *id; /* const → getter only, no annotation needed */ - char *secret; // !accessors:none - char *role; // !accessors:readonly + char *secret; // !access:none + char *role; // !access:readonly }; struct car { // !generate-accessors @@ -319,7 +319,7 @@ const char *car_get_vin(const struct car *p); #endif /* _ACCESSORS_H_ */ ``` -> **Note:** The `secret` member is absent because of `// !accessors:none` — excluded members leave no trace in the output. The `role` member has only a getter because of `// !accessors:readonly`. The `id` and `vin` members have only getters because they are declared `const`. +> **Note:** The `secret` member is absent because of `// !access:none` — excluded members leave no trace in the output. The `role` member has only a getter because of `// !access:readonly`. The `id` and `vin` members have only getters because they are declared `const`. ### Generated `accessors.c` @@ -422,7 +422,7 @@ LIBNVME_ACCESSORS_3 { }; ``` -> **Note:** Only symbols for members that have accessors generated appear in the linker script. The `secret` member (excluded via `// !accessors:none`) and the write-only `token` example would have no getter entry. The version node name `LIBNVME_ACCESSORS_3` is hardcoded in the generator. +> **Note:** Only symbols for members that have accessors generated appear in the linker script. The `secret` member (excluded via `// !access:none`) and the write-only `token` example would have no getter entry. The version node name `LIBNVME_ACCESSORS_3` is hardcoded in the generator. ------ @@ -437,8 +437,8 @@ struct person { // !generate-accessors !generate-lifecycle char *name; int age; const char *id; /* const → getter only; NOT freed by destructor */ - char *secret; // !accessors:none - char *role; // !accessors:readonly + char *secret; // !access:none + char *role; // !access:readonly }; ``` @@ -491,7 +491,7 @@ __public void person_free(struct person *p) > **Notes:** > - `id` is `const char *` — the destructor never frees `const` members. -> - `secret` is `// !accessors:none` but is still freed — `lifecycle:none` is the annotation to suppress a free. +> - `secret` is `// !access:none` but is still freed — `lifecycle:none` is the annotation to suppress a free. > - `age` is `int` — only `char *` and `char **` members are freed. ### Additional entries in `accessors.ld` @@ -611,10 +611,10 @@ __public void conn_opts_free(struct conn_opts *p) 2. **String arrays** (`char **`) — setters deep-copy NULL-terminated arrays (each element and the container). 3. **Fixed char arrays** (`char foo[N]`) — setters use `snprintf`, always NUL-terminated. 4. **`const` members** — only a getter is generated, no setter (applies regardless of any annotation). `const char *` members are also skipped by the destructor. -5. **`// !accessors:readonly`** — same effect as `const`: getter only. -6. **`// !accessors:writeonly`** — setter only; getter is suppressed. -7. **`// !accessors:readwrite`** — both getter and setter; overrides a restrictive struct-level default. -8. **`// !accessors:none`** — member is completely ignored by the accessor generator. The destructor still frees it unless `// !lifecycle:none` is also present. +5. **`// !access:readonly`** — same effect as `const`: getter only. +6. **`// !access:writeonly`** — setter only; getter is suppressed. +7. **`// !access:readwrite`** — both getter and setter; overrides a restrictive struct-level default. +8. **`// !access:none`** — member is completely ignored by the accessor generator. The destructor still frees it unless `// !lifecycle:none` is also present. 9. **Struct-level mode** — the qualifier on `generate-accessors` sets the default for every member in the struct; per-member annotations override the struct default. 10. **`// !generate-lifecycle`** — generates `foo_new()` (constructor) and `foo_free()` (destructor). Can appear on the same line as `generate-accessors`. A struct needs only one of the two annotations. 11. **`// !lifecycle:none`** — excludes a member from the destructor's free logic. Use this when the struct does not own the pointed-to memory. diff --git a/libnvme/tools/generator/generate-accessors.py b/libnvme/tools/generator/generate-accessors.py index 8d5154c47e..9236333bdb 100755 --- a/libnvme/tools/generator/generate-accessors.py +++ b/libnvme/tools/generator/generate-accessors.py @@ -55,20 +55,20 @@ strdup) since they are assumed to point to externally owned storage. Member exclusion — annotate the member declaration line: - char *model; // !accessors:none + char *model; // !access:none Read-only members (getter only, setter suppressed): - Members declared with the 'const' qualifier, or - Annotate the member declaration line: - char *state; // !accessors:readonly + char *state; // !access:readonly Write-only members (setter only, getter suppressed): - Annotate the member declaration line: - char *state; // !accessors:writeonly + char *state; // !access:writeonly Both getter and setter (override a restrictive struct-level default): - Annotate the member declaration line: - char *state; // !accessors:readwrite + char *state; // !access:readwrite Example usage: ./generate-accessors.py private.h @@ -301,14 +301,14 @@ def parse_members(struct_name, raw_body, struct_mode, verbose): # ---------------------------------------------------------------- # Annotation checks on the raw line — BEFORE stripping comments. # ---------------------------------------------------------------- - if has_annotation(raw_line, 'accessors:none'): + if has_annotation(raw_line, 'access:none'): continue - if has_annotation(raw_line, 'accessors:readwrite'): + if has_annotation(raw_line, 'access:readwrite'): member_mode = 'both' - elif has_annotation(raw_line, 'accessors:readonly'): + elif has_annotation(raw_line, 'access:readonly'): member_mode = 'readonly' - elif has_annotation(raw_line, 'accessors:writeonly'): + elif has_annotation(raw_line, 'access:writeonly'): member_mode = 'writeonly' else: member_mode = struct_mode From 1a0ccef76abbc5390feae7161694dddea0a5beed Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Thu, 23 Apr 2026 19:36:03 -0400 Subject: [PATCH 2/2] libnvme: replace single-mode access annotation with a two-axis model Replace the single-mode !access: annotation with an orthogonal two-axis spec: !access:read=,write= where read and write are independent and each takes one of: generated (generator emits the accessor), custom (hand-written accessor in the public API, generator emits nothing), or none (no accessor exists). The same spec is accepted at the struct level via !generate-accessors:read=,write=, with per-axis inheritance from the struct-level default to member-level overrides. The new model lets downstream consumers (the Python-binding generator, the nvme.i consistency check) distinguish "no accessor" from "hand-written accessor", and cleanly expresses mixed cases such as "generated getter + hand-written setter" that the old one-dimensional annotation could not name. This is a clean break: the old :none / :readonly / :writeonly / :readwrite tokens are no longer recognised. See libnvme/tools/generator/generate-accessors.md for the full syntax, inheritance rules, and examples. All 47 !access: annotations in private.h are updated accordingly. The regenerated accessors.{h,c,ld} are byte-identical to the pre-change baseline. Signed-off-by: Martin Belanger Assisted-by: Claude Opus 4.7 --- libnvme/src/nvme/private.h | 129 ++++---- libnvme/tools/generator/generate-accessors.md | 159 +++++----- libnvme/tools/generator/generate-accessors.py | 286 ++++++++++++------ 3 files changed, 349 insertions(+), 225 deletions(-) diff --git a/libnvme/src/nvme/private.h b/libnvme/src/nvme/private.h index 0df9eed2e9..94000a52cf 100644 --- a/libnvme/src/nvme/private.h +++ b/libnvme/src/nvme/private.h @@ -196,26 +196,27 @@ struct libnvme_stat { double ts_ms; /* timestamp when the stat is updated */ }; -struct libnvme_path { // !generate-accessors +struct libnvme_path { // !generate-accessors:read=custom,write=none struct list_node entry; struct list_node nentry; struct libnvme_stat stat[2]; /* gendisk I/O stat */ - unsigned int curr_idx; /* current index into the stat[] */ - bool diffstat; // !access:none + // curr_idx: current index into the stat[] + unsigned int curr_idx; // !access:read=generated,write=generated + bool diffstat; // !access:read=none struct libnvme_ctrl *c; struct libnvme_ns *n; - char *name; - char *sysfs_dir; - char *ana_state; // !access:none - char *numa_nodes; // !access:none - int grpid; - int queue_depth; // !access:none - long multipath_failover_count; // !access:none - long command_retry_count; // !access:none - long command_error_count; // !access:none + char *name; // !access:read=generated,write=generated + char *sysfs_dir; // !access:read=generated,write=generated + char *ana_state; + char *numa_nodes; + int grpid; // !access:read=generated,write=generated + int queue_depth; + long multipath_failover_count; + long command_retry_count; + long command_error_count; }; struct libnvme_ns_head { @@ -236,12 +237,12 @@ struct libnvme_ns { // !generate-accessors struct libnvme_stat stat[2]; /* gendisk I/O stat */ unsigned int curr_idx; /* current index into the stat[] */ - bool diffstat; // !access:none + bool diffstat; // !access:read=none,write=none struct libnvme_transport_handle *hdl; __u32 nsid; char *name; - char *generic_name; // !access:none + char *generic_name; // !access:read=custom,write=none char *sysfs_dir; int lba_shift; @@ -255,13 +256,13 @@ struct libnvme_ns { // !generate-accessors unsigned char uuid[NVME_UUID_LEN]; enum nvme_csi csi; - long command_retry_count; // !access:none - long command_error_count; // !access:none - long requeue_no_usable_path_count; // !access:none - long fail_no_available_path_count; // !access:none + long command_retry_count; // !access:read=custom,write=none + long command_error_count; // !access:read=custom,write=none + long requeue_no_usable_path_count; // !access:read=custom,write=none + long fail_no_available_path_count; // !access:read=custom,write=none }; -struct libnvme_ctrl { // !generate-accessors +struct libnvme_ctrl { // !generate-accessors:read=generated,write=none struct list_node entry; struct list_head paths; struct list_head namespaces; @@ -269,56 +270,56 @@ struct libnvme_ctrl { // !generate-accessors struct libnvme_global_ctx *ctx; struct libnvme_transport_handle *hdl; - char *name; // !access:readonly - char *sysfs_dir; // !access:readonly - char *address; // !access:none - char *firmware; // !access:readonly - char *model; // !access:readonly - char *state; // !access:none - char *numa_node; // !access:readonly - char *queue_count; // !access:readonly - char *serial; // !access:readonly - char *sqsize; // !access:readonly - char *transport; // !access:readonly - char *subsysnqn; // !access:readonly - char *traddr; // !access:readonly - char *trsvcid; // !access:readonly - char *dhchap_host_key; - char *dhchap_ctrl_key; - char *keyring; - char *tls_key_identity; - char *tls_key; - char *cntrltype; // !access:readonly - char *cntlid; // !access:readonly - char *dctype; // !access:readonly - char *phy_slot; // !access:readonly - char *host_traddr; // !access:readonly - char *host_iface; // !access:readonly - bool discovery_ctrl; - bool unique_discovery_ctrl; - bool discovered; - bool persistent; - long command_error_count; // !access:none - long reset_count; // !access:none - long reconnect_count; // !access:none + char *name; + char *sysfs_dir; + char *address; // !access:read=custom + char *firmware; + char *model; + char *state; // !access:read=custom + char *numa_node; + char *queue_count; + char *serial; + char *sqsize; + char *transport; + char *subsysnqn; + char *traddr; + char *trsvcid; + char *dhchap_host_key; // !access:write=generated + char *dhchap_ctrl_key; // !access:write=generated + char *keyring; // !access:write=generated + char *tls_key_identity; // !access:write=generated + char *tls_key; // !access:write=generated + char *cntrltype; + char *cntlid; + char *dctype; + char *phy_slot; + char *host_traddr; + char *host_iface; + bool discovery_ctrl; // !access:write=generated + bool unique_discovery_ctrl; // !access:write=generated + bool discovered; // !access:write=generated + bool persistent; // !access:write=generated + long command_error_count; // !access:read=custom + long reset_count; // !access:read=custom + long reconnect_count; // !access:read=custom struct libnvme_fabrics_config cfg; }; -struct libnvme_subsystem { // !generate-accessors +struct libnvme_subsystem { // !generate-accessors:read=generated,write=none struct list_node entry; struct list_head ctrls; struct list_head namespaces; struct libnvme_host *h; - char *name; // !access:readonly - char *sysfs_dir; // !access:readonly - char *subsysnqn; // !access:readonly - char *model; // !access:readonly - char *serial; // !access:readonly - char *firmware; // !access:readonly - char *subsystype; // !access:readonly - char *application; - char *iopolicy; // !access:none + char *name; + char *sysfs_dir; + char *subsysnqn; + char *model; + char *serial; + char *firmware; + char *subsystype; + char *application; // !access:write=generated + char *iopolicy; // !access:read=custom }; struct libnvme_host { // !generate-accessors @@ -326,11 +327,11 @@ struct libnvme_host { // !generate-accessors struct list_head subsystems; struct libnvme_global_ctx *ctx; - char *hostnqn; // !access:readonly - char *hostid; // !access:readonly + char *hostnqn; // !access:read=generated,write=none + char *hostid; // !access:read=generated,write=none char *dhchap_host_key; char *hostsymname; - bool pdc_enabled; // !access:none + bool pdc_enabled; // !access:read=none,write=custom bool pdc_enabled_valid; /* set if pdc_enabled doesn't have an undefined * value */ }; diff --git a/libnvme/tools/generator/generate-accessors.md b/libnvme/tools/generator/generate-accessors.md index 4175d450f7..06677b9330 100644 --- a/libnvme/tools/generator/generate-accessors.md +++ b/libnvme/tools/generator/generate-accessors.md @@ -25,82 +25,94 @@ python3 generate-accessors.py [options] ## Annotations -Struct inclusion and member behavior are controlled by **annotations written as `//` line comments directly in the header file**. After `//`, each `!keyword` token (optionally followed by `:qualifier` or `:VALUE`) is a command. Multiple annotations may share one comment, separated by spaces. The canonical form is `// !token` (one space between `//` and `!`); `//!token` and `//\t!token` are also accepted. +Struct inclusion and member behavior are controlled by **annotations written as `//` line comments directly in the header file**. After `//`, each `!keyword` token (optionally followed by `:spec` or `:VALUE`) is a command. Multiple annotations may share one comment, separated by spaces. The canonical form is `// !token` (one space between `//` and `!`); `//!token` and `//\t!token` are also accepted. -### Struct inclusion — `generate-accessors` +### Access model — two independent axes -Place the annotation on the same line as the struct's opening brace to opt that struct in to code generation. An optional mode qualifier sets the **default behaviour for all members** of that struct: +Accessor generation is controlled by **two independent axes**: -| Annotation | Default for all members | -| ---------------------------------------- | --------------------------------- | -| `// !generate-accessors` | getter **and** setter (default) | -| `// !generate-accessors:none` | no accessors | -| `// !generate-accessors:readonly` | getter only | -| `// !generate-accessors:writeonly` | setter only | +- **`read`** — whether a getter exists for the field, and if so, how it is provided +- **`write`** — whether a setter exists for the field, and if so, how it is provided -```c -struct nvme_ctrl { // !generate-accessors /* both getter and setter */ - ... -}; +Each axis takes one of three modes: -struct nvme_ctrl { // !generate-accessors:readonly /* getter only by default */ - ... -}; -``` - -Only structs carrying this annotation will have accessors generated. All other structs in the header are ignored. +| Mode | Meaning | +| ----------- | ------------------------------------------------------------ | +| `generated` | This generator emits the accessor | +| `custom` | An accessor is expected to exist and is provided as a hand-written function; the generator emits nothing | +| `none` | No accessor exists for this axis; the generator emits nothing | -Individual members can always override the struct-level default using a per-member annotation (see below). +Only `generated` produces output in this generator. The `custom` and `none` modes are **semantic declarations**: they advertise intent to downstream consumers — such as the Python-binding generator and the `nvme.i` consistency check — which need to distinguish "no accessor at all" from "accessor provided by hand". -### Member exclusion — `accessors:none` +### Struct inclusion — `generate-accessors` -Place the annotation on a member's declaration line to suppress accessor generation for that member entirely (no setter, no getter): +Place the annotation on the same line as the struct's opening brace to opt that struct in to code generation. An optional spec sets the **default mode for each axis** of every member of the struct: ```c struct nvme_ctrl { // !generate-accessors - char *name; - char *state; // !access:none - char *subsysnqn; // !access:none + /* shorthand for read=generated, write=generated */ }; -``` -### Read-only members — `accessors:readonly` +struct nvme_ctrl { // !generate-accessors:read=generated,write=generated + /* fully explicit form of the same default */ +}; -Place the annotation on a member's declaration line to generate only a getter (no setter). This has the same effect as declaring the member `const`, but without changing the type in the struct. Also useful to override a `generate-accessors:writeonly` struct default for individual members: +struct nvme_ctrl { // !generate-accessors:read=none,write=none + /* struct is included, but every member is opaque by default */ +}; -```c -struct nvme_ctrl { // !generate-accessors - char *name; - char *firmware; // !access:readonly - char *model; // !access:readonly +struct nvme_ctrl { // !generate-accessors:read=generated + /* partial spec: write inherits the built-in default (generated) */ }; ``` -Members declared with the `const` qualifier are also automatically read-only. +Only structs carrying this annotation are processed. All other structs in the header are ignored. -### Write-only members — `accessors:writeonly` +**Built-in defaults.** Any axis not named at the struct level falls back to `generated`. The bare `// !generate-accessors` form is therefore shorthand for `// !generate-accessors:read=generated,write=generated`. + +Individual members can always override the struct-level default using a per-member annotation (see below). -Place the annotation on a member's declaration line to generate only a setter (no getter). Useful to override a `generate-accessors:readonly` struct default for individual members: +### Member-level override — `access` + +Place the annotation on a member's declaration line to override the struct-level default for this field. The spec takes the same shape as the struct-level annotation: ```c -struct nvme_ctrl { // !generate-accessors:readonly - char *name; /* getter only (struct default) */ - char *token; // !access:writeonly /* setter only override */ +struct nvme_ctrl { // !generate-accessors + char *name; + char *state; // !access:read=custom,write=none + char *token; // !access:read=none,write=custom + char *secret; // !access:read=none,write=none }; ``` -### Read-write members — `accessors:readwrite` - -Place the annotation on a member's declaration line to generate both a getter and a setter, overriding a restrictive struct-level default (`none`, `readonly`, or `writeonly`): +**Partial specs** are allowed — any axis not named in the member-level spec is **inherited from the struct-level default**, which is in turn drawn from the struct-level annotation (or from the built-in default when the struct has none). The order of `read` and `write` in the annotation is not significant: ```c -struct nvme_ctrl { // !generate-accessors:none - char *name; /* no accessors (struct default) */ - char *model; // !access:readwrite /* both getter and setter */ - char *firmware; // !access:readonly /* getter only */ +struct nvme_ctrl { // !generate-accessors /* defaults: read=generated, write=generated */ + char *model; // !access:read=custom + /* effective: read=custom, write=generated (inherited) */ + char *pw; // !access:write=custom + /* effective: read=generated (inherited), write=custom */ }; ``` +**Common patterns:** + +| Member spec | Effective (inside `// !generate-accessors`) | Meaning | +| ------------------------------------- | --------------------------------------------- | ---------------------------------------- | +| *(no annotation)* | `read=generated, write=generated` | Both accessors auto-generated | +| `// !access:read=generated,write=none`| `read=generated, write=none` | Read-only (auto-generated getter only) | +| `// !access:read=none,write=generated`| `read=none, write=generated` | Write-only (auto-generated setter only) | +| `// !access:read=none,write=none` | `read=none, write=none` | Purely internal; no accessor of any kind | +| `// !access:read=custom,write=none` | `read=custom, write=none` | Hand-written getter only | +| `// !access:read=none,write=custom` | `read=none, write=custom` | Hand-written setter only | +| `// !access:read=custom,write=custom` | `read=custom, write=custom` | Hand-written getter and setter | +| `// !access:read=generated,write=custom` | `read=generated, write=custom` | Mixed: generated getter, hand-written setter | + +### The `const` qualifier + +A `const`-qualified member forces `write=none` regardless of what the annotation (or inherited default) says — the generator cannot emit a setter for a member that the C type system forbids from being assigned. `const char *` members are also **never** freed by the destructor; they are assumed to point to externally owned storage. + ### Struct lifecycle — `generate-lifecycle` Place the annotation on the same line as the struct's opening brace to generate a constructor and a destructor for that struct: @@ -140,7 +152,7 @@ struct nvme_ctrl { // !generate-lifecycle }; ``` -This annotation has no effect on accessor generation. Combine with `// !access:none` if both should be suppressed. +This annotation has no effect on accessor generation. Combine with `// !access:read=none,write=none` if both should be suppressed. ### Member defaults — `default:VALUE` @@ -161,20 +173,19 @@ The value is emitted verbatim, so any valid C expression — integer literals, m ### Annotation summary -| Annotation | Where | Effect | -| ---------------------------------------- | ------------ | --------------------------------------------------------- | -| `// !generate-accessors` | struct brace | Include struct, default: getter + setter | -| `// !generate-accessors:none` | struct brace | Include struct, default: no accessors | -| `// !generate-accessors:readonly` | struct brace | Include struct, default: getter only | -| `// !generate-accessors:writeonly` | struct brace | Include struct, default: setter only | -| `// !generate-lifecycle` | struct brace | Generate constructor + destructor | -| `// !access:none` | member line | Skip this member entirely (accessors only) | -| `// !access:readonly` | member line | Generate getter only | -| `// !access:writeonly` | member line | Generate setter only | -| `// !access:readwrite` | member line | Generate getter and setter | -| `// !lifecycle:none` | member line | Exclude member from destructor free logic | -| `// !default:VALUE` | member line | Set field to VALUE in `init_defaults()` | -| `const` qualifier on member | member type | Suppress setter; suppress free in destructor | +| Annotation | Where | Effect | +| ------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------- | +| `// !generate-accessors` | struct brace | Include struct; defaults: `read=generated, write=generated` | +| `// !generate-accessors:read=M,write=M` | struct brace | Include struct; set struct-level default for each axis | +| `// !generate-accessors:read=M` | struct brace | Partial; other axis inherits the built-in default (`generated`) | +| `// !generate-lifecycle` | struct brace | Generate constructor + destructor | +| `// !access:read=M,write=M` | member line | Override struct-level defaults for this member | +| `// !access:read=M` | member line | Partial override; other axis is inherited from the struct-level default | +| `// !lifecycle:none` | member line | Exclude member from destructor free logic | +| `// !default:VALUE` | member line | Set field to VALUE in `init_defaults()` | +| `const` qualifier on member | member type | Force `write=none`; suppress free in destructor | + +In the table above, `M` is one of `generated`, `custom`, or `none`. ------ @@ -187,8 +198,8 @@ struct person { // !generate-accessors char *name; int age; const char *id; /* const → getter only, no annotation needed */ - char *secret; // !access:none - char *role; // !access:readonly + char *secret; // !access:read=none,write=none + char *role; // !access:write=none /* read inherits: generated */ }; struct car { // !generate-accessors @@ -319,7 +330,7 @@ const char *car_get_vin(const struct car *p); #endif /* _ACCESSORS_H_ */ ``` -> **Note:** The `secret` member is absent because of `// !access:none` — excluded members leave no trace in the output. The `role` member has only a getter because of `// !access:readonly`. The `id` and `vin` members have only getters because they are declared `const`. +> **Note:** The `secret` member is absent because of `// !access:read=none,write=none` — with neither axis set to `generated`, the member leaves no trace in the output. The `role` member has only a getter because its `write=none` annotation (combined with the inherited `read=generated`) yields a read-only accessor. The `id` and `vin` members have only getters because they are declared `const`. ### Generated `accessors.c` @@ -422,7 +433,7 @@ LIBNVME_ACCESSORS_3 { }; ``` -> **Note:** Only symbols for members that have accessors generated appear in the linker script. The `secret` member (excluded via `// !access:none`) and the write-only `token` example would have no getter entry. The version node name `LIBNVME_ACCESSORS_3` is hardcoded in the generator. +> **Note:** Only symbols for members that have accessors generated appear in the linker script. The `secret` member (excluded via `// !access:read=none,write=none`) and a write-only member (e.g. `// !access:read=none`) would have no getter entry. The version node name `LIBNVME_ACCESSORS_3` is hardcoded in the generator. ------ @@ -437,8 +448,8 @@ struct person { // !generate-accessors !generate-lifecycle char *name; int age; const char *id; /* const → getter only; NOT freed by destructor */ - char *secret; // !access:none - char *role; // !access:readonly + char *secret; // !access:read=none,write=none + char *role; // !access:write=none /* read inherits: generated */ }; ``` @@ -491,7 +502,7 @@ __public void person_free(struct person *p) > **Notes:** > - `id` is `const char *` — the destructor never frees `const` members. -> - `secret` is `// !access:none` but is still freed — `lifecycle:none` is the annotation to suppress a free. +> - `secret` is `// !access:read=none,write=none` but is still freed — `lifecycle:none` is the annotation to suppress a free. > - `age` is `int` — only `char *` and `char **` members are freed. ### Additional entries in `accessors.ld` @@ -610,12 +621,12 @@ __public void conn_opts_free(struct conn_opts *p) 1. **Dynamic strings** (`char *`) — setters store a `strdup()` copy; passing `NULL` clears the field. 2. **String arrays** (`char **`) — setters deep-copy NULL-terminated arrays (each element and the container). 3. **Fixed char arrays** (`char foo[N]`) — setters use `snprintf`, always NUL-terminated. -4. **`const` members** — only a getter is generated, no setter (applies regardless of any annotation). `const char *` members are also skipped by the destructor. -5. **`// !access:readonly`** — same effect as `const`: getter only. -6. **`// !access:writeonly`** — setter only; getter is suppressed. -7. **`// !access:readwrite`** — both getter and setter; overrides a restrictive struct-level default. -8. **`// !access:none`** — member is completely ignored by the accessor generator. The destructor still frees it unless `// !lifecycle:none` is also present. -9. **Struct-level mode** — the qualifier on `generate-accessors` sets the default for every member in the struct; per-member annotations override the struct default. +4. **`const` members** — force `write=none` regardless of the annotation: the generator cannot emit a setter for a member the C type system forbids from being assigned. `const char *` members are also skipped by the destructor (they are assumed to point to externally owned storage). +5. **Access model — two independent axes** — every member has a `read` axis (getter) and a `write` axis (setter), each taking one of `generated`, `custom`, or `none`. Only `generated` produces output in this generator; `custom` and `none` are semantic declarations for downstream consumers (Python-binding generator, `nvme.i` consistency check). +6. **`// !access:read=M,write=M`** — member-level override for either or both axes. Partial specs are allowed: any axis not named is inherited from the struct-level default, which is in turn drawn from the struct's `// !generate-accessors[:spec]` annotation (or from the built-in default `generated` when the struct has none). +7. **Struct-level default** — `// !generate-accessors:read=M,write=M` sets the default mode for each axis for every member of the struct. Partial struct-level specs inherit the built-in default (`generated`) for any axis they do not name. Bare `// !generate-accessors` is shorthand for `read=generated, write=generated`. +8. **Members with no `generated` axis are skipped entirely** — if the effective modes are `read=custom|none` AND `write=custom|none` (no axis set to `generated`), the generator emits nothing for that field. The annotation is still meaningful for downstream consumers, which read the private header directly. +9. **Struct-level overrides cascade, member-level overrides win** — the cascade is: built-in default (`generated`) → struct-level spec → member-level spec. Each level only overrides the axes it names; un-named axes fall through to the level above. 10. **`// !generate-lifecycle`** — generates `foo_new()` (constructor) and `foo_free()` (destructor). Can appear on the same line as `generate-accessors`. A struct needs only one of the two annotations. 11. **`// !lifecycle:none`** — excludes a member from the destructor's free logic. Use this when the struct does not own the pointed-to memory. 12. **Destructor NULL safety** — `free(NULL)` is a no-op per the C standard, so destructors with no string members to dereference emit only `free(p)` with no NULL guard. Destructors that do dereference `p->field` guard with `if (!p) return;` first. In both cases passing NULL to the destructor is safe. diff --git a/libnvme/tools/generator/generate-accessors.py b/libnvme/tools/generator/generate-accessors.py index 9236333bdb..59576affd5 100755 --- a/libnvme/tools/generator/generate-accessors.py +++ b/libnvme/tools/generator/generate-accessors.py @@ -17,7 +17,7 @@ - Does not support struct within struct. Annotations use // line-comment style. After '//', each '!keyword' token -(optionally followed by ':qualifier' or ':VALUE') is a command. Multiple +(optionally followed by ':spec' or ':VALUE') is a command. Multiple annotations can appear in one comment: struct nvme_ctrl { // !generate-accessors !generate-lifecycle @@ -25,12 +25,57 @@ and //\t!token are all equivalent. The canonical form used in this project's headers is "// !token" (one space). +ACCESS MODEL — TWO INDEPENDENT AXES +----------------------------------- +Each struct member has two independent axes: + read — whether a getter exists, and how + write — whether a setter exists, and how + +Each axis takes one of three modes: + generated — the generator emits the accessor + custom — an accessor exists but is provided elsewhere as a hand-written + function in the public API; the generator emits nothing + none — no accessor exists for this axis; the generator + emits nothing + +Only the 'generated' mode produces output in this generator. 'custom' +and 'none' are semantic declarations for downstream consumers (the +Python-binding generator, the nvme.i consistency check) that need to +know the difference between "no accessor at all" and "accessor provided +by hand". + Struct inclusion — annotate the opening brace line of the struct. -The optional mode qualifier sets the default for all members of the struct: - struct nvme_ctrl { // !generate-accessors — default: both getter and setter - struct nvme_ctrl { // !generate-accessors:none — default: no accessors - struct nvme_ctrl { // !generate-accessors:readonly — default: getter only - struct nvme_ctrl { // !generate-accessors:writeonly — default: setter only +The optional spec sets the default mode for each axis of every member +of the struct: + struct nvme_ctrl { // !generate-accessors + — shorthand for read=generated, write=generated + struct nvme_ctrl { // !generate-accessors:read=generated,write=generated + — explicit form of the same default + struct nvme_ctrl { // !generate-accessors:read=none,write=none + — include struct but emit nothing by default + struct nvme_ctrl { // !generate-accessors:read=generated + — read=generated, write inherits the built-in default (generated) + +Only structs carrying this annotation are processed. Members of other +structs are ignored. + +Member-level override — annotate the member declaration line. Any axis +not named in the spec is inherited from the struct-level default: + char *state; // !access:read=custom,write=none + — custom getter, no setter + char *token; // !access:read=none,write=custom + — no getter, custom setter + char *secret; // !access:read=none,write=none + — no accessor of any kind + char *name; // !access:read=custom + — custom getter; write axis inherited from struct default + char *pw; // !access:write=custom + — custom setter; read axis inherited from struct default + +The 'const' qualifier on a member forces write=none regardless of the +annotation (you cannot generate a setter for a const member). 'const +char *' members are also never freed by the destructor — they are +assumed to point to externally owned storage. Lifecycle (constructor + destructor) — annotate the opening brace line: struct nvme_ctrl { // !generate-lifecycle @@ -54,22 +99,6 @@ strdup()s the new one. const char* members are assigned directly (no strdup) since they are assumed to point to externally owned storage. -Member exclusion — annotate the member declaration line: - char *model; // !access:none - -Read-only members (getter only, setter suppressed): - - Members declared with the 'const' qualifier, or - - Annotate the member declaration line: - char *state; // !access:readonly - -Write-only members (setter only, getter suppressed): - - Annotate the member declaration line: - char *state; // !access:writeonly - -Both getter and setter (override a restrictive struct-level default): - - Annotate the member declaration line: - char *state; // !access:readwrite - Example usage: ./generate-accessors.py private.h ./generate-accessors.py --prefix nvme_ private.h @@ -170,7 +199,8 @@ def has_annotation(text, annotation): Accepts '// !annotation', '//!annotation', '//\\t!annotation', etc. The match is token-delimited: ``!generate-accessors`` will not match - inside ``!generate-accessors:none``. + inside ``!generate-accessors:read=none,write=none`` (the ':' is not + whitespace, '!', or end-of-string). """ comment = _comment_text(text) if comment is None: @@ -261,17 +291,27 @@ def kdoc_summary(fn, *descriptions): # --------------------------------------------------------------------------- class Member: - """Represents one member of a parsed C struct.""" + """Represents one member of a parsed C struct. - __slots__ = ('name', 'type', 'gen_getter', 'gen_setter', + read_mode and write_mode each take one of: + 'generated' — this generator emits the accessor + 'custom' — an accessor exists elsewhere (hand-written or bridge) + 'none' — no accessor exists for this axis + + Only 'generated' produces output in this generator. The other two + modes exist for downstream consumers (Python binding generator, + consistency checks) that care about the semantic distinction. + """ + + __slots__ = ('name', 'type', 'read_mode', 'write_mode', 'is_char_array', 'is_char_ptr_array', 'array_size') - def __init__(self, name, type_str, gen_getter, gen_setter, + def __init__(self, name, type_str, read_mode, write_mode, is_char_array, is_char_ptr_array, array_size): self.name = name self.type = type_str # e.g. "const char *", "int", "__u32" - self.gen_getter = gen_getter # True → emit getter - self.gen_setter = gen_setter # True → emit setter + self.read_mode = read_mode # 'generated' | 'custom' | 'none' + self.write_mode = write_mode # 'generated' | 'custom' | 'none' self.is_char_array = is_char_array self.is_char_ptr_array = is_char_ptr_array self.array_size = array_size # only for fixed-size char arrays (char[N]) @@ -281,40 +321,44 @@ def __init__(self, name, type_str, gen_getter, gen_setter, # Parsing # --------------------------------------------------------------------------- -def parse_members(struct_name, raw_body, struct_mode, verbose): +def parse_members(struct_name, raw_body, struct_defaults, verbose): """Parse *raw_body* and return a list of Member objects. - *struct_mode* is the default access mode for all members of this struct, - derived from the generate-accessors annotation qualifier: - 'both' — generate getter and setter (default when no qualifier) - 'readonly' — generate getter only - 'writeonly' — generate setter only - 'none' — generate nothing unless a per-member annotation overrides + *struct_defaults* is a (read_mode, write_mode) tuple taken from the + struct-level ``!generate-accessors`` annotation. Each member inherits + these defaults and may override one or both axes with + ``// !access:read=...,write=...``. + + Modes: 'generated' | 'custom' | 'none'. + + Members whose effective read_mode and write_mode are both non-generated + are skipped — they produce no output, so there is no reason to carry + them through the emitter stage. Annotations are detected on the **raw** (un-stripped) line so that comment masking cannot hide them. Comments are stripped only afterwards, for regex matching. """ + struct_read, struct_write = struct_defaults members = [] for raw_line in raw_body.splitlines(): # ---------------------------------------------------------------- # Annotation checks on the raw line — BEFORE stripping comments. # ---------------------------------------------------------------- - if has_annotation(raw_line, 'access:none'): - continue - - if has_annotation(raw_line, 'access:readwrite'): - member_mode = 'both' - elif has_annotation(raw_line, 'access:readonly'): - member_mode = 'readonly' - elif has_annotation(raw_line, 'access:writeonly'): - member_mode = 'writeonly' + override = parse_access_override(raw_line) + if override is None: + read_mode = struct_read + write_mode = struct_write else: - member_mode = struct_mode + read_mode = override.get('read', struct_read) + write_mode = override.get('write', struct_write) - gen_getter = member_mode in ('both', 'readonly') - gen_setter = member_mode in ('both', 'writeonly') + # Skip members that produce no generator output. They may still + # carry a valid 'custom'/'none' declaration for downstream tools + # but this generator has nothing to emit for them. + if read_mode != 'generated' and write_mode != 'generated': + continue # ---------------------------------------------------------------- # Strip comments for member-declaration parsing. @@ -333,8 +377,10 @@ def parse_members(struct_name, raw_body, struct_mode, verbose): members.append(Member( name=m.group(2), type_str='const char *', - gen_getter=gen_getter, - gen_setter=gen_setter and not is_const_qual, + read_mode=read_mode, + # const forces write=none — you cannot generate a setter + # for a const member. + write_mode='none' if is_const_qual else write_mode, is_char_array=True, is_char_ptr_array=False, array_size=m.group(3), @@ -365,8 +411,8 @@ def parse_members(struct_name, raw_body, struct_mode, verbose): members.append(Member( name=name, type_str=type_str, - gen_getter=gen_getter, - gen_setter=gen_setter and not is_const_qual, + read_mode=read_mode, + write_mode='none' if is_const_qual else write_mode, is_char_array=False, is_char_ptr_array=(ptr_depth == 2), array_size=None, @@ -375,42 +421,103 @@ def parse_members(struct_name, raw_body, struct_mode, verbose): return members -_VALID_MODES = frozenset(('both', 'none', 'readonly', 'writeonly')) +_VALID_MODES = frozenset(('generated', 'custom', 'none')) + + +def parse_access_spec(spec, origin): + """Parse a spec body like ``read=generated,write=custom``. + + Returns a dict mapping axis names ('read', 'write') to mode strings. + Partial specs are allowed — any axis not named in the spec is absent + from the returned dict. Unknown axes or modes trigger a warning and + are dropped from the result. + + *origin* is a human-readable description of where the spec came from, + used only for warning messages (e.g. "!generate-accessors" or + "!access"). + """ + result = {} + for part in spec.split(','): + part = part.strip() + if not part: + continue + if '=' not in part: + print(f"warning: {origin} spec token '{part}' has no '='; " + f"expected 'read=MODE' or 'write=MODE'.", + file=sys.stderr) + continue + key, value = part.split('=', 1) + key = key.strip() + value = value.strip() + if key not in ('read', 'write'): + print(f"warning: {origin} spec axis '{key}' is unknown; " + f"expected 'read' or 'write'.", + file=sys.stderr) + continue + if value not in _VALID_MODES: + print(f"warning: {origin} spec mode '{value}' for axis " + f"'{key}' is unknown; expected one of: " + f"{', '.join(sorted(_VALID_MODES))}.", + file=sys.stderr) + continue + result[key] = value + return result def parse_struct_annotation(raw_body): - """Return the default mode for a struct from its generate-accessors annotation. + """Return the (read_mode, write_mode) defaults for a struct. - Recognises the ``//`` comment style with optional whitespace after ``//``: - // !generate-accessors → 'both' - // !generate-accessors:none → 'none' - // !generate-accessors:readonly → 'readonly' - // !generate-accessors:writeonly → 'writeonly' + Recognises:: - '//!', '// !', and '//\\t!' are all accepted. + // !generate-accessors + → ('generated', 'generated') [shorthand] + // !generate-accessors:read=X,write=Y + → (X, Y) [explicit, both axes] + // !generate-accessors:read=X + → (X, 'generated') [partial — write inherits built-in] + // !generate-accessors:write=Y + → ('generated', Y) [partial — read inherits built-in] Returns None when the annotation is absent. - Prints a warning and falls back to 'both' for unrecognised qualifiers. + + The built-in default for any axis not named at the struct level is + 'generated'. """ - first_token = raw_body.lstrip() + # Bare form first: "!generate-accessors" with no ':spec'. + bare_re = re.compile(r'!generate-accessors(?=[\s!]|$)') + # Specced form: "!generate-accessors:spec". + spec_re = re.compile(r'!generate-accessors:(\S+)(?=[\s!]|$)') - m = re.match(r'//\s*!generate-accessors(?::([a-z]+))?', first_token) + m = spec_re.search(raw_body) if m: - qualifier = m.group(1) or 'both' - if qualifier not in _VALID_MODES: - print( - f"warning: unknown generate-accessors qualifier " - f"'{qualifier}'; valid values are: " - f"{', '.join(sorted(_VALID_MODES))}. " - f"Defaulting to 'both'.", - file=sys.stderr, - ) - qualifier = 'both' - return qualifier + parsed = parse_access_spec(m.group(1), '!generate-accessors') + return (parsed.get('read', 'generated'), + parsed.get('write', 'generated')) + + if bare_re.search(raw_body): + return ('generated', 'generated') return None +def parse_access_override(raw_line): + """Parse a member-level ``!access:`` annotation. + + Returns a dict ``{'read': mode}`` / ``{'write': mode}`` / both, or + ``None`` when no ``!access`` annotation is present on the line. A + partial spec yields a dict with only the named axes — missing axes + are the caller's responsibility to fill in (typically from the + struct-level default). + """ + comment = _comment_text(raw_line) + if comment is None: + return None + m = re.search(r'!access:(\S+)(?=[\s!]|$)', comment) + if not m: + return None + return parse_access_spec(m.group(1), '!access') + + def parse_lifecycle_annotation(raw_body): """Return True when *raw_body* carries a ``!generate-lifecycle`` annotation. @@ -443,7 +550,8 @@ def parse_members_for_lifecycle(raw_body): """Return a list of LifecycleMember for every char* or char** member. Unlike parse_members(), this function: - - ignores ``accessors:none`` (the destructor must free all heap strings) + - ignores the ``!access`` annotation entirely (the destructor must + free all heap strings regardless of accessor mode) - respects ``lifecycle:none`` to let callers opt a member out - collects only char* and char** members (the only types that need explicit freeing) @@ -585,16 +693,16 @@ def parse_file(text, verbose): struct_name = match.group(1) raw_body = match.group(2) - struct_mode = parse_struct_annotation(raw_body) - want_lifecycle = parse_lifecycle_annotation(raw_body) + struct_defaults = parse_struct_annotation(raw_body) + want_lifecycle = parse_lifecycle_annotation(raw_body) - if struct_mode is None and not want_lifecycle: + if struct_defaults is None and not want_lifecycle: continue members = [] - if struct_mode is not None: + if struct_defaults is not None: members = parse_members( - struct_name, raw_body, struct_mode, verbose) + struct_name, raw_body, struct_defaults, verbose) lc_members = None if want_lifecycle: @@ -603,8 +711,12 @@ def parse_file(text, verbose): default_members = parse_members_for_defaults(raw_body) if verbose and (members or lc_members is not None or default_members): - acc = f"{len(members)} members [mode: {struct_mode}]" \ - if members else "no accessors" + if members: + sr, sw = struct_defaults + acc = (f"{len(members)} members " + f"[defaults: read={sr}, write={sw}]") + else: + acc = "no accessors" lc = (f"{len(lc_members)} lifecycle members" if lc_members is not None else "no lifecycle") df = (f"{len(default_members)} defaults" if default_members @@ -736,7 +848,7 @@ def generate_hdr(f, prefix, struct_name, members): is_dyn_str = (not member.is_char_array and not member.is_char_ptr_array and member.type == 'const char *') - if member.gen_setter: + if member.write_mode == 'generated': if member.is_char_ptr_array: emit_hdr_setter_str_array(f, prefix, struct_name, member.name) elif member.is_char_array or is_dyn_str: @@ -745,7 +857,7 @@ def generate_hdr(f, prefix, struct_name, members): else: emit_hdr_setter_val(f, prefix, struct_name, member.name, member.type) - if member.gen_getter: + if member.read_mode == 'generated': emit_hdr_getter(f, prefix, struct_name, member.name, member.type, is_dyn_str) @@ -886,7 +998,7 @@ def generate_src(f, prefix, struct_name, members): is_dyn_str = (not member.is_char_array and not member.is_char_ptr_array and member.type == 'const char *') - if member.gen_setter: + if member.write_mode == 'generated': if is_dyn_str: emit_src_setter_dynstr(f, prefix, struct_name, member.name) elif member.is_char_ptr_array: @@ -897,7 +1009,7 @@ def generate_src(f, prefix, struct_name, members): else: emit_src_setter_val(f, prefix, struct_name, member.name, member.type) - if member.gen_getter: + if member.read_mode == 'generated': cast = '(const char *const *)' if member.is_char_ptr_array else None emit_src_getter(f, prefix, struct_name, member.name, member.type, cast=cast) @@ -1092,9 +1204,9 @@ def generate_ld(f, prefix, struct_name, members, lc_members, default_members): if default_members: f.write(f'\t\t{_init_defaults_name(prefix, struct_name)};\n') for member in members: - if member.gen_getter: + if member.read_mode == 'generated': f.write(f'\t\t{_get_name(prefix, struct_name, member.name)};\n') - if member.gen_setter: + if member.write_mode == 'generated': f.write(f'\t\t{_set_name(prefix, struct_name, member.name)};\n')