From 3fdc00282820a10d42eff8aed3f17a67c91aa351 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Thu, 16 Apr 2026 12:45:12 -0400 Subject: [PATCH] generate-accessors: add lifecycle, defaults, and annotation redesign Extend the accessor generator with three new capabilities. 1. Lifecycle (constructor + destructor): annotate a struct's opening brace with //!generate-lifecycle to generate foo_new() and foo_free(). foo_new() allocates a zeroed instance with calloc() and returns -EINVAL / -ENOMEM on error. foo_free() frees all owned char* and char** members then frees the struct. Passing NULL to foo_free() is safe: destructors that dereference members guard with if (!p) return; those with no members to dereference rely on free(NULL) being a no-op. //!lifecycle:none on a member excludes it from the destructor. const char* members are never freed (assumed externally owned). 2. Defaults (init function): annotate individual members with //!default:VALUE to generate foo_init_defaults(), which assigns each annotated field its compile-time default. Any valid C expression is accepted as the value. When combined with //!generate-lifecycle, foo_new() calls foo_init_defaults() after allocation. foo_init_defaults() is also useful standalone to re-initialise a struct without reallocating. 3. Annotation style: drop support for the /*!annotation*/ block-comment style. Annotations now use only the // line-comment style. The parser was redesigned accordingly: after //, each !keyword token is treated as a command, so multiple annotations may share one comment: struct foo { //!generate-accessors !generate-lifecycle private.h and private-fabrics.h are updated throughout to use the new style. The generated .c files now include for EINVAL/ENOMEM. Apply these features to struct libnvmf_discovery_args, replacing the manually-written libnvmf_discovery_args_create() and libnvmf_discovery_args_free() with generated equivalents. The constructor is renamed _new (consistent with the generated naming convention). Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/examples/discover-loop.c | 2 +- libnvme/libnvme/nvme.i | 2 +- libnvme/src/accessors-fabrics.ld | 3 + libnvme/src/libnvmf.ld | 3 +- libnvme/src/nvme/accessors-fabrics.c | 26 + libnvme/src/nvme/accessors-fabrics.h | 30 + libnvme/src/nvme/accessors.c | 1 + libnvme/src/nvme/accessors.h | 4 +- libnvme/src/nvme/fabrics.c | 22 - libnvme/src/nvme/fabrics.h | 19 +- libnvme/src/nvme/private-fabrics.h | 23 +- libnvme/src/nvme/private.h | 30 +- libnvme/test/ioctl/discovery.c | 2 +- libnvme/tools/generator/generate-accessors.md | 290 +++++++++- libnvme/tools/generator/generate-accessors.py | 528 ++++++++++++++++-- libnvme/tools/generator/meson.build | 2 +- libnvme/tools/generator/update-accessors.sh | 2 +- 17 files changed, 835 insertions(+), 154 deletions(-) diff --git a/libnvme/examples/discover-loop.c b/libnvme/examples/discover-loop.c index 01bb2408bf..81541df833 100644 --- a/libnvme/examples/discover-loop.c +++ b/libnvme/examples/discover-loop.c @@ -94,7 +94,7 @@ int main() goto free_fctx; } - ret = libnvmf_discovery_args_create(&args); + ret = libnvmf_discovery_args_new(&args); if (!ret) { libnvmf_discovery_args_set_max_retries(args, 4); ret = libnvmf_get_discovery_log(c, args, &log); diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index 4717fbe59a..35d234afb6 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -829,7 +829,7 @@ struct libnvmf_context {}; discover_err = 1; return NULL; } - discover_err = libnvmf_discovery_args_create(&args); + discover_err = libnvmf_discovery_args_new(&args); if (discover_err) return NULL; libnvmf_discovery_args_set_lsp(args, lsp); diff --git a/libnvme/src/accessors-fabrics.ld b/libnvme/src/accessors-fabrics.ld index 59e9c0d6f7..75a4878c27 100644 --- a/libnvme/src/accessors-fabrics.ld +++ b/libnvme/src/accessors-fabrics.ld @@ -10,8 +10,11 @@ LIBNVMF_ACCESSORS_3 { global: + libnvmf_discovery_args_free; libnvmf_discovery_args_get_lsp; libnvmf_discovery_args_get_max_retries; + libnvmf_discovery_args_init_defaults; + libnvmf_discovery_args_new; libnvmf_discovery_args_set_lsp; libnvmf_discovery_args_set_max_retries; libnvmf_uri_get_fragment; diff --git a/libnvme/src/libnvmf.ld b/libnvme/src/libnvmf.ld index f9b54da071..59533dadaf 100644 --- a/libnvme/src/libnvmf.ld +++ b/libnvme/src/libnvmf.ld @@ -24,8 +24,7 @@ LIBNVMF_3 { libnvmf_ctrl_get_fabrics_config; libnvmf_disconnect_ctrl; libnvmf_discovery; - libnvmf_discovery_args_create; - libnvmf_discovery_args_free; + libnvmf_discovery_config_file; libnvmf_discovery_config_json; libnvmf_discovery_nbft; diff --git a/libnvme/src/nvme/accessors-fabrics.c b/libnvme/src/nvme/accessors-fabrics.c index eef1f58863..75d49a9b1d 100644 --- a/libnvme/src/nvme/accessors-fabrics.c +++ b/libnvme/src/nvme/accessors-fabrics.c @@ -17,6 +17,7 @@ * To update run: meson compile -C [BUILD-DIR] update-accessors * Or: make update-accessors */ +#include #include #include #include "accessors-fabrics.h" @@ -28,6 +29,31 @@ * Accessors for: struct libnvmf_discovery_args ****************************************************************************/ +__public int libnvmf_discovery_args_new(struct libnvmf_discovery_args **pp) +{ + if (!pp) + return -EINVAL; + *pp = calloc(1, sizeof(struct libnvmf_discovery_args)); + if (!*pp) + return -ENOMEM; + libnvmf_discovery_args_init_defaults(*pp); + return 0; +} + +__public void libnvmf_discovery_args_free(struct libnvmf_discovery_args *p) +{ + free(p); +} + +__public void libnvmf_discovery_args_init_defaults( + struct libnvmf_discovery_args *p) +{ + if (!p) + return; + p->max_retries = 6; + p->lsp = NVMF_LOG_DISC_LSP_NONE; +} + __public void libnvmf_discovery_args_set_max_retries( struct libnvmf_discovery_args *p, int max_retries) diff --git a/libnvme/src/nvme/accessors-fabrics.h b/libnvme/src/nvme/accessors-fabrics.h index 660171d7e3..e94616efb8 100644 --- a/libnvme/src/nvme/accessors-fabrics.h +++ b/libnvme/src/nvme/accessors-fabrics.h @@ -35,6 +35,36 @@ struct libnvmf_uri; * Accessors for: struct libnvmf_discovery_args ****************************************************************************/ +/** + * libnvmf_discovery_args_new() - Allocate and initialise a new instance. + * @pp: On success, *pp is set to the newly allocated object. + * + * Allocates a zeroed &struct libnvmf_discovery_args on the heap. + * The caller must release it with libnvmf_discovery_args_free(). + * + * Return: 0 on success, -EINVAL if @pp is NULL, + * -ENOMEM if allocation fails. + */ +int libnvmf_discovery_args_new(struct libnvmf_discovery_args **pp); + +/** + * libnvmf_discovery_args_free() - Release a libnvmf_discovery_args object. + * @p: Object previously returned by libnvmf_discovery_args_new(). + * A NULL pointer is silently ignored. + */ +void libnvmf_discovery_args_free(struct libnvmf_discovery_args *p); + +/** + * libnvmf_discovery_args_init_defaults() - Set fields to their defaults. + * @p: The &struct libnvmf_discovery_args instance to initialise. + * + * Sets each field that carries a default annotation to its + * compile-time default value. Called automatically by + * libnvmf_discovery_args_new() but may also be called directly to reset an + * instance to its defaults without reallocating it. + */ +void libnvmf_discovery_args_init_defaults(struct libnvmf_discovery_args *p); + /** * libnvmf_discovery_args_set_max_retries() - Set max_retries. * @p: The &struct libnvmf_discovery_args instance to update. diff --git a/libnvme/src/nvme/accessors.c b/libnvme/src/nvme/accessors.c index 76348ce1a3..43e0bbf7ec 100644 --- a/libnvme/src/nvme/accessors.c +++ b/libnvme/src/nvme/accessors.c @@ -17,6 +17,7 @@ * To update run: meson compile -C [BUILD-DIR] update-accessors * Or: make update-accessors */ +#include #include #include #include "accessors.h" diff --git a/libnvme/src/nvme/accessors.h b/libnvme/src/nvme/accessors.h index 22fcf1c269..834fc948a5 100644 --- a/libnvme/src/nvme/accessors.h +++ b/libnvme/src/nvme/accessors.h @@ -236,7 +236,7 @@ long libnvme_fabrics_config_get_tls_key_id( const struct libnvme_fabrics_config *p); /** - * libnvme_fabrics_config_set_tls_configured_key_id() - Set tls_configured_key_id. + * libnvme_fabrics_config_set_tls_configured_key_id() - Setter. * @p: The &struct libnvme_fabrics_config instance to update. * @tls_configured_key_id: Value to assign to the tls_configured_key_id field. */ @@ -245,7 +245,7 @@ void libnvme_fabrics_config_set_tls_configured_key_id( long tls_configured_key_id); /** - * libnvme_fabrics_config_get_tls_configured_key_id() - Get tls_configured_key_id. + * libnvme_fabrics_config_get_tls_configured_key_id() - Getter. * @p: The &struct libnvme_fabrics_config instance to query. * * Return: The value of the tls_configured_key_id field. diff --git a/libnvme/src/nvme/fabrics.c b/libnvme/src/nvme/fabrics.c index 0820ca9aed..bc36268a80 100644 --- a/libnvme/src/nvme/fabrics.c +++ b/libnvme/src/nvme/fabrics.c @@ -1494,28 +1494,6 @@ static void sanitize_discovery_log_entry(struct libnvme_global_ctx *ctx, } } -__public int libnvmf_discovery_args_create(struct libnvmf_discovery_args **argsp) -{ - struct libnvmf_discovery_args *args; - - if (!argsp) - return -EINVAL; - - args = calloc(1, sizeof(*args)); - if (!args) - return -ENOMEM; - - args->max_retries = 6; - args->lsp = NVMF_LOG_DISC_LSP_NONE; - - *argsp = args; - return 0; -} - -__public void libnvmf_discovery_args_free(struct libnvmf_discovery_args *args) -{ - free(args); -} __public int libnvmf_get_discovery_log(libnvme_ctrl_t ctrl, const struct libnvmf_discovery_args *args, diff --git a/libnvme/src/nvme/fabrics.h b/libnvme/src/nvme/fabrics.h index 0c8c911be6..f736c3c543 100644 --- a/libnvme/src/nvme/fabrics.h +++ b/libnvme/src/nvme/fabrics.h @@ -157,7 +157,7 @@ int libnvmf_connect_ctrl(libnvme_ctrl_t c); /* * struct libnvmf_discovery_args - Opaque arguments for libnvmf_get_discovery_log() * - * Allocate with libnvmf_discovery_args_create() and release with + * Allocate with libnvmf_discovery_args_new() and release with * libnvmf_discovery_args_free(). Use the setter/getter accessors to configure * fields; do not access members directly. */ @@ -168,23 +168,6 @@ struct libnvmf_discovery_args; */ struct libnvmf_uri; -/** - * libnvmf_discovery_args_create() - Allocate a discovery args object - * @argsp: On success, set to the newly allocated object - * - * Allocates and initialises a &struct libnvmf_discovery_args with sensible - * defaults. The caller must release it with libnvmf_discovery_args_free(). - * - * Return: 0 on success, or a negative error code on failure. - */ -int libnvmf_discovery_args_create(struct libnvmf_discovery_args **argsp); - -/** - * libnvmf_discovery_args_free() - Release a discovery args object - * @args: Object previously returned by libnvmf_discovery_args_create() - */ -void libnvmf_discovery_args_free(struct libnvmf_discovery_args *args); - /** * libnvmf_get_discovery_log() - Fetch the NVMe-oF discovery log page * @ctrl: Discovery controller diff --git a/libnvme/src/nvme/private-fabrics.h b/libnvme/src/nvme/private-fabrics.h index a56ab29287..367aafd66e 100644 --- a/libnvme/src/nvme/private-fabrics.h +++ b/libnvme/src/nvme/private-fabrics.h @@ -1,10 +1,18 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ /* + * NVMe-oF private struct definitions. + * * This file is part of libnvme. * Copyright (c) 2026, Dell Technologies Inc. or its subsidiaries. * * Authors: Martin Belanger + * + * Structs in this file are NVMe-oF-specific (fabrics layer). They are kept + * separate from private.h so that PCIe-only builds can exclude this entire + * file and its generated accessors (accessors-fabrics.{h,c}) along with the + * rest of the fabrics layer. */ + #pragma once #include @@ -69,18 +77,9 @@ struct libnvmf_context { }; -/** - * NVMe-oF private struct definitions. - * - * Structs in this file are NVMe-oF-specific (fabrics layer). They are kept - * separate from private.h so that PCIe-only builds can exclude this entire - * file and its generated accessors (accessors-fabrics.{h,c}) along with the - * rest of the fabrics layer. - */ - -struct libnvmf_discovery_args { /*!generate-accessors*/ - int max_retries; - __u8 lsp; +struct libnvmf_discovery_args { //!generate-accessors !generate-lifecycle + int max_retries; //!default:6 + __u8 lsp; //!default:NVMF_LOG_DISC_LSP_NONE }; /** diff --git a/libnvme/src/nvme/private.h b/libnvme/src/nvme/private.h index a1c879f349..9fef581fbd 100644 --- a/libnvme/src/nvme/private.h +++ b/libnvme/src/nvme/private.h @@ -167,7 +167,7 @@ struct libnvme_transport_handle { struct libnvme_log *log; }; -struct libnvme_path { /*!generate-accessors*/ +struct libnvme_path { //!generate-accessors struct list_node entry; struct list_node nentry; @@ -189,7 +189,7 @@ struct libnvme_ns_head { char *sysfs_dir; }; -struct libnvme_ns { /*!generate-accessors*/ +struct libnvme_ns { //!generate-accessors struct list_node entry; struct libnvme_subsystem *s; @@ -215,7 +215,7 @@ struct libnvme_ns { /*!generate-accessors*/ enum nvme_csi csi; }; -struct libnvme_ctrl { /*!generate-accessors*/ +struct libnvme_ctrl { //!generate-accessors struct list_node entry; struct list_head paths; struct list_head namespaces; @@ -255,30 +255,30 @@ struct libnvme_ctrl { /*!generate-accessors*/ struct libnvme_fabrics_config cfg; }; -struct libnvme_subsystem { /*!generate-accessors*/ +struct libnvme_subsystem { //!generate-accessors struct list_node entry; struct list_head ctrls; 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; //!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 *application; char *iopolicy; }; -struct libnvme_host { /*!generate-accessors*/ +struct libnvme_host { //!generate-accessors struct list_node entry; struct list_head subsystems; struct libnvme_global_ctx *ctx; - char *hostnqn; /*!accessors:readonly*/ - char *hostid; /*!accessors:readonly*/ + char *hostnqn; //!accessors:readonly + char *hostid; //!accessors:readonly char *dhchap_host_key; char *hostsymname; bool pdc_enabled; //!accessors:none @@ -286,7 +286,7 @@ struct libnvme_host { /*!generate-accessors*/ * value */ }; -struct libnvme_fabric_options { /*!generate-accessors*/ +struct libnvme_fabric_options { //!generate-accessors bool cntlid; bool concat; bool ctrl_loss_tmo; diff --git a/libnvme/test/ioctl/discovery.c b/libnvme/test/ioctl/discovery.c index 6e99e47695..07ae897350 100644 --- a/libnvme/test/ioctl/discovery.c +++ b/libnvme/test/ioctl/discovery.c @@ -45,7 +45,7 @@ static int fetch_discovery_log(libnvme_ctrl_t c, struct libnvmf_discovery_args *args; int err; - err = libnvmf_discovery_args_create(&args); + err = libnvmf_discovery_args_new(&args); if (err) return err; libnvmf_discovery_args_set_max_retries(args, max_retries); diff --git a/libnvme/tools/generator/generate-accessors.md b/libnvme/tools/generator/generate-accessors.md index 9f5baf7508..c8ed9787d9 100644 --- a/libnvme/tools/generator/generate-accessors.md +++ b/libnvme/tools/generator/generate-accessors.md @@ -1,6 +1,6 @@ # Generate Accessors Tool -This tool generates **setter and getter functions** for C structs automatically. It supports dynamic strings, fixed-size char arrays, and `const` fields, with control over which structs and members participate via **in-source annotations**. +This tool generates **setter and getter functions** for C structs automatically. It also optionally generates **constructor and destructor functions** (`foo_new` / `foo_free`). It supports dynamic strings, fixed-size char arrays, and `const` fields, with control over which structs and members participate via **in-source annotations**. ------ @@ -25,7 +25,7 @@ python3 generate-accessors.py [options] ## Annotations -Struct inclusion and member behaviour are controlled by **annotations written as comments directly in the header file**. Both `/* */` (block) and `//` (line) comment styles are supported for every annotation. +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: ### Struct inclusion — `generate-accessors` @@ -39,7 +39,7 @@ Place the annotation on the same line as the struct's opening brace to opt that | `//!generate-accessors:writeonly` | setter only | ```c -struct nvme_ctrl { /*!generate-accessors*/ /* both getter and setter */ +struct nvme_ctrl { //!generate-accessors /* both getter and setter */ ... }; @@ -57,10 +57,10 @@ Individual members can always override the struct-level default using a per-memb Place the annotation on a member's declaration line to suppress accessor generation for that member entirely (no setter, no getter): ```c -struct nvme_ctrl { /*!generate-accessors*/ +struct nvme_ctrl { //!generate-accessors char *name; char *state; //!accessors:none - char *subsysnqn; /*!accessors:none*/ + char *subsysnqn; //!accessors:none }; ``` @@ -69,10 +69,10 @@ struct nvme_ctrl { /*!generate-accessors*/ 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: ```c -struct nvme_ctrl { /*!generate-accessors*/ +struct nvme_ctrl { //!generate-accessors char *name; char *firmware; //!accessors:readonly - char *model; /*!accessors:readonly*/ + char *model; //!accessors:readonly }; ``` @@ -83,7 +83,7 @@ Members declared with the `const` qualifier are also automatically read-only. 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: ```c -struct nvme_ctrl { /*!generate-accessors:readonly*/ +struct nvme_ctrl { //!generate-accessors:readonly char *name; /* getter only (struct default) */ char *token; //!accessors:writeonly /* setter only override */ }; @@ -94,26 +94,87 @@ struct nvme_ctrl { /*!generate-accessors:readonly*/ 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`): ```c -struct nvme_ctrl { /*!generate-accessors:none*/ +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 */ }; ``` +### 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: + +```c +struct nvme_ctrl { //!generate-lifecycle + char *name; + char *subsysnqn; + char *serial; //!lifecycle:none /* excluded from destructor */ +}; +``` + +This generates: + +- **`nvme_ctrl_new(struct nvme_ctrl **pp)`** — allocates a zeroed instance on the heap. Returns `0` on success, `-EINVAL` if `pp` is `NULL`, or `-ENOMEM` on allocation failure. +- **`nvme_ctrl_free(struct nvme_ctrl *p)`** — frees all `char *` and `char **` members (except those marked `//!lifecycle:none`) and then frees the struct itself. A `NULL` argument is silently ignored. + +`generate-lifecycle` can appear alongside `generate-accessors` in the same comment: + +```c +struct nvme_ctrl { //!generate-accessors !generate-lifecycle + char *name; + char *subsysnqn; +}; +``` + +`const char *` members are **never** freed by the destructor — they are assumed to point to externally owned storage. + +### Lifecycle member exclusion — `lifecycle:none` + +Place the annotation on a member's declaration line to exclude it from the destructor's free logic: + +```c +struct nvme_ctrl { //!generate-lifecycle + char *name; + char *borrowed; //!lifecycle:none /* not freed — caller owns this */ +}; +``` + +This annotation has no effect on accessor generation. Combine with `//!accessors:none` if both should be suppressed. + +### Member defaults — `default:VALUE` + +Place the annotation on a member's declaration line to assign a compile-time default value. When any member in the struct carries this annotation, a `foo_init_defaults()` function is generated: + +```c +struct libnvmf_discovery_args { //!generate-accessors !generate-lifecycle + int max_retries; //!default:6 + __u8 lsp; //!default:NVMF_LOG_DISC_LSP_NONE +}; +``` + +This generates `libnvmf_discovery_args_init_defaults()`, which sets each annotated field to its default value. If `generate-lifecycle` is also present, the constructor automatically calls `init_defaults()` after allocation. This lets callers re-initialise an existing instance without freeing and reallocating it. + +The value is emitted verbatim, so any valid C expression — integer literals, macro names, enum constants — is accepted. + +`init_defaults()` can be used independently of `generate-lifecycle`. + ### 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 | -| `//!accessors:none` | member line | Skip this member entirely | -| `//!accessors:readonly` | member line | Generate getter only | -| `//!accessors:writeonly` | member line | Generate setter only | -| `//!accessors:readwrite` | member line | Generate getter and setter | -| `const` qualifier on member | member type | Suppress setter (built-in, always applies) | +| 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 | +| `//!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 | +| `//!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 | ------ @@ -122,7 +183,7 @@ struct nvme_ctrl { /*!generate-accessors:none*/ ### Header file (`person.h`) ```c -struct person { /*!generate-accessors*/ +struct person { //!generate-accessors char *name; int age; const char *id; /* const → getter only, no annotation needed */ @@ -130,7 +191,7 @@ struct person { /*!generate-accessors*/ char *role; //!accessors:readonly }; -struct car { /*!generate-accessors*/ +struct car { //!generate-accessors char *model; int year; const char *vin; @@ -365,6 +426,177 @@ LIBNVME_ACCESSORS_3 { ------ +## Lifecycle example + +### Header file (`person.h`) — with lifecycle + +Adding `//!generate-lifecycle` to the same struct enables constructor and destructor generation alongside the accessors: + +```c +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 +}; +``` + +### Additional declarations in `accessors.h` + +The constructor and destructor declarations are appended after the accessor declarations for the same struct: + +```c +/** + * person_new() - Allocate and initialise a person object. + * @pp: On success, *pp is set to the newly allocated object. + * + * Allocates a zeroed &struct person on the heap. + * The caller must release it with person_free(). + * + * Return: 0 on success, -EINVAL if @pp is NULL, + * -ENOMEM if allocation fails. + */ +int person_new(struct person **pp); + +/** + * person_free() - Release a person object. + * @p: Object previously returned by person_new(). + * A NULL pointer is silently ignored. + */ +void person_free(struct person *p); +``` + +### Additional implementations in `accessors.c` + +```c +__public int person_new(struct person **pp) +{ + if (!pp) + return -EINVAL; + *pp = calloc(1, sizeof(struct person)); + return *pp ? 0 : -ENOMEM; +} + +__public void person_free(struct person *p) +{ + if (!p) + return; + free(p->name); + free(p->secret); + free(p->role); + free(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. +> - `age` is `int` — only `char *` and `char **` members are freed. + +### Additional entries in `accessors.ld` + +``` + person_new; + person_free; +``` + +------ + +## Defaults example + +```c +struct conn_opts { //!generate-accessors !generate-lifecycle + int port; //!default:4420 + char *transport; //!default:"tcp" + const char *trsvcid; //!default:"4420" +}; +``` + +### Generated declaration in `accessors.h` + +```c +/** + * conn_opts_init_defaults() - Apply default values to a conn_opts instance. + * @p: The &struct conn_opts instance to initialise. + * + * Sets each field that carries a default annotation to its + * compile-time default value. Called automatically by + * conn_opts_new() but may also be called directly to reset + * an instance to its defaults without reallocating it. + */ +void conn_opts_init_defaults(struct conn_opts *p); +``` + +### Generated implementation in `accessors.c` + +Note how `transport` (`char *`) is assigned via `strdup()` — the struct owns +the memory and the destructor frees it. In contrast, `trsvcid` (`const char *`) +receives a plain assignment to a string literal — no heap allocation, no free. + +```c +__public void conn_opts_init_defaults(struct conn_opts *p) +{ + if (!p) + return; + p->port = 4420; + if (!p->transport || strcmp(p->transport, "tcp") != 0) { + free(p->transport); + p->transport = strdup("tcp"); + } + p->trsvcid = "4420"; +} + +__public int conn_opts_new(struct conn_opts **pp) +{ + if (!pp) + return -EINVAL; + *pp = calloc(1, sizeof(struct conn_opts)); + if (!*pp) + return -ENOMEM; + conn_opts_init_defaults(*pp); + return 0; +} + +__public void conn_opts_free(struct conn_opts *p) +{ + if (!p) + return; + free(p->transport); + free(p); +} +``` + +> **Notes:** +> - Scalar members (`int`, `__u8`, etc.) are assigned directly. +> - `char *` members use a compare-before-replace pattern: if the current +> value already matches the default (`strcmp`), nothing happens; otherwise +> the old value is freed and the new default is `strdup()`'d. This makes +> `init_defaults()` safe to call on an already-initialised struct without +> leaking memory. +> - `const char *` members are assigned directly (no `strdup`) since they +> are assumed to point to externally owned storage. They are also not +> freed by the destructor, as seen in `conn_opts_free` — `trsvcid` has +> no `free()` call. +> - The constructor (`_new`) calls `init_defaults()` after `calloc()`, so +> freshly allocated structs always start at their defined defaults rather +> than zero. + +### Additional entries in `accessors.ld` + +``` + conn_opts_new; + conn_opts_free; + conn_opts_init_defaults; + conn_opts_get_port; + conn_opts_set_port; + conn_opts_get_transport; + conn_opts_set_transport; + conn_opts_get_trsvcid; +``` + +------ + ## Limitations - `typedef struct` is not supported. @@ -378,11 +610,17 @@ LIBNVME_ACCESSORS_3 { 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). +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 generator. +8. **`//!accessors: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. **`--prefix`** — prepended to every function name (e.g. `--prefix nvme_` turns `ctrl_set_name` into `nvme_ctrl_set_name`). -11. **Line length** — generated code is automatically wrapped to stay within the 80-column limit required by `checkpatch.pl`. +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. +13. **`//!default:VALUE`** — generates `foo_init_defaults()` that sets the annotated field to `VALUE`. Scalar members are assigned directly. `char *` members use a compare-before-replace pattern: if the current value already equals the default (`strcmp`), nothing happens; otherwise the old value is freed and the new default is `strdup()`'d. `const char *` members are assigned directly (no `strdup`). Quoted string values (`"foo bar"`) may contain spaces. +14. **`init_defaults()` and `new()`** — when a struct has both `generate-lifecycle` and at least one `//!default:`, the constructor calls `init_defaults()` after `calloc()`. Without `generate-lifecycle`, `init_defaults()` is still generated as a standalone function. +15. **`init_defaults()` for re-initialisation** — callers can call `init_defaults()` directly on an already-allocated instance to reset scalar fields to their defaults without freeing and reallocating the struct. +16. **`--prefix`** — prepended to every function name (e.g. `--prefix nvme_` turns `ctrl_set_name` into `nvme_ctrl_set_name`). +17. **Line length** — generated code is automatically wrapped to stay within the 80-column limit required by `checkpatch.pl`. diff --git a/libnvme/tools/generator/generate-accessors.py b/libnvme/tools/generator/generate-accessors.py index 614396ba78..2b865d3cd1 100755 --- a/libnvme/tools/generator/generate-accessors.py +++ b/libnvme/tools/generator/generate-accessors.py @@ -16,35 +16,54 @@ - Does not support typedef struct. - 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 +annotations can appear in one comment: + struct nvme_ctrl { //!generate-accessors !generate-lifecycle + 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 — default: both getter and setter - struct nvme_ctrl { /*!generate-accessors:none*/ — default: no accessors struct nvme_ctrl { //!generate-accessors:none — default: no accessors - struct nvme_ctrl { /*!generate-accessors:readonly*/ — default: getter only struct nvme_ctrl { //!generate-accessors:readonly — default: getter only - struct nvme_ctrl { /*!generate-accessors:writeonly*/ — default: setter only - struct nvme_ctrl { //!generate-accessors:writeonly — default: setter only + struct nvme_ctrl { //!generate-accessors:writeonly — default: setter only + +Lifecycle (constructor + destructor) — annotate the opening brace line: + struct nvme_ctrl { //!generate-lifecycle +The two annotations are independent and may appear in the same comment: + struct nvme_ctrl { //!generate-accessors !generate-lifecycle + +Lifecycle member exclusion — annotate the member declaration line: + char *cache; //!lifecycle:none — skip this member in the destructor + +Defaults — annotate the member declaration line with a value to assign: + int max_retries; //!default:6 + __u8 lsp; //!default:NVMF_LOG_DISC_LSP_NONE + char *transport; //!default:"tcp" +When any member carries a default annotation, an init_defaults function is +generated. If generate-lifecycle is also present, the constructor calls it. +The init_defaults function is also useful standalone to re-initialise a +struct to its defaults without reallocating it. +For scalar members the value is assigned directly. For char* members the +generated code avoids unnecessary work by comparing the current value with +the default first (strcmp); if they differ it frees the old value and +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; /*!accessors:none*/ char *model; //!accessors: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; //!accessors:readonly Write-only members (setter only, getter suppressed): - Annotate the member declaration line: - char *state; /*!accessors:writeonly*/ char *state; //!accessors:writeonly Both getter and setter (override a restrictive struct-level default): - Annotate the member declaration line: - char *state; /*!accessors:readwrite*/ char *state; //!accessors:readwrite Example usage: @@ -126,9 +145,32 @@ # Annotation helpers # --------------------------------------------------------------------------- +def _comment_text(text): + """Return the portion of *text* after the first '//', or None. + + This is the raw comment payload — the text the parser scans for + ``!keyword`` annotation tokens. + """ + idx = text.find('//') + return text[idx + 2:] if idx >= 0 else None + + def has_annotation(text, annotation): - """Return True if *text* contains /*!annotation*/ or //!annotation.""" - return f'/*!{annotation}*/' in text or f'//!{annotation}' in text + """Return True if *text* carries ``!annotation`` inside a ``//`` comment. + + Annotations are ``!keyword`` tokens that appear anywhere after ``//`` on + the same line. A single comment may carry several annotations, e.g.:: + + struct foo { //!generate-accessors !generate-lifecycle + + The match is token-delimited: ``!generate-accessors`` will not match + inside ``!generate-accessors:none``. + """ + comment = _comment_text(text) + if comment is None: + return False + return bool(re.search( + rf'!{re.escape(annotation)}(?=[\s!]|$)', comment)) def strip_block_comments(text): @@ -195,6 +237,19 @@ def fits_80_ntabs(n, s): return len(s) + n * 7 <= 80 +def kdoc_summary(fn, *descriptions): + """Return a KernelDoc summary line that fits within 80 columns. + + Tries each description in order and returns the first that fits as + ' * fn() - description'. Falls back to ' * fn()' if none fit. + """ + for desc in descriptions: + line = f' * {fn}() - {desc}' + if fits_80(line): + return line + return f' * {fn}()' + + # --------------------------------------------------------------------------- # Member data class # --------------------------------------------------------------------------- @@ -320,11 +375,7 @@ def parse_members(struct_name, raw_body, struct_mode, verbose): def parse_struct_annotation(raw_body): """Return the default mode for a struct from its generate-accessors annotation. - Recognises both comment styles with an optional mode qualifier: - /*!generate-accessors*/ → 'both' - /*!generate-accessors:none*/ → 'none' - /*!generate-accessors:readonly*/ → 'readonly' - /*!generate-accessors:writeonly*/ → 'writeonly' + Recognises the ``//!`` comment style with an optional mode qualifier: //!generate-accessors → 'both' //!generate-accessors:none → 'none' //!generate-accessors:readonly → 'readonly' @@ -335,33 +386,190 @@ def parse_struct_annotation(raw_body): """ first_token = raw_body.lstrip() - for pattern in ( - r'/\*!generate-accessors(?::([a-z]+))?\*/', - r'//!generate-accessors(?::([a-z]+))?', - ): - m = re.match(pattern, first_token) - 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 + m = re.match(r'//!generate-accessors(?::([a-z]+))?', first_token) + 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 return None +def parse_lifecycle_annotation(raw_body): + """Return True when *raw_body* carries a ``!generate-lifecycle`` annotation. + + The annotation must appear inside a ``//`` comment on the struct's opening + brace line. It may share the comment with other annotations:: + + struct foo { //!generate-accessors !generate-lifecycle + """ + for line in raw_body.splitlines(): + if has_annotation(line, 'generate-lifecycle'): + return True + return False + + +# --------------------------------------------------------------------------- +# Lifecycle member: name + whether it is a char** (string array) +# --------------------------------------------------------------------------- + +class LifecycleMember: + """A char* or char** member that the destructor must free.""" + + __slots__ = ('name', 'is_char_ptr_array') + + def __init__(self, name, is_char_ptr_array): + self.name = name + self.is_char_ptr_array = is_char_ptr_array + + +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) + - respects ``lifecycle:none`` to let callers opt a member out + - collects only char* and char** members (the only types that need + explicit freeing) + """ + members = [] + + for raw_line in raw_body.splitlines(): + if has_annotation(raw_line, 'lifecycle:none'): + continue + + clean = strip_inline_comment(strip_block_comments(raw_line)).strip() + + if not clean or ';' not in clean: + continue + if 'static' in clean or 'struct' in clean: + continue + + m = MEMBER_RE.match(clean) + if not m: + continue + + is_const = bool(m.group(1)) + type_base = m.group(2) + ptr_part = m.group(3) + name = m.group(4) + + # const char * members are not owned by the struct (no strdup), + # so the destructor must not free them. + if is_const: + continue + + ptr_depth = ptr_part.count('*') + if not ptr_depth or type_base != 'char': + continue + + if ptr_depth > 2: + continue + + members.append(LifecycleMember( + name=name, + is_char_ptr_array=(ptr_depth == 2), + )) + + return members + + +# --------------------------------------------------------------------------- +# Default member: name + default value expression +# --------------------------------------------------------------------------- + +class DefaultMember: + """A member that carries a ``//!default:VALUE`` annotation.""" + + __slots__ = ('name', 'value', 'is_char_ptr') + + def __init__(self, name, value, is_char_ptr=False): + self.name = name + self.value = value # raw value string, emitted verbatim + self.is_char_ptr = is_char_ptr # True → emit strdup/free pattern + + +def parse_default_annotation(raw_line): + """Return the default value string from a ``!default:VALUE`` annotation. + + The annotation must appear inside a ``//`` comment:: + + int port; //!default:4420 + char *host; //!default:"localhost" + + VALUE may be a quoted C string literal (``"foo bar"``), which may contain + spaces, or any non-whitespace token (integer literal, macro name, etc.). + + Returns None when no annotation is found. + """ + comment = _comment_text(raw_line) + if comment is None: + return None + # Quoted string (double-quoted, with basic escape support) or bare token. + _val = r'"(?:[^"\\]|\\.)*"|\S+' + m = re.search(rf'!default:({_val})', comment) + return m.group(1) if m else None + + +def parse_members_for_defaults(raw_body): + """Return a list of DefaultMember for every member with a default annotation. + + Any member in the struct body that carries ``//!default:VALUE`` is + collected here, regardless of its accessor or lifecycle status. + The value is emitted verbatim in the generated assignment, so any + valid C expression (integer literal, macro name, etc.) is accepted. + """ + defaults = [] + + for raw_line in raw_body.splitlines(): + value = parse_default_annotation(raw_line) + if value is None: + continue + + clean = strip_inline_comment(strip_block_comments(raw_line)).strip() + + if not clean or ';' not in clean: + continue + if 'static' in clean or 'struct' in clean: + continue + + # Try char array first, then general member regex. + m = CHAR_ARRAY_RE.match(clean) + if m: + defaults.append(DefaultMember(name=m.group(2), value=value, + is_char_ptr=False)) + continue + + m = MEMBER_RE.match(clean) + if m: + is_const = bool(m.group(1)) + type_base = m.group(2) + ptr_part = m.group(3) + name = m.group(4) + is_char_ptr = (type_base == 'char' + and ptr_part.count('*') == 1 + and not is_const) + defaults.append(DefaultMember(name=name, value=value, + is_char_ptr=is_char_ptr)) + + return defaults + + def parse_file(text, verbose): - """Return list of (struct_name, [Member]) tuples found in *text*. + """Return list of (struct_name, [Member], [LifecycleMember], + [DefaultMember]) tuples. - Only structs annotated with ``/*!generate-accessors*/`` or - ``//!generate-accessors`` as the first token inside the opening brace - are processed. + Only structs annotated with ``//!generate-accessors`` as the first token + inside the opening brace, or with ``//!generate-lifecycle`` anywhere + inside the opening brace line, are processed. """ result = [] @@ -369,18 +577,34 @@ def parse_file(text, verbose): struct_name = match.group(1) raw_body = match.group(2) - struct_mode = parse_struct_annotation(raw_body) - if struct_mode is None: + struct_mode = parse_struct_annotation(raw_body) + want_lifecycle = parse_lifecycle_annotation(raw_body) + + if struct_mode is None and not want_lifecycle: continue - members = parse_members(struct_name, raw_body, struct_mode, verbose) + members = [] + if struct_mode is not None: + members = parse_members( + struct_name, raw_body, struct_mode, verbose) + + lc_members = None + if want_lifecycle: + lc_members = parse_members_for_lifecycle(raw_body) - if verbose and members: - print(f"Found struct: {struct_name} ({len(members)} members)" - f" [mode: {struct_mode}]") + default_members = parse_members_for_defaults(raw_body) - if members: - result.append((struct_name, members)) + 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" + 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 + else "no defaults") + print(f"Found struct: {struct_name} — {acc}, {lc}, {df}") + + if members or lc_members is not None or default_members: + result.append((struct_name, members, lc_members, default_members)) return result @@ -399,9 +623,10 @@ def _get_name(prefix, sname, mname): def emit_hdr_setter_str(f, prefix, sname, mname, is_dyn_str): """Emit a header declaration for a string setter.""" + fn = _set_name(prefix, sname, mname) f.write( f'/**\n' - f' * {_set_name(prefix, sname, mname)}() - Set {mname}.\n' + f'{kdoc_summary(fn, f"Set {mname}.", "Setter.")}\n' f' * @p: The &struct {sname} instance to update.\n' ) if is_dyn_str: @@ -428,9 +653,10 @@ def emit_hdr_setter_str(f, prefix, sname, mname, is_dyn_str): def emit_hdr_setter_str_array(f, prefix, sname, mname): """Emit a header declaration for a string-array setter.""" + fn = _set_name(prefix, sname, mname) f.write( f'/**\n' - f' * {_set_name(prefix, sname, mname)}() - Set {mname}.\n' + f'{kdoc_summary(fn, f"Set {mname}.", "Setter.")}\n' f' * @p: The &struct {sname} instance to update.\n' f' * @{mname}: New NULL-terminated string array; deep-copied.\n' f' */\n' @@ -450,9 +676,10 @@ def emit_hdr_setter_str_array(f, prefix, sname, mname): def emit_hdr_setter_val(f, prefix, sname, mname, mtype): """Emit a header declaration for a value setter.""" + fn = _set_name(prefix, sname, mname) f.write( f'/**\n' - f' * {_set_name(prefix, sname, mname)}() - Set {mname}.\n' + f'{kdoc_summary(fn, f"Set {mname}.", "Setter.")}\n' f' * @p: The &struct {sname} instance to update.\n' f' * @{mname}: Value to assign to the {mname} field.\n' f' */\n' @@ -472,10 +699,11 @@ def emit_hdr_setter_val(f, prefix, sname, mname, mtype): def emit_hdr_getter(f, prefix, sname, mname, mtype, is_dyn_str): """Emit a header declaration for a getter.""" + fn = _get_name(prefix, sname, mname) tail = ', or NULL if not set.' if is_dyn_str else '.' f.write( f'/**\n' - f' * {_get_name(prefix, sname, mname)}() - Get {mname}.\n' + f'{kdoc_summary(fn, f"Get {mname}.", "Getter.")}\n' f' * @p: The &struct {sname} instance to query.\n' f' *\n' f' * Return: The value of the {mname} field{tail}\n' @@ -667,12 +895,194 @@ def generate_src(f, prefix, struct_name, members): cast=cast) +# --------------------------------------------------------------------------- +# Lifecycle (constructor / destructor) emitters +# --------------------------------------------------------------------------- + +def _new_name(prefix, sname): + return f'{prefix}{sname}_new' + + +def _free_name(prefix, sname): + return f'{prefix}{sname}_free' + + +def _init_defaults_name(prefix, sname): + return f'{prefix}{sname}_init_defaults' + + +def emit_hdr_defaults(f, prefix, sname, default_members): + """Emit header declaration for the init_defaults function.""" + fn = _init_defaults_name(prefix, sname) + new_fn = _new_name(prefix, sname) + f.write( + f'/**\n' + f'{kdoc_summary(fn, f"Apply default values to a {sname} instance.", "Set fields to their defaults.", "Initialise to defaults.")}\n' + f' * @p: The &struct {sname} instance to initialise.\n' + f' *\n' + f' * Sets each field that carries a default annotation to its\n' + f' * compile-time default value. Called automatically by\n' + f' * {new_fn}() but may also be called directly to reset an\n' + f' * instance to its defaults without reallocating it.\n' + f' */\n' + ) + single = f'void {fn}(struct {sname} *p);' + if fits_80(single): + f.write(single + '\n\n') + else: + f.write(f'void {fn}(\n\t\tstruct {sname} *p);\n\n') + + +def emit_src_defaults(f, prefix, sname, default_members): + """Emit the init_defaults function implementation.""" + fn = _init_defaults_name(prefix, sname) + sig = f'{PUB}void {fn}(struct {sname} *p)' + if fits_80(sig): + f.write(sig + '\n') + else: + f.write(f'{PUB}void {fn}(\n\t\tstruct {sname} *p)\n') + + f.write('{\n') + f.write('\tif (!p)\n\t\treturn;\n') + for dm in default_members: + if dm.is_char_ptr: + # Skip the assignment when the string is already at its default + # value; otherwise free the old value and strdup the new one. + cmp = f'\tif (!p->{dm.name} || strcmp(p->{dm.name}, {dm.value}) != 0) {{' + if fits_80_ntabs(1, cmp.lstrip()): + f.write(cmp + '\n') + else: + f.write( + f'\tif (!p->{dm.name} ||\n' + f'\t strcmp(p->{dm.name}, {dm.value}) != 0) {{\n' + ) + f.write(f'\t\tfree(p->{dm.name});\n') + f.write(f'\t\tp->{dm.name} = strdup({dm.value});\n') + f.write('\t}\n') + else: + f.write(f'\tp->{dm.name} = {dm.value};\n') + f.write('}\n\n') + + +def emit_hdr_lifecycle(f, prefix, sname, lc_members): + """Emit header declarations for the constructor and destructor.""" + + # --- constructor ------------------------------------------------------- + new_fn = _new_name(prefix, sname) + f.write( + f'/**\n' + f'{kdoc_summary(new_fn, f"Allocate and initialise a {sname} object.", "Allocate and initialise a new instance.", "Constructor.")}\n' + f' * @pp: On success, *pp is set to the newly allocated object.\n' + f' *\n' + f' * Allocates a zeroed &struct {sname} on the heap.\n' + f' * The caller must release it with {_free_name(prefix, sname)}().\n' + f' *\n' + f' * Return: 0 on success, -EINVAL if @pp is NULL,\n' + f' * -ENOMEM if allocation fails.\n' + f' */\n' + ) + single = f'int {new_fn}(struct {sname} **pp);' + if fits_80(single): + f.write(single + '\n\n') + else: + f.write(f'int {new_fn}(\n\t\tstruct {sname} **pp);\n\n') + + # --- destructor -------------------------------------------------------- + free_fn = _free_name(prefix, sname) + f.write( + f'/**\n' + f'{kdoc_summary(free_fn, f"Release a {sname} object.", "Release this instance.", "Destructor.")}\n' + f' * @p: Object previously returned by {new_fn}().\n' + f' * A NULL pointer is silently ignored.\n' + f' */\n' + ) + single = f'void {free_fn}(struct {sname} *p);' + if fits_80(single): + f.write(single + '\n\n') + else: + f.write(f'void {free_fn}(\n\t\tstruct {sname} *p);\n\n') + + +def emit_src_lifecycle(f, prefix, sname, lc_members, default_members): + """Emit constructor and destructor implementations.""" + + # --- constructor ------------------------------------------------------- + new_fn = _new_name(prefix, sname) + sig = f'{PUB}int {new_fn}(struct {sname} **pp)' + if fits_80(sig): + f.write(sig + '\n') + else: + f.write(f'{PUB}int {new_fn}(\n\t\tstruct {sname} **pp)\n') + + if default_members: + init_fn = _init_defaults_name(prefix, sname) + f.write( + '{\n' + '\tif (!pp)\n' + '\t\treturn -EINVAL;\n' + f'\t*pp = calloc(1, sizeof(struct {sname}));\n' + '\tif (!*pp)\n' + '\t\treturn -ENOMEM;\n' + f'\t{init_fn}(*pp);\n' + '\treturn 0;\n' + '}\n\n' + ) + else: + f.write( + '{\n' + '\tif (!pp)\n' + '\t\treturn -EINVAL;\n' + f'\t*pp = calloc(1, sizeof(struct {sname}));\n' + '\treturn *pp ? 0 : -ENOMEM;\n' + '}\n\n' + ) + + # --- destructor -------------------------------------------------------- + free_fn = _free_name(prefix, sname) + sig = f'{PUB}void {free_fn}(struct {sname} *p)' + if fits_80(sig): + f.write(sig + '\n') + else: + f.write(f'{PUB}void {free_fn}(\n\t\tstruct {sname} *p)\n') + + f.write('{\n') + + if lc_members: + # Members must be dereferenced, so guard against NULL p. + f.write('\tif (!p)\n\t\treturn;\n') + for m in lc_members: + if m.is_char_ptr_array: + # free each element then the container + loop = (f'\tfor (size_t i = 0;' + f' p->{m.name} && p->{m.name}[i]; i++)') + free = f'\t\tfree(p->{m.name}[i]);' + if fits_80_ntabs(1, loop.lstrip()): + f.write(loop + '\n') + else: + f.write( + f'\tfor (size_t i = 0;\n' + f'\t p->{m.name} && p->{m.name}[i]; i++)\n' + ) + f.write(free + '\n') + f.write(f'\tfree(p->{m.name});\n') + else: + f.write(f'\tfree(p->{m.name});\n') + + # free(NULL) is safe — no NULL check needed when there are no members. + f.write('\tfree(p);\n}\n\n') + + # --------------------------------------------------------------------------- # Linker script (*.ld) emitter # --------------------------------------------------------------------------- -def generate_ld(f, prefix, struct_name, members): +def generate_ld(f, prefix, struct_name, members, lc_members, default_members): """Write linker version-script entries for all members of one struct.""" + if lc_members is not None: + f.write(f'\t\t{_new_name(prefix, struct_name)};\n') + f.write(f'\t\t{_free_name(prefix, struct_name)};\n') + if default_members: + f.write(f'\t\t{_init_defaults_name(prefix, struct_name)};\n') for member in members: if member.gen_getter: f.write(f'\t\t{_get_name(prefix, struct_name, member.name)};\n') @@ -752,7 +1162,7 @@ def main(): files_to_include.append(os.path.basename(in_hdr)) - for struct_name, members in structs: + for struct_name, members, lc_members, default_members in structs: forward_declares.append(struct_name) section_banner = ( @@ -764,16 +1174,29 @@ def main(): hdr_buf = io.StringIO() hdr_buf.write(section_banner) + if lc_members is not None: + emit_hdr_lifecycle(hdr_buf, args.prefix, struct_name, + lc_members) + if default_members: + emit_hdr_defaults(hdr_buf, args.prefix, struct_name, + default_members) generate_hdr(hdr_buf, args.prefix, struct_name, members) hdr_parts.append(hdr_buf.getvalue()) src_buf = io.StringIO() src_buf.write(section_banner) + if lc_members is not None: + emit_src_lifecycle(src_buf, args.prefix, struct_name, + lc_members, default_members) + if default_members: + emit_src_defaults(src_buf, args.prefix, struct_name, + default_members) generate_src(src_buf, args.prefix, struct_name, members) src_parts.append(src_buf.getvalue()) ld_buf = io.StringIO() - generate_ld(ld_buf, args.prefix, struct_name, members) + generate_ld(ld_buf, args.prefix, struct_name, members, + lc_members, default_members) ld_parts.append(ld_buf.getvalue()) # ----------------------------------------------------------------------- @@ -813,6 +1236,7 @@ def main(): f'{SPDX_C}\n' f'\n' f'{BANNER}\n' + f'#include \n' f'#include \n' f'#include \n' f'#include "{os.path.basename(args.h_fname)}"\n' diff --git a/libnvme/tools/generator/meson.build b/libnvme/tools/generator/meson.build index e282043f92..d6d02d43a0 100644 --- a/libnvme/tools/generator/meson.build +++ b/libnvme/tools/generator/meson.build @@ -10,7 +10,7 @@ # regenerated during a normal build. # # To regenerate them after changing private.h (e.g. adding or removing -# a /*!generate-accessors*/ annotation): +# a //!generate-accessors annotation): # # meson compile -C update-accessors diff --git a/libnvme/tools/generator/update-accessors.sh b/libnvme/tools/generator/update-accessors.sh index 692b65fad4..72cbe45f42 100755 --- a/libnvme/tools/generator/update-accessors.sh +++ b/libnvme/tools/generator/update-accessors.sh @@ -25,7 +25,7 @@ # $4 full path of the output .c file # $5 full path of the output .ld file # $6 ... one or more input headers scanned for -# /*!generate-accessors*/ structs +# //!generate-accessors structs set -euo pipefail