From 84dd0d98cb4bdf1ca92214f81442b7c004e9f041 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Wed, 29 Apr 2026 13:23:14 -0400 Subject: [PATCH 1/2] =?UTF-8?q?python=20bindings:=20redesign=20Phase=201?= =?UTF-8?q?=20=E2=80=94=20generator-emitted=20SWIG=20fragments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend generate-accessors.py to emit three new SWIG fragment files from the existing !generate-accessors annotations in private.h and private-fabrics.h: accessors.i — kernel-object structs (Ctrl, Host, Subsystem, Namespace, GlobalCtx) accessors-fabrics.i — fabrics-specific structs nvme-manual-bridges.i — residual hand-written %rename bridges A new struct-level annotation !generate-python[:alias=NAME] gates emission for Python and carries the PascalCase class alias. Remove the hand-written %rename(ctrl) / %rename(host) / ... lines from nvme.i; they are now generator-emitted. Restructure nvme.i so all %typemap directives precede the generated %include "accessors.i". SWIG freezes type-to-typemap associations when it first processes a struct body; struct bodies now arrive via the generated include so typemaps must come first. A generator-emitted _nvme_guarded_setattr is installed on each class at import to catch typos and writes to read-only (%immutable) properties. C-internal iteration helpers are renamed with a leading underscore via %rename to keep them out of the public API surface. Update nvme.i docstrings to Pythonic style (Args:/Returns: conventions, PascalCase class names, module-level usage examples). Update tests and examples to use the new API. Verified: meson test -C .build passes (64 OK, 2 expected failures). Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet 4.6 --- libnvme/examples/discover-loop.py | 13 +- libnvme/libnvme/accessors-fabrics.i | 19 + libnvme/libnvme/accessors.i | 217 +++ libnvme/libnvme/meson.build | 5 + libnvme/libnvme/nvme-manual-bridges.i | 24 + libnvme/libnvme/nvme.i | 1495 ++++++++--------- libnvme/libnvme/tests/create-ctrl-obj.py | 4 +- libnvme/libnvme/tests/gc.py | 6 +- libnvme/libnvme/tests/test-nbft.py | 2 +- libnvme/libnvme/tests/test-objects.py | 48 +- libnvme/libnvme/tests/test-setattr.py | 12 +- libnvme/src/accessors.ld | 1 - libnvme/src/nvme/accessors.c | 6 - libnvme/src/nvme/accessors.h | 7 - libnvme/src/nvme/private.h | 82 +- libnvme/tools/generator/generate-accessors.md | 625 +++---- libnvme/tools/generator/generate-accessors.py | 430 ++++- libnvme/tools/generator/meson.build | 15 +- libnvme/tools/generator/update-accessors.sh | 28 +- 19 files changed, 1683 insertions(+), 1356 deletions(-) create mode 100644 libnvme/libnvme/accessors-fabrics.i create mode 100644 libnvme/libnvme/accessors.i create mode 100644 libnvme/libnvme/nvme-manual-bridges.i diff --git a/libnvme/examples/discover-loop.py b/libnvme/examples/discover-loop.py index 8c285b7e71..b3433a72f8 100644 --- a/libnvme/examples/discover-loop.py +++ b/libnvme/examples/discover-loop.py @@ -16,7 +16,7 @@ def disc_supp_str(dlp_supp_opts): } return [txt for msk, txt in d.items() if dlp_supp_opts & msk] -def discover(host, ctrl, iteration): +def discover(ctx, host, ctrl, iteration): # Only 8 levels of indirection are supported if iteration > 8: return @@ -51,19 +51,18 @@ def discover(host, ctrl, iteration): if dlpe['subtype'] == 'discovery' and dlpe['subnqn'] == nvme.NVME_DISC_SUBSYS_NAME: continue print(f'{iteration}: {dlpe["subtype"]} {dlpe["subnqn"]}') - with nvme.ctrl(root, subsysnqn=dlpe['subnqn'], transport=dlpe['trtype'], traddr=dlpe['traddr'], trsvcid=dlpe['trsvcid']) as new_ctrl: + with nvme.Ctrl(ctx, subsysnqn=dlpe['subnqn'], transport=dlpe['trtype'], traddr=dlpe['traddr'], trsvcid=dlpe['trsvcid']) as new_ctrl: discover(host, new_ctrl, iteration + 1) -root = nvme.root() -host = nvme.host(root) - +ctx = nvme.GlobalCtx() +host = nvme.Host(ctx) subsysnqn = nvme.NVME_DISC_SUBSYS_NAME transport = 'tcp' traddr = '127.0.0.1' trsvcid = '4420' -with nvme.ctrl(root, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid) as ctrl: - discover(host, ctrl, 0) +with nvme.Ctrl(ctx, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid) as ctrl: + discover(ctx, host, ctrl, 0) for s in host.subsystems(): for c in s.controllers(): diff --git a/libnvme/libnvme/accessors-fabrics.i b/libnvme/libnvme/accessors-fabrics.i new file mode 100644 index 0000000000..e791e60b8e --- /dev/null +++ b/libnvme/libnvme/accessors-fabrics.i @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/* + * This file is part of libnvme. + * + * Copyright (c) 2025, Dell Technologies Inc. or its subsidiaries. + * Authors: Martin Belanger + * + * ____ _ _ ____ _ + * / ___| ___ _ __ ___ _ __ __ _| |_ ___ __| | / ___|___ __| | ___ + * | | _ / _ \ '_ \ / _ \ '__/ _` | __/ _ \/ _` | | | / _ \ / _` |/ _ \ + * | |_| | __/ | | | __/ | | (_| | || __/ (_| | | |__| (_) | (_| | __/ + * \____|\___|_| |_|\___|_| \__,_|\__\___|\__,_| \____\___/ \__,_|\___| + * + * Auto-generated struct member accessors (setter/getter) + * + * To update run: meson compile -C [BUILD-DIR] update-accessors + * Or: make update-accessors + */ diff --git a/libnvme/libnvme/accessors.i b/libnvme/libnvme/accessors.i new file mode 100644 index 0000000000..6207906f5f --- /dev/null +++ b/libnvme/libnvme/accessors.i @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/* + * This file is part of libnvme. + * + * Copyright (c) 2025, Dell Technologies Inc. or its subsidiaries. + * Authors: Martin Belanger + * + * ____ _ _ ____ _ + * / ___| ___ _ __ ___ _ __ __ _| |_ ___ __| | / ___|___ __| | ___ + * | | _ / _ \ '_ \ / _ \ '__/ _` | __/ _ \/ _` | | | / _ \ / _` |/ _ \ + * | |_| | __/ | | | __/ | | (_| | || __/ (_| | | |__| (_) | (_| | __/ + * \____|\___|_| |_|\___|_| \__,_|\__\___|\__,_| \____\___/ \__,_|\___| + * + * Auto-generated struct member accessors (setter/getter) + * + * To update run: meson compile -C [BUILD-DIR] update-accessors + * Or: make update-accessors + */ +%pythoncode %{ +def _nvme_guarded_setattr(self, name, value): + """Reject writes to unknown attributes. + + Typos like ``ctrl.nqn = x`` (should be ``ctrl.subsysnqn``) are + silently ignored by default Python ``__setattr__``. This guard + raises ``AttributeError`` for any name not already present on the + object, keeping the struct-like API strict. + """ + if name.startswith('_') or name in ('this', 'thisown') or hasattr(type(self), name): + object.__setattr__(self, name, value) + else: + raise AttributeError( + f"{type(self).__name__!r} has no attribute {name!r}") +%} + +/* struct libnvme_ns */ +%rename(Namespace) libnvme_ns; +%rename(libnvme_ns_command_retry_count_get) libnvme_ns_get_command_retry_count; +%rename(libnvme_ns_command_error_count_get) libnvme_ns_get_command_error_count; +%rename(libnvme_ns_requeue_no_usable_path_count_get) libnvme_ns_get_requeue_no_usable_path_count; +%rename(libnvme_ns_fail_no_available_path_count_get) libnvme_ns_get_fail_no_available_path_count; +%{ + #define libnvme_ns_command_retry_count_get libnvme_ns_get_command_retry_count + #define libnvme_ns_command_error_count_get libnvme_ns_get_command_error_count + #define libnvme_ns_requeue_no_usable_path_count_get libnvme_ns_get_requeue_no_usable_path_count + #define libnvme_ns_fail_no_available_path_count_get libnvme_ns_get_fail_no_available_path_count +%} +struct libnvme_ns { + __u32 nsid; + %immutable name; + const char * name; + %immutable generic_name; + const char * generic_name; + const char * sysfs_dir; + int lba_shift; + int lba_size; + int meta_size; + uint64_t lba_count; + uint64_t lba_util; + %immutable eui64; + uint8_t eui64[8]; + %immutable nguid; + uint8_t nguid[16]; + %immutable csi; + enum nvme_csi csi; + %extend { + %immutable command_retry_count; + long command_retry_count; + %immutable command_error_count; + long command_error_count; + %immutable requeue_no_usable_path_count; + long requeue_no_usable_path_count; + %immutable fail_no_available_path_count; + long fail_no_available_path_count; + } +}; + +%pythoncode %{ +Namespace.__setattr__ = _nvme_guarded_setattr +%} + +/* struct libnvme_ctrl */ +%rename(Ctrl) libnvme_ctrl; +%rename(libnvme_ctrl_state_get) libnvme_ctrl_get_state; +%rename(libnvme_ctrl_command_error_count_get) libnvme_ctrl_get_command_error_count; +%rename(libnvme_ctrl_reset_count_get) libnvme_ctrl_get_reset_count; +%rename(libnvme_ctrl_reconnect_count_get) libnvme_ctrl_get_reconnect_count; +%{ + #define libnvme_ctrl_state_get libnvme_ctrl_get_state + #define libnvme_ctrl_command_error_count_get libnvme_ctrl_get_command_error_count + #define libnvme_ctrl_reset_count_get libnvme_ctrl_get_reset_count + #define libnvme_ctrl_reconnect_count_get libnvme_ctrl_get_reconnect_count +%} +struct libnvme_ctrl { + %immutable name; + const char * name; + %immutable sysfs_dir; + const char * sysfs_dir; + %immutable address; + const char * address; + %immutable firmware; + const char * firmware; + %immutable model; + const char * model; + %immutable numa_node; + const char * numa_node; + %immutable queue_count; + const char * queue_count; + %immutable serial; + const char * serial; + %immutable sqsize; + const char * sqsize; + %immutable transport; + const char * transport; + %immutable subsysnqn; + const char * subsysnqn; + %immutable traddr; + const char * traddr; + %immutable trsvcid; + const char * trsvcid; + const char * dhchap_host_key; + const char * dhchap_ctrl_key; + const char * keyring; + const char * tls_key_identity; + const char * tls_key; + %immutable cntrltype; + const char * cntrltype; + %immutable cntlid; + const char * cntlid; + %immutable dctype; + const char * dctype; + %immutable phy_slot; + const char * phy_slot; + %immutable host_traddr; + const char * host_traddr; + %immutable host_iface; + const char * host_iface; + bool discovery_ctrl; + bool unique_discovery_ctrl; + bool discovered; + bool persistent; + %extend { + %immutable state; + const char * state; + %immutable command_error_count; + long command_error_count; + %immutable reset_count; + long reset_count; + %immutable reconnect_count; + long reconnect_count; + } +}; + +%pythoncode %{ +Ctrl.__setattr__ = _nvme_guarded_setattr +%} + +/* struct libnvme_subsystem */ +%rename(Subsystem) libnvme_subsystem; +%rename(libnvme_subsystem_iopolicy_get) libnvme_subsystem_get_iopolicy; +%{ + #define libnvme_subsystem_iopolicy_get libnvme_subsystem_get_iopolicy +%} +struct libnvme_subsystem { + %immutable name; + const char * name; + %immutable sysfs_dir; + const char * sysfs_dir; + %immutable subsysnqn; + const char * subsysnqn; + %immutable model; + const char * model; + %immutable serial; + const char * serial; + %immutable firmware; + const char * firmware; + %immutable subsystype; + const char * subsystype; + const char * application; + %extend { + %immutable iopolicy; + const char * iopolicy; + } +}; + +%pythoncode %{ +Subsystem.__setattr__ = _nvme_guarded_setattr +%} + +/* struct libnvme_host */ +%rename(Host) libnvme_host; +%rename(libnvme_host_pdc_enabled_set) libnvme_host_set_pdc_enabled; +%{ + #define libnvme_host_pdc_enabled_set libnvme_host_set_pdc_enabled +%} +struct libnvme_host { + %immutable hostnqn; + const char * hostnqn; + %immutable hostid; + const char * hostid; + const char * dhchap_host_key; + const char * hostsymname; +}; + +%pythoncode %{ +Host.__setattr__ = _nvme_guarded_setattr +%} + +/* struct libnvme_global_ctx */ +%rename(GlobalCtx) libnvme_global_ctx; +struct libnvme_global_ctx { +}; + +%pythoncode %{ +GlobalCtx.__setattr__ = _nvme_guarded_setattr +%} + diff --git a/libnvme/libnvme/meson.build b/libnvme/libnvme/meson.build index ac5eda2b43..222ffe2ed6 100644 --- a/libnvme/libnvme/meson.build +++ b/libnvme/libnvme/meson.build @@ -23,6 +23,11 @@ if want_python input: ['nvme.i'], output: ['nvme.py', 'nvme_wrap.c'], command: swig_cmd, + depend_files: [ + 'accessors.i', + 'accessors-fabrics.i', + 'nvme-manual-bridges.i', + ], install: true, install_dir: [python3.get_install_dir(pure: false, subdir: 'libnvme'), false], ) diff --git a/libnvme/libnvme/nvme-manual-bridges.i b/libnvme/libnvme/nvme-manual-bridges.i new file mode 100644 index 0000000000..0b44b47d70 --- /dev/null +++ b/libnvme/libnvme/nvme-manual-bridges.i @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/* + * This file is part of libnvme. + * + * Copyright (c) 2025, Dell Technologies Inc. or its subsidiaries. + * Authors: Martin Belanger + * + * Hand-maintained SWIG accessor bridges. + * + * These members expose nested struct pointers and cannot be expressed + * as generated accessors — they must be kept here, manually. + */ + +/* ctrl.subsystem: exposes the parent libnvme_subsystem pointer */ +%rename(libnvme_ctrl_subsystem_get) libnvme_ctrl_get_subsystem; + +/* subsystem.host: exposes the parent libnvme_host pointer */ +%rename(libnvme_subsystem_host_get) libnvme_subsystem_get_host; + +%{ + #define libnvme_ctrl_subsystem_get libnvme_ctrl_get_subsystem + #define libnvme_subsystem_host_get libnvme_subsystem_get_host +%} diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index a0c9a284bc..22ad1a0496 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -1,9 +1,12 @@ // SPDX-License-Identifier: LGPL-2.1-or-later /* * This file is part of libnvme. - * Copyright (c) 2021 SUSE Software Solutions * + * Copyright (c) 2021 SUSE Software Solutions * Authors: Hannes Reinecke + * + * Copyright (c) 2026, Dell Technologies Inc. or its subsidiaries. + * Authors: Martin Belanger */ %begin %{ /* WORKAROUND: The top-level meson.build defines the macro "fallthrough", which @@ -26,277 +29,505 @@ static inline PyObject *Py_NewRef(PyObject *obj) #endif %} -%module(docstring="Python bindings for libnvme") nvme +%define MODULE_DOCSTRING +"Python bindings for libnvme — the Linux NVMe management library.\n" +"\n" +"Classes\n" +"-------\n" +"GlobalCtx Root context; owns the device tree and configuration.\n" +"Host Host (initiator) identity — NQN, host ID, credentials.\n" +"Subsystem An NVMe subsystem visible to a host.\n" +"Ctrl An NVMe or NVMe-oF controller; connect, discover, disconnect.\n" +"Namespace A namespace within a subsystem or controller.\n" +"\n" +"Scan attached NVMe devices::\n" +"\n" +" import nvme\n" +" ctx = nvme.GlobalCtx()\n" +" for host in ctx.hosts():\n" +" for sub in host.subsystems():\n" +" for ctrl in sub.controllers():\n" +" print(ctrl.name, ctrl.transport)\n" +"\n" +"Discover NVMe-oF controllers at a remote target::\n" +"\n" +" with nvme.Ctrl(ctx, {\n" +" 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME,\n" +" 'transport': 'tcp',\n" +" 'traddr': '192.168.1.100',\n" +" 'trsvcid': '8009',\n" +" }) as c:\n" +" c.connect(host)\n" +" log = c.discover()\n" +"\n" +"All classes support the context manager protocol (the ``with`` statement).\n" +"read_hostnqn() and read_hostid() return the system-wide host NQN and ID.\n" +%enddef +%module(docstring=MODULE_DOCSTRING) nvme %feature("autodoc", "1"); %include "exception.i" -%include "nvme-swig-accessors.i" %allowexception; -%rename(global_ctx) libnvme_global_ctx; -%rename(host) libnvme_host; -%rename(ctrl) libnvme_ctrl; -%rename(subsystem) libnvme_subsystem; -%rename(ns) libnvme_ns; +PyObject *read_hostnqn(); +PyObject *read_hostid(); +/******************************************************************************* + * This is the single C implementation block. All pure C code — headers, + * #defines, static helpers, and callback implementations — belongs here. + * Do not introduce additional %{...%} blocks elsewhere in this file. + * SWIG-specific directives (%typemap, %extend, %exception, %include) + * follow this block. + ******************************************************************************/ %{ - #include - #include - #include - #include "nvme/private.h" - #include "nvme/private-fabrics.h" - - /* - * These bridges cannot be auto-generated by generate-swig-accessors.py - * and added to "nvme-swig-accessors.i" included above. These are used - * to bridge between SWIG's API naming convention - * "struct_member_[set|get]" and libnvme's API naming convention - * "struct_[set|get]_member. - */ - #define libnvme_ctrl_address_get libnvme_ctrl_get_address - #define libnvme_ctrl_command_error_count_get libnvme_ctrl_get_command_error_count - #define libnvme_ctrl_reconnect_count_get libnvme_ctrl_get_reconnect_count - #define libnvme_ctrl_reset_count_get libnvme_ctrl_get_reset_count - #define libnvme_ctrl_state_get libnvme_ctrl_get_state - #define libnvme_ctrl_subsystem_get libnvme_ctrl_get_subsystem - #define libnvme_ns_command_error_count_get libnvme_ns_get_command_error_count - #define libnvme_ns_command_retry_count_get libnvme_ns_get_command_retry_count - #define libnvme_ns_fail_no_available_path_count_get libnvme_ns_get_fail_no_available_path_count - #define libnvme_ns_inflights_get libnvme_ns_get_inflights - #define libnvme_ns_io_ticks_get libnvme_ns_get_io_ticks - #define libnvme_ns_read_ios_get libnvme_ns_get_read_ios - #define libnvme_ns_read_sectors_get libnvme_ns_get_read_sectors - #define libnvme_ns_read_ticks_get libnvme_ns_get_read_ticks - #define libnvme_ns_requeue_no_usable_path_count_get libnvme_ns_get_requeue_no_usable_path_count - #define libnvme_ns_stat_interval_get libnvme_ns_get_stat_interval - #define libnvme_ns_write_ios_get libnvme_ns_get_write_ios - #define libnvme_ns_write_sectors_get libnvme_ns_get_write_sectors - #define libnvme_ns_write_ticks_get libnvme_ns_get_write_ticks - #define libnvme_path_ana_state_get libnvme_path_get_ana_stat - #define libnvme_path_command_error_count_get libnvme_path_get_command_error_count - #define libnvme_path_command_retry_count_get libnvme_path_get_command_retry_count - #define libnvme_path_inflights_get libnvme_path_get_inflights - #define libnvme_path_io_ticks_get libnvme_path_get_io_ticks - #define libnvme_path_multipath_failover_count_get libnvme_path_get_multipath_failover_count - #define libnvme_path_numa_nodes_get libnvme_path_get_numa_nodes - #define libnvme_path_read_ios_get libnvme_path_get_read_ios - #define libnvme_path_read_sectors_get libnvme_path_get_read_sectors - #define libnvme_path_read_ticks_get libnvme_path_get_read_ticks - #define libnvme_path_stat_interval_get libnvme_path_get_stat_interval - #define libnvme_path_write_ios_get libnvme_path_get_write_ios - #define libnvme_path_write_sectors_get libnvme_path_get_write_sectors - #define libnvme_path_write_ticks_get libnvme_path_get_write_ticks - #define libnvme_subsystem_host_get libnvme_subsystem_get_host - #define libnvme_subsystem_iopolicy_get libnvme_subsystem_get_iopolicy - - static int connect_err = 0; - static int discover_err = 0; - - static void PyDict_SetItemStringDecRef(PyObject * p, const char *key, PyObject *val) { - PyDict_SetItemString(p, key, val); /* Does NOT steal reference to val .. */ - Py_XDECREF(val); /* .. therefore decrement ref. count. */ - } - PyObject *read_hostnqn() { - char * val = libnvme_read_hostnqn(); - PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None); - free(val); - return obj; +#include +#include +#include +#include "nvme/private.h" +#include "nvme/private-fabrics.h" + +#define STR_OR_NONE(str) (!(str) ? "None" : str) + +static int connect_err = 0; +static int discover_err = 0; + +static void PyDict_SetItemStringDecRef(PyObject * p, const char *key, PyObject *val) { + PyDict_SetItemString(p, key, val); /* Does NOT steal reference to val .. */ + Py_XDECREF(val); /* .. therefore decrement ref. count. */ +} +PyObject *read_hostnqn() { + char * val = libnvme_read_hostnqn(); + PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None); + free(val); + return obj; +} +PyObject *read_hostid() { + char * val = libnvme_read_hostid(); + PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None); + free(val); + return obj; +} + +static const char *dict_get_str(PyObject *dict, const char *key) +{ + PyObject *val = PyDict_GetItemString(dict, key); + + if (!val || val == Py_None) + return NULL; + return PyUnicode_AsUTF8(val); +} + +static int set_fctx_from_dict(struct libnvmf_context *fctx, PyObject *dict) +{ + struct libnvme_fabrics_config *cfg; + const char *subsysnqn, *transport; + const char *hostnqn = NULL, *hostid = NULL; + const char *hostkey = NULL, *ctrlkey = NULL; + const char *keyring = NULL, *tls_key = NULL, *tls_key_identity = NULL; + bool persistent = false; + bool has_persistent = false; + Py_ssize_t pos = 0; + PyObject *key, *value; + + subsysnqn = dict_get_str(dict, "subsysnqn"); + transport = dict_get_str(dict, "transport"); + + if (!subsysnqn || !transport) { + PyErr_SetString(PyExc_KeyError, + "'subsysnqn' and 'transport' are required"); + return -1; } - PyObject *read_hostid() { - char * val = libnvme_read_hostid(); - PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None); - free(val); - return obj; + + libnvmf_context_set_connection(fctx, subsysnqn, transport, + dict_get_str(dict, "traddr"), + dict_get_str(dict, "trsvcid"), + dict_get_str(dict, "host_traddr"), + dict_get_str(dict, "host_iface")); + + cfg = libnvmf_context_get_fabrics_config(fctx); + + while (PyDict_Next(dict, &pos, &key, &value)) { + /* Already consumed above via dict_get_str() */ + if (!PyUnicode_CompareWithASCIIString(key, "subsysnqn") || + !PyUnicode_CompareWithASCIIString(key, "transport") || + !PyUnicode_CompareWithASCIIString(key, "traddr") || + !PyUnicode_CompareWithASCIIString(key, "trsvcid") || + !PyUnicode_CompareWithASCIIString(key, "host_traddr") || + !PyUnicode_CompareWithASCIIString(key, "host_iface")) + continue; + if (!PyUnicode_CompareWithASCIIString(key, "queue_size")) { + cfg->queue_size = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { + cfg->nr_io_queues = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { + cfg->reconnect_delay = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { + cfg->ctrl_loss_tmo = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "fast_io_fail_tmo")) { + cfg->fast_io_fail_tmo = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { + cfg->keep_alive_tmo = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { + cfg->nr_write_queues = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { + cfg->nr_poll_queues = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tos")) { + cfg->tos = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring_id")) { + cfg->keyring_id = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_id")) { + cfg->tls_key_id = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_configured_key_id")) { + cfg->tls_configured_key_id = PyLong_AsLong(value); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { + cfg->duplicate_connect = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { + cfg->disable_sqflow = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { + cfg->hdr_digest = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { + cfg->data_digest = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls")) { + cfg->tls = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "concat")) { + cfg->concat = PyObject_IsTrue(value) ? true : false; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostnqn")) { + hostnqn = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostid")) { + hostid = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostkey")) { + hostkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrlkey")) { + ctrlkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring")) { + keyring = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key")) { + tls_key = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_identity")) { + tls_key_identity = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "persistent")) { + persistent = PyObject_IsTrue(value) ? true : false; + has_persistent = true; + continue; + } + PyErr_Format(PyExc_KeyError, "unknown ctrl config key: '%U'", key); + return -1; } - static const char *dict_get_str(PyObject *dict, const char *key) + if (hostnqn || hostid) + libnvmf_context_set_hostnqn(fctx, hostnqn, hostid); + if (hostkey || ctrlkey || keyring || tls_key || tls_key_identity) + libnvmf_context_set_crypto(fctx, hostkey, ctrlkey, + keyring, tls_key, + tls_key_identity); + if (has_persistent) + libnvmf_context_set_persistent(fctx, persistent); + + return 0; +} + +/****** +NBFT +******/ +static PyObject *ssns_to_dict(struct libnbft_subsystem_ns *ss) +{ + unsigned int i; + PyObject *output = PyDict_New(); + PyObject *hfis = PyList_New(ss->num_hfis); + + for (i = 0; i < ss->num_hfis; i++) + PyList_SetItem(hfis, i, PyLong_FromLong(ss->hfis[i]->index - 1)); /* steals ref. to object - no need to decref */ + + PyDict_SetItemStringDecRef(output, "hfi_indexes", hfis); + + PyDict_SetItemStringDecRef(output, "trtype", PyUnicode_FromString(ss->transport)); + PyDict_SetItemStringDecRef(output, "traddr", PyUnicode_FromString(ss->traddr)); + PyDict_SetItemStringDecRef(output, "trsvcid", PyUnicode_FromString(ss->trsvcid)); + PyDict_SetItemStringDecRef(output, "subsys_port_id", PyLong_FromLong(ss->subsys_port_id)); + PyDict_SetItemStringDecRef(output, "nsid", PyLong_FromLong(ss->nsid)); + { - PyObject *val = PyDict_GetItemString(dict, key); + PyObject *nid; + switch (ss->nid_type) { + case LIBNBFT_NID_TYPE_EUI64: + PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("eui64")); + nid = PyUnicode_FromFormat("%02x%02x%02x%02x%02x%02x%02x%02x", + ss->nid[0], ss->nid[1], ss->nid[2], ss->nid[3], + ss->nid[4], ss->nid[5], ss->nid[6], ss->nid[7]); + break; - if (!val || val == Py_None) - return NULL; - return PyUnicode_AsUTF8(val); + case LIBNBFT_NID_TYPE_NGUID: + PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("nguid")); + nid = PyUnicode_FromFormat("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + ss->nid[0], ss->nid[1], ss->nid[2], ss->nid[3], + ss->nid[4], ss->nid[5], ss->nid[6], ss->nid[7], + ss->nid[8], ss->nid[9], ss->nid[10], ss->nid[11], + ss->nid[12], ss->nid[13], ss->nid[14], ss->nid[15]); + break; + + case LIBNBFT_NID_TYPE_NS_UUID: + { + char uuid_str[NVME_UUID_LEN_STRING]; + PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("uuid")); + libnvme_uuid_to_string(ss->nid, uuid_str); + nid = PyUnicode_FromString(uuid_str); + break; + } + + default: + nid = NULL; + break; + } + if (nid) + PyDict_SetItemStringDecRef(output, "nid", nid); + } + + if (ss->subsys_nqn) + PyDict_SetItemStringDecRef(output, "subsys_nqn", PyUnicode_FromString(ss->subsys_nqn)); + + PyDict_SetItemStringDecRef(output, "controller_id", PyLong_FromLong(ss->controller_id)); + PyDict_SetItemStringDecRef(output, "asqsz", PyLong_FromLong(ss->asqsz)); + + if (ss->dhcp_root_path_string) + PyDict_SetItemStringDecRef(output, "dhcp_root_path_string", PyUnicode_FromString(ss->dhcp_root_path_string)); + + PyDict_SetItemStringDecRef(output, "pdu_header_digest_required", PyBool_FromLong(ss->pdu_header_digest_required)); + PyDict_SetItemStringDecRef(output, "data_digest_required", PyBool_FromLong(ss->data_digest_required)); + + return output; +} + +static PyObject *hfi_to_dict(struct libnbft_hfi *hfi) +{ + PyObject *output = PyDict_New(); + + PyDict_SetItemStringDecRef(output, "trtype", PyUnicode_FromString(hfi->transport)); + + if (!strcmp(hfi->transport, "tcp")) { + PyDict_SetItemStringDecRef(output, "pcidev", + PyUnicode_FromFormat("%x:%x:%x.%x", + ((hfi->tcp_info.pci_sbdf & 0xffff0000) >> 16), + ((hfi->tcp_info.pci_sbdf & 0x0000ff00) >> 8), + ((hfi->tcp_info.pci_sbdf & 0x000000f8) >> 3), + ((hfi->tcp_info.pci_sbdf & 0x00000007) >> 0))); + + PyDict_SetItemStringDecRef(output, "mac_addr", + PyUnicode_FromFormat("%02x:%02x:%02x:%02x:%02x:%02x", + hfi->tcp_info.mac_addr[0], + hfi->tcp_info.mac_addr[1], + hfi->tcp_info.mac_addr[2], + hfi->tcp_info.mac_addr[3], + hfi->tcp_info.mac_addr[4], + hfi->tcp_info.mac_addr[5])); + + PyDict_SetItemStringDecRef(output, "vlan", PyLong_FromLong(hfi->tcp_info.vlan)); + PyDict_SetItemStringDecRef(output, "ip_origin", PyLong_FromLong(hfi->tcp_info.ip_origin)); + PyDict_SetItemStringDecRef(output, "ipaddr", PyUnicode_FromString(hfi->tcp_info.ipaddr)); + PyDict_SetItemStringDecRef(output, "subnet_mask_prefix", PyLong_FromLong(hfi->tcp_info.subnet_mask_prefix)); + PyDict_SetItemStringDecRef(output, "gateway_ipaddr", PyUnicode_FromString(hfi->tcp_info.gateway_ipaddr)); + PyDict_SetItemStringDecRef(output, "route_metric", PyLong_FromLong(hfi->tcp_info.route_metric)); + PyDict_SetItemStringDecRef(output, "primary_dns_ipaddr", PyUnicode_FromString(hfi->tcp_info.primary_dns_ipaddr)); + PyDict_SetItemStringDecRef(output, "secondary_dns_ipaddr", PyUnicode_FromString(hfi->tcp_info.secondary_dns_ipaddr)); + PyDict_SetItemStringDecRef(output, "dhcp_server_ipaddr", PyUnicode_FromString(hfi->tcp_info.dhcp_server_ipaddr)); + + if (hfi->tcp_info.host_name) + PyDict_SetItemStringDecRef(output, "host_name", PyUnicode_FromString(hfi->tcp_info.host_name)); + + PyDict_SetItemStringDecRef(output, "this_hfi_is_default_route", PyBool_FromLong(hfi->tcp_info.this_hfi_is_default_route)); + PyDict_SetItemStringDecRef(output, "dhcp_override", PyBool_FromLong(hfi->tcp_info.dhcp_override)); } - static int set_fctx_from_dict(struct libnvmf_context *fctx, PyObject *dict) + return output; +} + +static PyObject *discovery_to_dict(struct libnbft_discovery *disc) +{ + PyObject *output = PyDict_New(); + + if (disc->security) + PyDict_SetItemStringDecRef(output, "security_index", PyLong_FromLong(disc->security->index)); + if (disc->hfi) + PyDict_SetItemStringDecRef(output, "hfi_index", PyLong_FromLong(disc->hfi->index - 1)); + if (disc->uri) + PyDict_SetItemStringDecRef(output, "uri", PyUnicode_FromString(disc->uri)); + if (disc->nqn) + PyDict_SetItemStringDecRef(output, "nqn", PyUnicode_FromString(disc->nqn)); + + return output; +} + +static PyObject *nbft_to_pydict(struct libnbft_info *nbft) +{ + PyObject *val; + PyObject *output = PyDict_New(); + { - struct libnvme_fabrics_config *cfg; - const char *subsysnqn, *transport; - const char *hostnqn = NULL, *hostid = NULL; - const char *hostkey = NULL, *ctrlkey = NULL; - const char *keyring = NULL, *tls_key = NULL, *tls_key_identity = NULL; - bool persistent = false; - bool has_persistent = false; - Py_ssize_t pos = 0; - PyObject *key, *value; - - subsysnqn = dict_get_str(dict, "subsysnqn"); - transport = dict_get_str(dict, "transport"); - - if (!subsysnqn || !transport) { - PyErr_SetString(PyExc_KeyError, - "'subsysnqn' and 'transport' are required"); - return -1; + PyObject *host = PyDict_New(); + + if (nbft->host.nqn) + PyDict_SetItemStringDecRef(host, "nqn", PyUnicode_FromString(nbft->host.nqn)); + if (nbft->host.id) { + char uuid_str[NVME_UUID_LEN_STRING]; + libnvme_uuid_to_string((unsigned char *)nbft->host.id, uuid_str); + PyDict_SetItemStringDecRef(host, "id", PyUnicode_FromString(uuid_str)); } - libnvmf_context_set_connection(fctx, subsysnqn, transport, - dict_get_str(dict, "traddr"), - dict_get_str(dict, "trsvcid"), - dict_get_str(dict, "host_traddr"), - dict_get_str(dict, "host_iface")); - - cfg = libnvmf_context_get_fabrics_config(fctx); - - while (PyDict_Next(dict, &pos, &key, &value)) { - /* Already consumed above via dict_get_str() */ - if (!PyUnicode_CompareWithASCIIString(key, "subsysnqn") || - !PyUnicode_CompareWithASCIIString(key, "transport") || - !PyUnicode_CompareWithASCIIString(key, "traddr") || - !PyUnicode_CompareWithASCIIString(key, "trsvcid") || - !PyUnicode_CompareWithASCIIString(key, "host_traddr") || - !PyUnicode_CompareWithASCIIString(key, "host_iface")) - continue; - if (!PyUnicode_CompareWithASCIIString(key, "queue_size")) { - libnvme_fabrics_config_set_queue_size(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { - libnvme_fabrics_config_set_nr_io_queues(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { - libnvme_fabrics_config_set_reconnect_delay(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { - libnvme_fabrics_config_set_ctrl_loss_tmo(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "fast_io_fail_tmo")) { - libnvme_fabrics_config_set_fast_io_fail_tmo(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { - libnvme_fabrics_config_set_keep_alive_tmo(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { - libnvme_fabrics_config_set_nr_write_queues(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { - libnvme_fabrics_config_set_nr_poll_queues(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tos")) { - libnvme_fabrics_config_set_tos(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "keyring_id")) { - libnvme_fabrics_config_set_keyring_id(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tls_key_id")) { - libnvme_fabrics_config_set_tls_key_id(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tls_configured_key_id")) { - libnvme_fabrics_config_set_tls_configured_key_id(cfg, PyLong_AsLong(value)); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { - libnvme_fabrics_config_set_duplicate_connect(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { - libnvme_fabrics_config_set_disable_sqflow(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { - libnvme_fabrics_config_set_hdr_digest(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { - libnvme_fabrics_config_set_data_digest(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tls")) { - libnvme_fabrics_config_set_tls(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "concat")) { - libnvme_fabrics_config_set_concat(cfg, - PyObject_IsTrue(value) ? true : false); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hostnqn")) { - hostnqn = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hostid")) { - hostid = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hostkey")) { - hostkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "ctrlkey")) { - ctrlkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "keyring")) { - keyring = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tls_key")) { - tls_key = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tls_key_identity")) { - tls_key_identity = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "persistent")) { - persistent = PyObject_IsTrue(value) ? true : false; - has_persistent = true; - continue; - } - PyErr_Format(PyExc_KeyError, "unknown ctrl config key: '%U'", key); - return -1; - } + PyDict_SetItemStringDecRef(host, "host_id_configured", PyBool_FromLong(nbft->host.host_id_configured)); + PyDict_SetItemStringDecRef(host, "host_nqn_configured", PyBool_FromLong(nbft->host.host_nqn_configured)); - if (hostnqn || hostid) - libnvmf_context_set_hostnqn(fctx, hostnqn, hostid); - if (hostkey || ctrlkey || keyring || tls_key || tls_key_identity) - libnvmf_context_set_crypto(fctx, hostkey, ctrlkey, - keyring, tls_key, - tls_key_identity); - if (has_persistent) - libnvmf_context_set_persistent(fctx, persistent); + val = PyUnicode_FromString(nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_NOT_INDICATED ? "not indicated" : + nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_UNSELECTED ? "unselected" : + nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_SELECTED ? "selected" : "reserved"); + PyDict_SetItemStringDecRef(host, "primary_admin_host_flag", val); - return 0; + PyDict_SetItemStringDecRef(output, "host", host); } -%} -PyObject *read_hostnqn(); -PyObject *read_hostid(); -/* libnvmf_context is declared as an opaque forward declaration in the public - * header fabrics.h. SWIG treats any type it knows only as an incomplete type - * (no struct body) as an opaque pointer, which causes SWIG's own built-in - * opaque-pointer typemap to take precedence over any user-defined %typemap. - * Providing an empty struct body here gives SWIG a complete type, which - * restores normal typemap precedence rules. The actual fields are in - * private-fabrics.h, which is included in the %{%} block above and is - * therefore visible to all generated C code — just not to SWIG's parser. + { + size_t ss_num = 0; + struct libnbft_subsystem_ns **ss; + PyObject *subsystem; + + /* First, let's find how many entries there are */ + for (ss = nbft->subsystem_ns_list; ss && *ss; ss++) + ss_num++; + + /* Now, let's fill the list using "(*ss)->index - 1" + as the index for writing to the list */ + subsystem = PyList_New(ss_num); + for (ss = nbft->subsystem_ns_list; ss && *ss; ss++) + PyList_SetItem(subsystem, (*ss)->index - 1, ssns_to_dict(*ss)); /* steals ref. to object - no need to decref */ + + PyDict_SetItemStringDecRef(output, "subsystem", subsystem); + } + + { + size_t hfi_num = 0; + struct libnbft_hfi **hfi; + PyObject *hfis; + + /* First, let's find how many entries there are */ + for (hfi = nbft->hfi_list; hfi && *hfi; hfi++) + hfi_num++; + + /* Now, let's fill the list using "(*hfi)->index - 1" + as the index for writing to the list */ + hfis = PyList_New(hfi_num); + for (hfi = nbft->hfi_list; hfi && *hfi; hfi++) + PyList_SetItem(hfis, (*hfi)->index-1, hfi_to_dict(*hfi)); /* steals ref. to object - no need to decref */ + + PyDict_SetItemStringDecRef(output, "hfi", hfis); + } + + { + size_t disc_num = 0; + struct libnbft_discovery **disc; + PyObject *discovery; + + /* First, let's find how many entries there are */ + for (disc = nbft->discovery_list; disc && *disc; disc++) + disc_num++; + + /* Now, let's fill the list using "(*disc)->index - 1" + as the index for writing to the list */ + discovery = PyList_New(disc_num); + for (disc = nbft->discovery_list; disc && *disc; disc++) + PyList_SetItem(discovery, (*disc)->index - 1, discovery_to_dict(*disc)); /* steals ref. to object - no need to decref */ + + PyDict_SetItemStringDecRef(output, "discovery", discovery); + } + + /* Security profiles are currently not implemented. */ + + return output; +} + +PyObject *nbft_get(struct libnvme_global_ctx *ctx, const char * filename) +{ + struct libnbft_info *nbft; + PyObject *output; + int ret; + + ret = libnvme_read_nbft(ctx, &nbft, filename); + if (ret) { + Py_RETURN_NONE; + } + + output = nbft_to_pydict(nbft); + libnvme_free_nbft(ctx, nbft); + return output; +} +%} /* --------- end C implementation block --------- */ + +//############################################################################## + +/* All typemaps must be defined before the %include statements below so that + * they are in scope when SWIG processes the struct and method declarations. */ -struct libnvmf_context {}; + +/* Override SWIG's default char * struct-member setter. SWIG's built-in + * uses memcpy(malloc(...)) which leaks the old value and triggers + * -Wdiscarded-qualifiers for const char *. Use free + strdup instead. + */ +%typemap(memberin) char * { + free($1); + $1 = $input ? strdup($input) : NULL; +} +%typemap(memberin) const char * { + free((char *)$1); + $1 = $input ? strdup($input) : NULL; +} /* Convert a Python dict to a struct libnvmf_context * automatically. * arg1 is the libnvme_global_ctx * (first argument of the enclosing function). @@ -325,51 +556,6 @@ struct libnvmf_context {}; libnvmf_context_free($1); } -%exception libnvme_ctrl::connect { - connect_err = 0; - $action /* $action sets connect_err to non-zero value on failure */ - if (connect_err) { - const char *errstr = libnvme_errno_to_string(-connect_err); - if (errstr) { - SWIG_exception(SWIG_RuntimeError, errstr); - } else { - SWIG_exception(SWIG_RuntimeError, "Connect failed"); - } - } -} - -%exception libnvme_ctrl::disconnect { - connect_err = 0; - errno = 0; - $action /* $action sets connect_err to non-zero value on failure */ - if (connect_err == 1) { - SWIG_exception(SWIG_AttributeError, "No controller connection"); - } else if (connect_err) { - const char *errstr = libnvme_errno_to_string(-connect_err); - if (errstr) { - SWIG_exception(SWIG_RuntimeError, errstr); - } else { - SWIG_exception(SWIG_RuntimeError, "Disconnect failed"); - } - } -} - -%exception libnvme_ctrl::discover { - discover_err = 0; - $action /* $action sets discover_err to non-zero value on failure */ - if (discover_err == 1) { - SWIG_exception(SWIG_AttributeError, "No controller connection"); - } else if (discover_err) { - const char *errstr = libnvme_errno_to_string(-discover_err); - if (errstr) { - SWIG_exception(SWIG_RuntimeError, errstr); - } else { - SWIG_exception(SWIG_RuntimeError, "Discover failed"); - } - } -} - - %typemap(out) uint8_t [8] { $result = PyBytes_FromStringAndSize((char *)$1, 8); }; @@ -556,10 +742,79 @@ struct libnvmf_context {}; $result = obj; }; +// These %include statements must be located after the main %{...%} block and +// all %typemap directives above, so that typemaps are in scope when SWIG +// processes the struct and method declarations in the included files. +%include "nvme-manual-bridges.i" +%include "accessors.i" +%include "accessors-fabrics.i" + +%exception libnvme_ctrl::connect { + connect_err = 0; + $action /* $action sets connect_err to non-zero value on failure */ + if (connect_err) { + const char *errstr = libnvme_errno_to_string(-connect_err); + if (errstr) { + SWIG_exception(SWIG_RuntimeError, errstr); + } else { + SWIG_exception(SWIG_RuntimeError, "Connect failed"); + } + } +} + +%exception libnvme_ctrl::disconnect { + connect_err = 0; + errno = 0; + $action /* $action sets connect_err to non-zero value on failure */ + if (connect_err == 1) { + SWIG_exception(SWIG_AttributeError, "No controller connection"); + } else if (connect_err) { + const char *errstr = libnvme_errno_to_string(-connect_err); + if (errstr) { + SWIG_exception(SWIG_RuntimeError, errstr); + } else { + SWIG_exception(SWIG_RuntimeError, "Disconnect failed"); + } + } +} + +%exception libnvme_ctrl::discover { + discover_err = 0; + $action /* $action sets discover_err to non-zero value on failure */ + if (discover_err == 1) { + SWIG_exception(SWIG_AttributeError, "No controller connection"); + } else if (discover_err) { + const char *errstr = libnvme_errno_to_string(-discover_err); + if (errstr) { + SWIG_exception(SWIG_RuntimeError, errstr); + } else { + SWIG_exception(SWIG_RuntimeError, "Discover failed"); + } + } +} + #include "tree.h" #include "fabrics.h" -#define STR_OR_NONE(str) (!(str) ? "None" : str) +%feature("autodoc", "Read an NBFT binary table from disk.\n" + "\n" + "Args:\n" + " filename: Path to the NBFT binary file.\n" + "\n" + "Returns:\n" + " A dict containing the NBFT data, or None on failure.") nbft_get; +PyObject *nbft_get(struct libnvme_global_ctx *ctx, const char * filename); + +%rename(_libnvme_first_host) libnvme_first_host; +%rename(_libnvme_next_host) libnvme_next_host; +%rename(_libnvme_first_subsystem) libnvme_first_subsystem; +%rename(_libnvme_next_subsystem) libnvme_next_subsystem; +%rename(_libnvme_subsystem_first_ctrl) libnvme_subsystem_first_ctrl; +%rename(_libnvme_subsystem_next_ctrl) libnvme_subsystem_next_ctrl; +%rename(_libnvme_subsystem_first_ns) libnvme_subsystem_first_ns; +%rename(_libnvme_subsystem_next_ns) libnvme_subsystem_next_ns; +%rename(_libnvme_ctrl_first_ns) libnvme_ctrl_first_ns; +%rename(_libnvme_ctrl_next_ns) libnvme_ctrl_next_ns; struct libnvme_host * libnvme_first_host(struct libnvme_global_ctx * ctx); struct libnvme_host * libnvme_next_host(struct libnvme_global_ctx *ctx, struct libnvme_host * h); struct libnvme_subsystem * libnvme_first_subsystem(struct libnvme_host * h); @@ -571,138 +826,16 @@ struct libnvme_ns * libnvme_subsystem_next_ns(struct libnvme_subsystem * s, stru struct libnvme_ns * libnvme_ctrl_first_ns(struct libnvme_ctrl * c); struct libnvme_ns * libnvme_ctrl_next_ns(struct libnvme_ctrl * c, struct libnvme_ns * n); -struct libnvme_global_ctx { - %immutable config_file; - %immutable application; - char *config_file; - char *application; -}; - -struct libnvme_host { - %immutable hostnqn; - %immutable hostid; - %immutable hostsymname; - char *hostnqn; - char *hostid; - char *hostsymname; - %extend { - char *dhchap_host_key; - } -}; - -struct libnvme_subsystem { - %immutable sysfs_dir; - %immutable subsysnqn; - %immutable model; - %immutable serial; - %immutable firmware; - %immutable subsystype; - %immutable iopolicy; - char *subsysnqn; - char *model; - char *serial; - char *firmware; - char *subsystype; - - %extend { - const char *sysfs_dir; - const char *application; - const char *iopolicy; - } -}; - -struct libnvme_ctrl { - %immutable name; - %immutable subsystem; - %immutable state; - %immutable sysfs_dir; - %immutable address; - %immutable firmware; - %immutable model; - %immutable numa_node; - %immutable queue_count; - %immutable serial; - %immutable sqsize; - %immutable transport; - %immutable subsysnqn; - %immutable traddr; - %immutable trsvcid; - %immutable cntrltype; - %immutable cntlid; - %immutable dctype; - %immutable phy_slot; - %immutable discovered; - - const char *cntrltype; // Do not put in %extend because there's no getter method in libnvme.map - const char *dctype; // Do not put in %extend because there's no getter method in libnvme.map - const bool discovered; // Do not put in %extend because there's no getter method in libnvme.map - - %extend { - /** - * By putting these attributes in an %extend block, we're - * forcing SWIG to invoke getter/setter methods instead of - * accessing the members directly. - * - * For example, SWIG will generate code like this: - * name = libnvme_ctrl_name_get(ctrl) - * - * instead of that: - * name = ctrl->name - */ - const char *name; - const char *state; - const char *sysfs_dir; - const char *address; - const char *firmware; - const char *model; - const char *numa_node; - const char *queue_count; - const char *serial; - const char *sqsize; - const char *transport; - const char *subsysnqn; - const char *traddr; - const char *trsvcid; - const char *cntlid; - const char *phy_slot; - - bool unique_discovery_ctrl; - bool discovery_ctrl; - bool persistent; - - char *keyring; - char *tls_key_identity; - char *tls_key; - - char *dhchap_host_key; - char *dhchap_ctrl_key; - - /** - * We are remapping the following member(s) of the C code's - * libnvme_ctrl_t to different name(s) in Python. Here's the - * mapping: - * - * C code Python (SWIG) - * ===================== ===================== - * ctrl->s ctrl->subsystem - */ - struct libnvme_subsystem *subsystem; // Maps to "s" in the C code - } -}; - -struct libnvme_ns { - %immutable nsid; - %immutable eui64; - %immutable nguid; - %immutable uuid; - unsigned int nsid; - uint8_t eui64[8]; - uint8_t nguid[16]; - uint8_t uuid[16]; -}; - - %extend libnvme_global_ctx { + %feature("autodoc", "__init__(self, config_file=None)\n" + "\n" + "Create the root context for the libnvme device tree.\n" + "\n" + "Scans the NVMe topology and loads configuration on creation.\n" + "Supports use as a context manager (``with GlobalCtx() as ctx:``).\n" + "\n" + "Args:\n" + " config_file: Path to a JSON config file, or None for defaults.") libnvme_global_ctx; libnvme_global_ctx(const char *config_file = NULL) { struct libnvme_global_ctx *ctx; @@ -724,6 +857,10 @@ struct libnvme_ns { struct libnvme_global_ctx* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { return $self; } + %feature("autodoc", "Set the libnvme logging verbosity.\n" + "\n" + "Args:\n" + " level: One of 'debug', 'info', 'warning', or 'err'.") log_level; void log_level(const char *level) { int log_level = LIBNVME_DEFAULT_LOGLEVEL; if (!strcmp(level, "debug")) log_level = LIBNVME_LOG_DEBUG; @@ -734,26 +871,13 @@ struct libnvme_ns { } %pythoncode %{ def hosts(self): - """Iterator over all host objects""" - h = libnvme_first_host(self) + """Yield each Host in this context.""" + h = _libnvme_first_host(self) while h: yield h - h = libnvme_next_host(self, h) - def __setattr__(self, name, value): - if name == 'this' or name.startswith('_'): - object.__setattr__(self, name, value) - return - for klass in type(self).__mro__: - attr = klass.__dict__.get(name) - if attr is not None: - if isinstance(attr, property) and attr.fset is not None: - attr.fset(self, value) - return - break - raise AttributeError( - f"'{type(self).__name__}' object has no writable attribute '{name}'" - ) + h = _libnvme_next_host(self, h) %} + %feature("autodoc", "Rescan the NVMe topology and update the device tree.") refresh_topology; void refresh_topology() { libnvme_refresh_topology($self); } @@ -764,12 +888,10 @@ struct libnvme_ns { %define SET_SYMNAME_DOCSTRING -"@brief Set or Clear Host's Symbolic Name - -@param hostsymname: A symbolic name, or None to clear the symbolic name. -@type hostsymname: str|None +"Set or clear the host's symbolic name. -@return: None" +Args: + hostsymname: Symbolic name string, or None to clear it." %enddef %pythonappend libnvme_host::libnvme_host(struct libnvme_global_ctx *ctx, @@ -779,18 +901,32 @@ struct libnvme_ns { const char *hostsymname) { self.__parent = ctx # Keep a reference to parent to ensure garbage collection happens in the right order} %extend libnvme_host { + %feature("autodoc", "__init__(self, ctx, hostnqn=None, hostid=None, hostkey=None, hostsymname=None)\n" + "\n" + "Create a host (initiator) identity object.\n" + "\n" + "Supports use as a context manager (``with Host(ctx) as h:``).\n" + "\n" + "Args:\n" + " ctx: Global context.\n" + " hostnqn: Host NQN. Defaults to the system-wide value.\n" + " hostid: Host UUID. Defaults to the system-wide value.\n" + " hostkey: DH-HMAC-CHAP host key.\n" + " hostsymname: Symbolic host name.") libnvme_host; libnvme_host(struct libnvme_global_ctx *ctx, - const char *hostnqn = NULL, - const char *hostid = NULL, - const char *hostkey = NULL, - const char *hostsymname = NULL) { + const char *hostnqn = NULL, + const char *hostid = NULL, + const char *hostkey = NULL, + const char *hostsymname = NULL) { libnvme_host_t h; + if (libnvme_get_host(ctx, hostnqn, hostid, &h)) return NULL; if (hostsymname) libnvme_host_set_hostsymname(h, hostsymname); if (hostkey) libnvme_host_set_dhchap_host_key(h, hostkey); + return h; } ~libnvme_host() { @@ -808,29 +944,15 @@ struct libnvme_ns { } PyObject* __str__() { - return PyUnicode_FromFormat("nvme.host(%s,%s)", STR_OR_NONE($self->hostnqn), STR_OR_NONE($self->hostid)); + return PyUnicode_FromFormat("nvme.Host(%s,%s)", STR_OR_NONE($self->hostnqn), STR_OR_NONE($self->hostid)); } %pythoncode %{ def subsystems(self): - """Iterator over all subsystem objects""" - s = libnvme_first_subsystem(self) + """Yield each Subsystem under this host.""" + s = _libnvme_first_subsystem(self) while s: yield s - s = libnvme_next_subsystem(self, s) - def __setattr__(self, name, value): - if name == 'this' or name.startswith('_'): - object.__setattr__(self, name, value) - return - for klass in type(self).__mro__: - attr = klass.__dict__.get(name) - if attr is not None: - if isinstance(attr, property) and attr.fset is not None: - attr.fset(self, value) - return - break - raise AttributeError( - f"'{type(self).__name__}' object has no writable attribute '{name}'" - ) + s = _libnvme_next_subsystem(self, s) %} } @@ -841,10 +963,19 @@ struct libnvme_ns { const char *name) { self.__parent = host # Keep a reference to parent to ensure garbage collection happens in the right order} %extend libnvme_subsystem { + %feature("autodoc", "__init__(self, ctx, host, subsysnqn, name=None)\n" + "\n" + "Look up or create a subsystem entry under the given host.\n" + "\n" + "Args:\n" + " ctx: Global context.\n" + " host: Parent Host object.\n" + " subsysnqn: Subsystem NQN.\n" + " name: Optional subsystem device name.") libnvme_subsystem; libnvme_subsystem(struct libnvme_global_ctx *ctx, - struct libnvme_host *host, - const char *subsysnqn, - const char *name = NULL) { + struct libnvme_host *host, + const char *subsysnqn, + const char *name = NULL) { struct libnvme_subsystem *s; if (libnvme_get_subsystem(ctx, host, name, subsysnqn, &s)) @@ -862,40 +993,24 @@ struct libnvme_ns { return $self; } PyObject *__str__() { - return PyUnicode_FromFormat("nvme.subsystem(%s,%s)", STR_OR_NONE($self->name), STR_OR_NONE($self->subsysnqn)); + return PyUnicode_FromFormat("nvme.Subsystem(%s,%s)", STR_OR_NONE($self->name), STR_OR_NONE($self->subsysnqn)); } %pythoncode %{ def controllers(self): - """Iterator over all controller objects""" - c = libnvme_subsystem_first_ctrl(self) + """Yield each Ctrl under this subsystem.""" + c = _libnvme_subsystem_first_ctrl(self) while c: yield c - c = libnvme_subsystem_next_ctrl(self, c) + c = _libnvme_subsystem_next_ctrl(self, c) %} %pythoncode %{ def namespaces(self): - """Iterator over all namespace objects""" - ns = libnvme_subsystem_first_ns(self) + """Yield each Namespace under this subsystem.""" + ns = _libnvme_subsystem_first_ns(self) while ns: yield ns - ns = libnvme_subsystem_next_ns(self, ns) - def __setattr__(self, name, value): - if name == 'this' or name.startswith('_'): - object.__setattr__(self, name, value) - return - for klass in type(self).__mro__: - attr = klass.__dict__.get(name) - if attr is not None: - if isinstance(attr, property) and attr.fset is not None: - attr.fset(self, value) - return - break - raise AttributeError( - f"'{type(self).__name__}' object has no writable attribute '{name}'" - ) + ns = _libnvme_subsystem_next_ns(self, ns) %} - %immutable name; - const char *name; %immutable host; struct libnvme_host *host; } @@ -903,14 +1018,17 @@ struct libnvme_ns { %pythonappend libnvme_ctrl::connect(struct libnvme_host *h) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} -%pythonappend libnvme_ctrl::init(struct libnvme_host *h, int instance) { +%pythonappend libnvme_ctrl::load_from_device(struct libnvme_host *h, int instance) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} %extend libnvme_ctrl { - %feature("autodoc", "__init__(ctrl self, global_ctx ctx, dict fctx) -> ctrl\n" + %feature("autodoc", "__init__(self, ctx, cfg)\n" "\n" "Create a new NVMe-oF controller object.\n" "\n" - " params dict keys:\n" + "``cfg`` is a flat dict — all keys are at the top level, no nesting.\n" + "An unknown key raises KeyError immediately.\n" + "\n" + " cfg keys:\n" "\n" " Required:\n" " subsysnqn (str) -- Subsystem NQN\n" @@ -955,6 +1073,33 @@ struct libnvme_ns { "\n" " Persistence (optional):\n" " persistent (bool) -- Keep connection alive after process exit\n" + "\n" + " Examples::\n" + "\n" + " import nvme\n" + "\n" + " ctx = nvme.GlobalCtx()\n" + "\n" + " # Discover controllers at a remote target\n" + " with nvme.Ctrl(ctx, {\n" + " 'subsysnqn': 'nqn.2014-08.org.nvmexpress.discovery',\n" + " 'transport': 'tcp',\n" + " 'traddr': '192.168.1.100',\n" + " 'trsvcid': '8009',\n" + " }) as c:\n" + " log = c.discover()\n" + "\n" + " # Connect to a subsystem with TLS and header digest\n" + " host = nvme.Host(ctx)\n" + " with nvme.Ctrl(ctx, {\n" + " 'subsysnqn': 'nqn.2019-08.org.nvmexpress:uuid:...',\n" + " 'transport': 'tcp',\n" + " 'traddr': '192.168.1.100',\n" + " 'trsvcid': '4420',\n" + " 'hdr_digest': True,\n" + " 'tls': True,\n" + " }) as c:\n" + " c.connect(host)\n" ) libnvme_ctrl; libnvme_ctrl(struct libnvme_global_ctx *ctx, struct libnvmf_context *fctx) { struct libnvme_ctrl *c; @@ -978,10 +1123,32 @@ struct libnvme_ns { return $self; } - bool init(struct libnvme_host *h, int instance) { + %feature("autodoc", "Bind this controller object to an existing kernel NVMe device.\n" + "\n" + "Associates the object with the kernel device identified by\n" + "``instance`` (e.g. 0 for /dev/nvme0).\n" + "\n" + "Args:\n" + " h: Host object.\n" + " instance: Kernel device instance number.\n" + "\n" + "Returns:\n" + " True on success, False on failure.") load_from_device; + bool load_from_device(struct libnvme_host *h, int instance) { return libnvme_init_ctrl(h, $self, instance) == 0; } + %feature("autodoc", "Connect this controller to an NVMe-oF target.\n" + "\n" + "Establishes the kernel connection. This call may block while\n" + "performing network operations. Other Python threads continue\n" + "to run during this time.\n" + "\n" + "Args:\n" + " h: Host object to associate with the connection.\n" + "\n" + "Raises:\n" + " RuntimeError: Connection failed.") connect; void connect(struct libnvme_host *h) { int ret; @@ -994,12 +1161,19 @@ struct libnvme_ns { return; } } + %feature("autodoc", "Return True if this controller is currently connected.") connected; bool connected() { return libnvme_ctrl_get_name($self) != NULL; } + %feature("autodoc", "Rescan this controller and refresh its namespace list.") rescan; void rescan() { libnvme_rescan_ctrl($self); } + %feature("autodoc", "Disconnect this controller from the NVMe-oF target.\n" + "\n" + "Raises:\n" + " AttributeError: Controller is not currently connected.\n" + " RuntimeError: Disconnect failed.") disconnect; void disconnect() { int ret; const char *dev; @@ -1016,13 +1190,16 @@ struct libnvme_ns { connect_err = 2; } - %feature("autodoc", "@return: True if controller supports explicit registration. False otherwise.") is_registration_supported; + %feature("autodoc", "Return True if this controller supports explicit host registration.") is_registration_supported; bool is_registration_supported() { return libnvmf_is_registration_supported($self); } - %feature("autodoc", "@return None on success or Error string on error.") registration_ctlr; - PyObject *registration_ctlr(enum nvmf_dim_tas tas) { + %feature("autodoc", "Register this controller with the NVMe-oF DIM service.\n" + "\n" + "Returns:\n" + " None on success, or an error string describing the failure.") registration_ctrl; + PyObject *registration_ctrl(enum nvmf_dim_tas tas) { __u32 result; int status; @@ -1030,17 +1207,35 @@ struct libnvme_ns { status = libnvmf_register_ctrl($self, NVMF_DIM_TAS_REGISTER, &result); Py_END_ALLOW_THREADS /* Reacquire Python GIL */ - if (status != NVME_SC_SUCCESS) { - /* On error, return an error message */ - return (status < 0) ? - PyUnicode_FromFormat("Status:0x%04x - %s", status, libnvme_status_to_string(status, false)) : - PyUnicode_FromFormat("Result:0x%04x, Status:0x%04x - %s", result, status, libnvme_status_to_string(status, false)); - } + if (status != NVME_SC_SUCCESS) { + /* On error, return an error message */ + return (status < 0) ? + PyUnicode_FromFormat("Status:0x%04x - %s", status, libnvme_status_to_string(status, false)) : + PyUnicode_FromFormat("Result:0x%04x, Status:0x%04x - %s", result, status, libnvme_status_to_string(status, false)); + } /* On success, return None */ Py_RETURN_NONE; } + %feature("autodoc", "Retrieve the discovery log page from a connected discovery controller.\n" + "\n" + "This call may block while performing network operations.\n" + "Other Python threads continue to run during this time.\n" + "\n" + "Args:\n" + " lsp: Log Specific Parameter (default 0).\n" + " max_retries: Maximum number of retries (default 6).\n" + "\n" + "Returns:\n" + " A list of discovery log entries. Each entry is a dictionary\n" + " describing a reachable controller or referral, with keys such\n" + " as ``trtype``, ``traddr``, ``trsvcid``, ``subnqn``, and\n" + " ``subtype``.\n" + "\n" + "Raises:\n" + " AttributeError: Controller is not connected.\n" + " RuntimeError: Discovery failed.") discover; %newobject discover; struct nvmf_discovery_log *discover(int lsp = 0, int max_retries = 6) { struct nvmf_discovery_log *logp = NULL; @@ -1064,7 +1259,14 @@ struct libnvme_ns { return logp; } - %feature("autodoc", "@return: List of supported log pages") supported_log_pages; + %feature("autodoc", "Fetch the Supported Log Pages log.\n" + "\n" + "Args:\n" + " rae: Retain Asynchronous Events (default True).\n" + "\n" + "Returns:\n" + " A list of integers, one per Log Identifier, encoding its\n" + " supported features. Returns None if the command fails.") supported_log_pages; PyObject *supported_log_pages(bool rae = true) { struct nvme_supported_log_pages log; struct libnvme_passthru_cmd cmd; @@ -1091,31 +1293,17 @@ struct libnvme_ns { PyObject* __str__() { return $self->address ? - PyUnicode_FromFormat("libnvme_ctrl(transport=%s,%s)", STR_OR_NONE($self->transport), STR_OR_NONE($self->address)) : - PyUnicode_FromFormat("libnvme_ctrl(transport=%s)", STR_OR_NONE($self->transport)); + PyUnicode_FromFormat("nvme.Ctrl(transport=%s,%s)", STR_OR_NONE($self->transport), STR_OR_NONE($self->address)) : + PyUnicode_FromFormat("nvme.Ctrl(transport=%s)", STR_OR_NONE($self->transport)); } %pythoncode %{ def namespaces(self): - """Iterator over all namespace objects""" - ns = libnvme_ctrl_first_ns(self) + """Yield each Namespace under this controller.""" + ns = _libnvme_ctrl_first_ns(self) while ns: yield ns - ns = libnvme_ctrl_next_ns(self, ns) - def __setattr__(self, name, value): - if name == 'this' or name.startswith('_'): - object.__setattr__(self, name, value) - return - for klass in type(self).__mro__: - attr = klass.__dict__.get(name) - if attr is not None: - if isinstance(attr, property) and attr.fset is not None: - attr.fset(self, value) - return - break - raise AttributeError( - f"'{type(self).__name__}' object has no writable attribute '{name}'" - ) + ns = _libnvme_ctrl_next_ns(self, ns) %} } @@ -1124,6 +1312,13 @@ struct libnvme_ns { unsigned int nsid) { self.__parent = s # Keep a reference to parent to ensure garbage collection happens in the right order} %extend libnvme_ns { + %feature("autodoc", "__init__(self, subsystem, nsid)\n" + "\n" + "Look up a namespace by ID within a subsystem.\n" + "\n" + "Args:\n" + " subsystem: Parent Subsystem object.\n" + " nsid: Namespace identifier.") libnvme_ns; libnvme_ns(struct libnvme_subsystem *s, unsigned int nsid) { return libnvme_subsystem_lookup_namespace(s, nsid); @@ -1138,251 +1333,11 @@ struct libnvme_ns { return $self; } PyObject *__str__() { - return PyUnicode_FromFormat("nvme.ns(%u)", $self->nsid); + return PyUnicode_FromFormat("nvme.Namespace(%u)", $self->nsid); } - %immutable name; - const char *name; } -/****** - NBFT - ******/ -%{ - static PyObject *ssns_to_dict(struct libnbft_subsystem_ns *ss) - { - unsigned int i; - PyObject *output = PyDict_New(); - PyObject *hfis = PyList_New(ss->num_hfis); - - for (i = 0; i < ss->num_hfis; i++) - PyList_SetItem(hfis, i, PyLong_FromLong(ss->hfis[i]->index - 1)); /* steals ref. to object - no need to decref */ - - PyDict_SetItemStringDecRef(output, "hfi_indexes", hfis); - - PyDict_SetItemStringDecRef(output, "trtype", PyUnicode_FromString(ss->transport)); - PyDict_SetItemStringDecRef(output, "traddr", PyUnicode_FromString(ss->traddr)); - PyDict_SetItemStringDecRef(output, "trsvcid", PyUnicode_FromString(ss->trsvcid)); - PyDict_SetItemStringDecRef(output, "subsys_port_id", PyLong_FromLong(ss->subsys_port_id)); - PyDict_SetItemStringDecRef(output, "nsid", PyLong_FromLong(ss->nsid)); - - { - PyObject *nid; - switch (ss->nid_type) { - case LIBNBFT_NID_TYPE_EUI64: - PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("eui64")); - nid = PyUnicode_FromFormat("%02x%02x%02x%02x%02x%02x%02x%02x", - ss->nid[0], ss->nid[1], ss->nid[2], ss->nid[3], - ss->nid[4], ss->nid[5], ss->nid[6], ss->nid[7]); - break; - - case LIBNBFT_NID_TYPE_NGUID: - PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("nguid")); - nid = PyUnicode_FromFormat("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - ss->nid[0], ss->nid[1], ss->nid[2], ss->nid[3], - ss->nid[4], ss->nid[5], ss->nid[6], ss->nid[7], - ss->nid[8], ss->nid[9], ss->nid[10], ss->nid[11], - ss->nid[12], ss->nid[13], ss->nid[14], ss->nid[15]); - break; - - case LIBNBFT_NID_TYPE_NS_UUID: - { - char uuid_str[NVME_UUID_LEN_STRING]; - PyDict_SetItemStringDecRef(output, "nid_type", PyUnicode_FromString("uuid")); - libnvme_uuid_to_string(ss->nid, uuid_str); - nid = PyUnicode_FromString(uuid_str); - break; - } - - default: - nid = NULL; - break; - } - if (nid) - PyDict_SetItemStringDecRef(output, "nid", nid); - } - - if (ss->subsys_nqn) - PyDict_SetItemStringDecRef(output, "subsys_nqn", PyUnicode_FromString(ss->subsys_nqn)); - - PyDict_SetItemStringDecRef(output, "controller_id", PyLong_FromLong(ss->controller_id)); - PyDict_SetItemStringDecRef(output, "asqsz", PyLong_FromLong(ss->asqsz)); - - if (ss->dhcp_root_path_string) - PyDict_SetItemStringDecRef(output, "dhcp_root_path_string", PyUnicode_FromString(ss->dhcp_root_path_string)); - - PyDict_SetItemStringDecRef(output, "pdu_header_digest_required", PyBool_FromLong(ss->pdu_header_digest_required)); - PyDict_SetItemStringDecRef(output, "data_digest_required", PyBool_FromLong(ss->data_digest_required)); - - return output; - } - - static PyObject *hfi_to_dict(struct libnbft_hfi *hfi) - { - PyObject *output = PyDict_New(); - - PyDict_SetItemStringDecRef(output, "trtype", PyUnicode_FromString(hfi->transport)); - - if (!strcmp(hfi->transport, "tcp")) { - PyDict_SetItemStringDecRef(output, "pcidev", - PyUnicode_FromFormat("%x:%x:%x.%x", - ((hfi->tcp_info.pci_sbdf & 0xffff0000) >> 16), - ((hfi->tcp_info.pci_sbdf & 0x0000ff00) >> 8), - ((hfi->tcp_info.pci_sbdf & 0x000000f8) >> 3), - ((hfi->tcp_info.pci_sbdf & 0x00000007) >> 0))); - - PyDict_SetItemStringDecRef(output, "mac_addr", - PyUnicode_FromFormat("%02x:%02x:%02x:%02x:%02x:%02x", - hfi->tcp_info.mac_addr[0], - hfi->tcp_info.mac_addr[1], - hfi->tcp_info.mac_addr[2], - hfi->tcp_info.mac_addr[3], - hfi->tcp_info.mac_addr[4], - hfi->tcp_info.mac_addr[5])); - - PyDict_SetItemStringDecRef(output, "vlan", PyLong_FromLong(hfi->tcp_info.vlan)); - PyDict_SetItemStringDecRef(output, "ip_origin", PyLong_FromLong(hfi->tcp_info.ip_origin)); - PyDict_SetItemStringDecRef(output, "ipaddr", PyUnicode_FromString(hfi->tcp_info.ipaddr)); - PyDict_SetItemStringDecRef(output, "subnet_mask_prefix", PyLong_FromLong(hfi->tcp_info.subnet_mask_prefix)); - PyDict_SetItemStringDecRef(output, "gateway_ipaddr", PyUnicode_FromString(hfi->tcp_info.gateway_ipaddr)); - PyDict_SetItemStringDecRef(output, "route_metric", PyLong_FromLong(hfi->tcp_info.route_metric)); - PyDict_SetItemStringDecRef(output, "primary_dns_ipaddr", PyUnicode_FromString(hfi->tcp_info.primary_dns_ipaddr)); - PyDict_SetItemStringDecRef(output, "secondary_dns_ipaddr", PyUnicode_FromString(hfi->tcp_info.secondary_dns_ipaddr)); - PyDict_SetItemStringDecRef(output, "dhcp_server_ipaddr", PyUnicode_FromString(hfi->tcp_info.dhcp_server_ipaddr)); - - if (hfi->tcp_info.host_name) - PyDict_SetItemStringDecRef(output, "host_name", PyUnicode_FromString(hfi->tcp_info.host_name)); - - PyDict_SetItemStringDecRef(output, "this_hfi_is_default_route", PyBool_FromLong(hfi->tcp_info.this_hfi_is_default_route)); - PyDict_SetItemStringDecRef(output, "dhcp_override", PyBool_FromLong(hfi->tcp_info.dhcp_override)); - } - - return output; - } - - static PyObject *discovery_to_dict(struct libnbft_discovery *disc) - { - PyObject *output = PyDict_New(); - - if (disc->security) - PyDict_SetItemStringDecRef(output, "security_index", PyLong_FromLong(disc->security->index)); - if (disc->hfi) - PyDict_SetItemStringDecRef(output, "hfi_index", PyLong_FromLong(disc->hfi->index - 1)); - if (disc->uri) - PyDict_SetItemStringDecRef(output, "uri", PyUnicode_FromString(disc->uri)); - if (disc->nqn) - PyDict_SetItemStringDecRef(output, "nqn", PyUnicode_FromString(disc->nqn)); - - return output; - } - - static PyObject *nbft_to_pydict(struct libnbft_info *nbft) - { - PyObject *val; - PyObject *output = PyDict_New(); - - { - PyObject *host = PyDict_New(); - - if (nbft->host.nqn) - PyDict_SetItemStringDecRef(host, "nqn", PyUnicode_FromString(nbft->host.nqn)); - if (nbft->host.id) { - char uuid_str[NVME_UUID_LEN_STRING]; - libnvme_uuid_to_string((unsigned char *)nbft->host.id, uuid_str); - PyDict_SetItemStringDecRef(host, "id", PyUnicode_FromString(uuid_str)); - } - - PyDict_SetItemStringDecRef(host, "host_id_configured", PyBool_FromLong(nbft->host.host_id_configured)); - PyDict_SetItemStringDecRef(host, "host_nqn_configured", PyBool_FromLong(nbft->host.host_nqn_configured)); - - val = PyUnicode_FromString(nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_NOT_INDICATED ? "not indicated" : - nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_UNSELECTED ? "unselected" : - nbft->host.primary == LIBNBFT_PRIMARY_ADMIN_HOST_FLAG_SELECTED ? "selected" : "reserved"); - PyDict_SetItemStringDecRef(host, "primary_admin_host_flag", val); - - PyDict_SetItemStringDecRef(output, "host", host); - } - - { - size_t ss_num = 0; - struct libnbft_subsystem_ns **ss; - PyObject *subsystem; - - /* First, let's find how many entries there are */ - for (ss = nbft->subsystem_ns_list; ss && *ss; ss++) - ss_num++; - - /* Now, let's fill the list using "(*ss)->index - 1" - as the index for writing to the list */ - subsystem = PyList_New(ss_num); - for (ss = nbft->subsystem_ns_list; ss && *ss; ss++) - PyList_SetItem(subsystem, (*ss)->index - 1, ssns_to_dict(*ss)); /* steals ref. to object - no need to decref */ - - PyDict_SetItemStringDecRef(output, "subsystem", subsystem); - } - - { - size_t hfi_num = 0; - struct libnbft_hfi **hfi; - PyObject *hfis; - - /* First, let's find how many entries there are */ - for (hfi = nbft->hfi_list; hfi && *hfi; hfi++) - hfi_num++; - - /* Now, let's fill the list using "(*hfi)->index - 1" - as the index for writing to the list */ - hfis = PyList_New(hfi_num); - for (hfi = nbft->hfi_list; hfi && *hfi; hfi++) - PyList_SetItem(hfis, (*hfi)->index-1, hfi_to_dict(*hfi)); /* steals ref. to object - no need to decref */ - - PyDict_SetItemStringDecRef(output, "hfi", hfis); - } - - { - size_t disc_num = 0; - struct libnbft_discovery **disc; - PyObject *discovery; - - /* First, let's find how many entries there are */ - for (disc = nbft->discovery_list; disc && *disc; disc++) - disc_num++; - - /* Now, let's fill the list using "(*disc)->index - 1" - as the index for writing to the list */ - discovery = PyList_New(disc_num); - for (disc = nbft->discovery_list; disc && *disc; disc++) - PyList_SetItem(discovery, (*disc)->index - 1, discovery_to_dict(*disc)); /* steals ref. to object - no need to decref */ - - PyDict_SetItemStringDecRef(output, "discovery", discovery); - } - - /* Security profiles are currently not implemented. */ - - return output; - } - - PyObject *nbft_get(struct libnvme_global_ctx *ctx, const char * filename) - { - struct libnbft_info *nbft; - PyObject *output; - int ret; - - ret = libnvme_read_nbft(ctx, &nbft, filename); - if (ret) { - Py_RETURN_NONE; - } - - output = nbft_to_pydict(nbft); - libnvme_free_nbft(ctx, nbft); - return output; - } -%}; - -%feature("autodoc", "@return an NBFT table as a dict on success, None otherwise.\n" - "@param filename: file to read") nbft_get; -PyObject *nbft_get(struct libnvme_global_ctx *ctx, const char * filename); - // We want to swig all the #define and enum from nvme-types.h, but none of the structs. #pragma SWIG nowarn=503 // Supress warnings about unnamed struct #define __attribute__(x) diff --git a/libnvme/libnvme/tests/create-ctrl-obj.py b/libnvme/libnvme/tests/create-ctrl-obj.py index 59a05f15c5..59c52d5c4b 100755 --- a/libnvme/libnvme/tests/create-ctrl-obj.py +++ b/libnvme/libnvme/tests/create-ctrl-obj.py @@ -4,10 +4,10 @@ from libnvme import nvme -ctx = nvme.global_ctx() +ctx = nvme.GlobalCtx() ctx.log_level('debug') -ctrl = nvme.ctrl(ctx, { +ctrl = nvme.Ctrl(ctx, { 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, 'transport': 'loop', 'traddr': '127.0.0.1', diff --git a/libnvme/libnvme/tests/gc.py b/libnvme/libnvme/tests/gc.py index 682074b9e8..0d571e01a3 100755 --- a/libnvme/libnvme/tests/gc.py +++ b/libnvme/libnvme/tests/gc.py @@ -5,16 +5,16 @@ import gc from libnvme import nvme -ctx = nvme.global_ctx() +ctx = nvme.GlobalCtx() ctx.log_level('debug') print(f'ctx: {ctx}') -host = nvme.host(ctx) +host = nvme.Host(ctx) print(f'host: {host}') ctrls = [] for i in range(10): - ctrl = nvme.ctrl(ctx, { + ctrl = nvme.Ctrl(ctx, { 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, 'transport': 'loop', }) diff --git a/libnvme/libnvme/tests/test-nbft.py b/libnvme/libnvme/tests/test-nbft.py index aa96745391..2e72d1ca2f 100755 --- a/libnvme/libnvme/tests/test-nbft.py +++ b/libnvme/libnvme/tests/test-nbft.py @@ -77,7 +77,7 @@ def setUp(self): def test_read_nbft_file(self): """Make sure we get expected data when reading from binary NBFT file""" - ctx = nvme.global_ctx() + ctx = nvme.GlobalCtx() ctx.log_level('debug') actual_nbft = nvme.nbft_get(ctx, args.filename) self.assertEqual(actual_nbft, self.expected_nbft) diff --git a/libnvme/libnvme/tests/test-objects.py b/libnvme/libnvme/tests/test-objects.py index 0fc2a885c4..3e0077b2ab 100644 --- a/libnvme/libnvme/tests/test-objects.py +++ b/libnvme/libnvme/tests/test-objects.py @@ -27,24 +27,24 @@ def test_log_lid_discovery_is_int(self): class TestGlobalCtx(unittest.TestCase): def test_creation_no_args(self): - ctx = nvme.global_ctx() + ctx = nvme.GlobalCtx() self.assertIsNotNone(ctx) def test_context_manager(self): - with nvme.global_ctx() as ctx: + with nvme.GlobalCtx() as ctx: self.assertIsNotNone(ctx) def test_hosts_iterator_returns_list(self): - ctx = nvme.global_ctx() + ctx = nvme.GlobalCtx() hosts = list(ctx.hosts()) self.assertIsInstance(hosts, list) def test_refresh_topology_does_not_raise(self): - ctx = nvme.global_ctx() + ctx = nvme.GlobalCtx() ctx.refresh_topology() def test_log_level_all_valid_levels(self): - ctx = nvme.global_ctx() + ctx = nvme.GlobalCtx() for level in ('debug', 'info', 'warning', 'err'): with self.subTest(level=level): ctx.log_level(level) @@ -53,64 +53,64 @@ def test_log_level_all_valid_levels(self): class TestHost(unittest.TestCase): def setUp(self): - self.ctx = nvme.global_ctx() + self.ctx = nvme.GlobalCtx() def tearDown(self): self.ctx = None gc.collect() def test_creation_default(self): - host = nvme.host(self.ctx) + host = nvme.Host(self.ctx) self.assertIsNotNone(host) def test_creation_with_explicit_hostnqn(self): hostnqn = 'nqn.2014-08.com.example:test-host-creation' - host = nvme.host(self.ctx, hostnqn=hostnqn) + host = nvme.Host(self.ctx, hostnqn=hostnqn) self.assertIsNotNone(host) self.assertEqual(host.hostnqn, hostnqn) def test_creation_with_hostnqn_and_hostid(self): hostnqn = 'nqn.2014-08.com.example:test-host-props' hostid = '11111111-2222-3333-4444-555555555555' - host = nvme.host(self.ctx, hostnqn=hostnqn, hostid=hostid) + host = nvme.Host(self.ctx, hostnqn=hostnqn, hostid=hostid) self.assertEqual(host.hostnqn, hostnqn) self.assertEqual(host.hostid, hostid) def test_creation_with_hostsymname(self): hostnqn = 'nqn.2014-08.com.example:test-host-symname' symname = 'my-storage-host' - host = nvme.host(self.ctx, hostnqn=hostnqn, hostsymname=symname) + host = nvme.Host(self.ctx, hostnqn=hostnqn, hostsymname=symname) self.assertEqual(host.hostsymname, symname) def test_set_symname(self): hostnqn = 'nqn.2014-08.com.example:test-host-set-symname' - host = nvme.host(self.ctx, hostnqn=hostnqn) + host = nvme.Host(self.ctx, hostnqn=hostnqn) host.set_symname('updated-symname') self.assertEqual(host.hostsymname, 'updated-symname') def test_dhchap_host_key_is_none_by_default(self): hostnqn = 'nqn.2014-08.com.example:test-host-dhchap' - host = nvme.host(self.ctx, hostnqn=hostnqn) + host = nvme.Host(self.ctx, hostnqn=hostnqn) self.assertIsNone(host.dhchap_host_key) def test_subsystems_iterator_returns_list(self): - host = nvme.host(self.ctx) + host = nvme.Host(self.ctx) subsystems = list(host.subsystems()) self.assertIsInstance(subsystems, list) def test_str_contains_class_name(self): - host = nvme.host(self.ctx) - self.assertIn('nvme.host', str(host)) + host = nvme.Host(self.ctx) + self.assertIn('nvme.Host', str(host)) def test_context_manager(self): - with nvme.host(self.ctx) as h: + with nvme.Host(self.ctx) as h: self.assertIsNotNone(h) class TestCtrl(unittest.TestCase): def setUp(self): - self.ctx = nvme.global_ctx() + self.ctx = nvme.GlobalCtx() self.subsysnqn = nvme.NVME_DISC_SUBSYS_NAME def tearDown(self): @@ -118,7 +118,7 @@ def tearDown(self): gc.collect() def _make_loop_ctrl(self): - return nvme.ctrl(self.ctx, { + return nvme.Ctrl(self.ctx, { 'subsysnqn': self.subsysnqn, 'transport': 'loop', }) @@ -128,7 +128,7 @@ def test_creation_loop_transport(self): self.assertIsNotNone(ctrl) def test_creation_tcp_transport_with_traddr(self): - ctrl = nvme.ctrl(self.ctx, { + ctrl = nvme.Ctrl(self.ctx, { 'subsysnqn': self.subsysnqn, 'transport': 'tcp', 'traddr': '192.168.1.1', @@ -145,7 +145,7 @@ def test_subsysnqn_property(self): self.assertEqual(ctrl.subsysnqn, self.subsysnqn) def test_traddr_property(self): - ctrl = nvme.ctrl(self.ctx, { + ctrl = nvme.Ctrl(self.ctx, { 'subsysnqn': self.subsysnqn, 'transport': 'tcp', 'traddr': '10.0.0.1', @@ -153,7 +153,7 @@ def test_traddr_property(self): self.assertEqual(ctrl.traddr, '10.0.0.1') def test_trsvcid_property(self): - ctrl = nvme.ctrl(self.ctx, { + ctrl = nvme.Ctrl(self.ctx, { 'subsysnqn': self.subsysnqn, 'transport': 'tcp', 'traddr': '10.0.0.1', @@ -175,7 +175,7 @@ def test_str_contains_transport(self): self.assertIn('loop', s) def test_context_manager(self): - with nvme.ctrl(self.ctx, { + with nvme.Ctrl(self.ctx, { 'subsysnqn': self.subsysnqn, 'transport': 'loop', }) as c: @@ -223,8 +223,8 @@ class TestCtrlErrorHandling(unittest.TestCase): """Error paths that can be exercised without real hardware.""" def setUp(self): - self.ctx = nvme.global_ctx() - self.ctrl = nvme.ctrl(self.ctx, { + self.ctx = nvme.GlobalCtx() + self.ctrl = nvme.Ctrl(self.ctx, { 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, 'transport': 'loop', }) diff --git a/libnvme/libnvme/tests/test-setattr.py b/libnvme/libnvme/tests/test-setattr.py index 776e054e7d..ee37be66c5 100644 --- a/libnvme/libnvme/tests/test-setattr.py +++ b/libnvme/libnvme/tests/test-setattr.py @@ -11,8 +11,8 @@ class TestCtrlSetattr(unittest.TestCase): def setUp(self): - self.ctx = nvme.global_ctx() - self.ctrl = nvme.ctrl(self.ctx, { + self.ctx = nvme.GlobalCtx() + self.ctrl = nvme.Ctrl(self.ctx, { 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, 'transport': 'loop', }) @@ -40,12 +40,12 @@ def test_unknown_attr_raises(self): class TestCtrlDictValidation(unittest.TestCase): def setUp(self): - self.ctx = nvme.global_ctx() + self.ctx = nvme.GlobalCtx() def test_unknown_dict_key_raises(self): """An unknown key in the constructor dict must raise KeyError.""" with self.assertRaises(KeyError): - nvme.ctrl(self.ctx, { + nvme.Ctrl(self.ctx, { 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, 'transport': 'loop', 'typo_key': 'value', @@ -54,9 +54,9 @@ def test_unknown_dict_key_raises(self): def test_missing_required_key_raises(self): """Omitting a required key (subsysnqn or transport) must raise KeyError.""" with self.assertRaises(KeyError): - nvme.ctrl(self.ctx, {'transport': 'loop'}) + nvme.Ctrl(self.ctx, {'transport': 'loop'}) with self.assertRaises(KeyError): - nvme.ctrl(self.ctx, {'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME}) + nvme.Ctrl(self.ctx, {'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME}) if __name__ == '__main__': diff --git a/libnvme/src/accessors.ld b/libnvme/src/accessors.ld index cc61e38924..38f4b4aa4d 100644 --- a/libnvme/src/accessors.ld +++ b/libnvme/src/accessors.ld @@ -55,7 +55,6 @@ LIBNVME_ACCESSORS_3 { libnvme_ns_get_nsid; libnvme_ns_set_nsid; libnvme_ns_get_name; - libnvme_ns_set_name; libnvme_ns_get_generic_name; libnvme_ns_get_sysfs_dir; libnvme_ns_set_sysfs_dir; diff --git a/libnvme/src/nvme/accessors.c b/libnvme/src/nvme/accessors.c index 66c9a12fcf..ccf049214f 100644 --- a/libnvme/src/nvme/accessors.c +++ b/libnvme/src/nvme/accessors.c @@ -315,12 +315,6 @@ __public __u32 libnvme_ns_get_nsid(const struct libnvme_ns *p) return p->nsid; } -__public void libnvme_ns_set_name(struct libnvme_ns *p, const char *name) -{ - free(p->name); - p->name = name ? strdup(name) : NULL; -} - __public const char *libnvme_ns_get_name(const struct libnvme_ns *p) { return p->name; diff --git a/libnvme/src/nvme/accessors.h b/libnvme/src/nvme/accessors.h index fe94781829..408f70762b 100644 --- a/libnvme/src/nvme/accessors.h +++ b/libnvme/src/nvme/accessors.h @@ -426,13 +426,6 @@ void libnvme_ns_set_nsid(struct libnvme_ns *p, __u32 nsid); */ __u32 libnvme_ns_get_nsid(const struct libnvme_ns *p); -/** - * libnvme_ns_set_name() - Set name. - * @p: The &struct libnvme_ns instance to update. - * @name: New string; a copy is stored. Pass NULL to clear. - */ -void libnvme_ns_set_name(struct libnvme_ns *p, const char *name); - /** * libnvme_ns_get_name() - Get name. * @p: The &struct libnvme_ns instance to query. diff --git a/libnvme/src/nvme/private.h b/libnvme/src/nvme/private.h index 38b3e07270..e40919c181 100644 --- a/libnvme/src/nvme/private.h +++ b/libnvme/src/nvme/private.h @@ -196,7 +196,7 @@ struct libnvme_stat { double ts_ms; /* timestamp when the stat is updated */ }; -struct libnvme_path { // !generate-accessors:read=custom,write=none +struct libnvme_path { // !generate-accessors:read=generated,write=none struct list_node entry; struct list_node nentry; @@ -212,15 +212,15 @@ struct libnvme_path { // !generate-accessors:read=custom,write=none struct libnvme_ctrl *c; struct libnvme_ns *n; - 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; + char *name; // !access:write=generated + char *sysfs_dir; // !access:write=generated + char *ana_state; // !access:read=custom + char *numa_nodes; // !access:read=custom + int grpid; // !access:write=generated + int queue_depth; // !access:read=custom + long multipath_failover_count; // !access:read=custom + long command_retry_count; // !access:read=custom + long command_error_count; // !access:read=custom }; struct libnvme_ns_head { @@ -230,7 +230,7 @@ struct libnvme_ns_head { char *sysfs_dir; }; -struct libnvme_ns { // !generate-accessors +struct libnvme_ns { // !generate-accessors:read=generated,write=none !generate-python:alias=Namespace struct list_node entry; struct libnvme_subsystem *s; @@ -245,33 +245,33 @@ struct libnvme_ns { // !generate-accessors * Managed exclusively by the stat subsystem — do not access directly. */ struct libnvme_stat stat[2]; - unsigned int curr_idx; // !access:read=none,write=none - bool diffstat; // !access:read=none,write=none + unsigned int curr_idx; // !access:read=none + bool diffstat; // !access:read=none struct libnvme_transport_handle *hdl; - __u32 nsid; + __u32 nsid; // !access:write=generated char *name; - char *generic_name; // !access:write=none - char *sysfs_dir; - - int lba_shift; - int lba_size; - int meta_size; - uint64_t lba_count; - uint64_t lba_util; - - uint8_t eui64[8]; // !access:write=none - uint8_t nguid[16]; // !access:write=none - unsigned char uuid[NVME_UUID_LEN]; // !access:read=none,write=none - enum nvme_csi csi; // !access:write=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 + char *generic_name; + char *sysfs_dir; // !access:write=generated + + int lba_shift; // !access:write=generated + int lba_size; // !access:write=generated + int meta_size; // !access:write=generated + uint64_t lba_count; // !access:write=generated + uint64_t lba_util; // !access:write=generated + + uint8_t eui64[8]; + uint8_t nguid[16]; + unsigned char uuid[NVME_UUID_LEN]; // !access:read=none + enum nvme_csi csi; + + long command_retry_count; // !access:read=custom + long command_error_count; // !access:read=custom + long requeue_no_usable_path_count; // !access:read=custom + long fail_no_available_path_count; // !access:read=custom }; -struct libnvme_ctrl { // !generate-accessors:read=generated,write=none +struct libnvme_ctrl { // !generate-accessors:read=generated,write=none !generate-python:alias=Ctrl struct list_node entry; struct list_head paths; struct list_head namespaces; @@ -314,7 +314,7 @@ struct libnvme_ctrl { // !generate-accessors:read=generated,write=none struct libnvme_fabrics_config cfg; }; -struct libnvme_subsystem { // !generate-accessors:read=generated,write=none +struct libnvme_subsystem { // !generate-accessors:read=generated,write=none !generate-python:alias=Subsystem struct list_node entry; struct list_head ctrls; struct list_head namespaces; @@ -331,22 +331,22 @@ struct libnvme_subsystem { // !generate-accessors:read=generated,write=none char *iopolicy; // !access:read=custom }; -struct libnvme_host { // !generate-accessors +struct libnvme_host { // !generate-accessors:read=generated,write=none !generate-python:alias=Host struct list_node entry; struct list_head subsystems; struct libnvme_global_ctx *ctx; - char *hostnqn; // !access:read=generated,write=none - char *hostid; // !access:read=generated,write=none - char *dhchap_host_key; - char *hostsymname; + char *hostnqn; + char *hostid; + char *dhchap_host_key; // !access:write=generated + char *hostsymname; // !access:write=generated /* pdc_enabled and pdc_enabled_valid work together. pdc_enabled_valid, * when true, indicates that pdc_enabled has been explicitly defined. * pdc_enabled_valid is internal meta-data for pdc_enabled. */ bool pdc_enabled; // !access:read=none,write=custom - bool pdc_enabled_valid; // !access:read=none,write=none + bool pdc_enabled_valid; // !access:read=none }; struct libnvme_fabric_options { // !generate-accessors @@ -388,7 +388,7 @@ enum libnvme_io_uring_state { LIBNVME_IO_URING_STATE_AVAILABLE, }; -struct libnvme_global_ctx { +struct libnvme_global_ctx { // !generate-python:alias=GlobalCtx char *config_file; char *application; struct list_head endpoints; /* MI endpoints */ diff --git a/libnvme/tools/generator/generate-accessors.md b/libnvme/tools/generator/generate-accessors.md index 06677b9330..5b69e5d3a1 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 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**. +This tool generates **setter and getter functions** for C structs automatically. It also optionally generates a **constructor and destructor** (`foo_new` / `foo_free`) and a **defaults initialiser** (`foo_init_defaults`). It supports dynamic strings, fixed-size char arrays, `char **` arrays, and `const` fields, with control over which structs and members participate via **in-source annotations**. ------ @@ -25,7 +25,16 @@ 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 `: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 and member behavior are controlled by **annotations written as `//` line comments directly in the header file**. Each annotation begins with a `!keyword` token. Optional parameters — called **metadata** — follow the keyword after a colon (`:`) and carry additional configuration. Multiple annotations may share one comment, separated by spaces. + +``` +// !keyword annotation with no metadata +// !keyword:key=value annotation with metadata key=value +// !keyword:key1=value1,key2=value2 annotation with metadata key1=value1,key2=value2 +// !keyword1 !keyword2:key=value two annotations on one line +``` + +The canonical form is `// !token` (one space between `//` and `!`); `//!token` and `//\t!token` are also accepted. ### Access model — two independent axes @@ -46,68 +55,56 @@ Only `generated` produces output in this generator. The `custom` and `none` mode ### Struct inclusion — `generate-accessors` -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: +Place the annotation on the same line as the struct's opening brace to opt that struct in to code generation. The optional metadata sets the **default mode for each axis** of every member of the struct: ```c -struct nvme_ctrl { // !generate-accessors +struct libnvme_ctrl { // !generate-accessors /* shorthand for read=generated, write=generated */ }; -struct nvme_ctrl { // !generate-accessors:read=generated,write=generated +struct libnvme_ctrl { // !generate-accessors:read=generated,write=generated /* fully explicit form of the same default */ }; -struct nvme_ctrl { // !generate-accessors:read=none,write=none - /* struct is included, but every member is opaque by default */ -}; - -struct nvme_ctrl { // !generate-accessors:read=generated - /* partial spec: write inherits the built-in default (generated) */ +struct libnvme_ctrl { // !generate-accessors:read=generated,write=none + /* all members read-only by default; setters require a per-member override */ }; ``` Only structs carrying this annotation are processed. All other structs in the header are ignored. -**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`. +**Built-in defaults.** Any axis not named in the metadata 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). ### 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: +Place the annotation on a member's declaration line to override the struct-level default for this field: ```c -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 +struct libnvme_ctrl { // !generate-accessors:read=generated,write=none + char *name; /* effective: read=generated, write=none (inherited) */ + char *dhchap_host_key; // !access:write=generated + /* effective: read=generated (inherited), write=generated */ + long command_error_count; // !access:read=custom + /* effective: read=custom, write=none (inherited) — hand-written getter */ }; ``` -**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 /* 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 */ -}; -``` +**Partial metadata** is allowed — any axis not named in the metadata is **inherited from the struct-level default**, which is in turn drawn from the struct's `// !generate-accessors` annotation (or from the built-in default when none is given). The order of `read` and `write` in the metadata is not significant. **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 | +| Member annotation | 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 @@ -115,39 +112,30 @@ A `const`-qualified member forces `write=none` regardless of what the annotation ### 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: +Place the annotation on the same line as the struct's opening brace to generate a constructor and a destructor for that struct. This annotation takes **no metadata** — its presence means "generate," its absence means "don't": ```c -struct nvme_ctrl { // !generate-lifecycle - char *name; - char *subsysnqn; - char *serial; // !lifecycle:none /* excluded from destructor */ +struct libnvmf_uri { // !generate-accessors !generate-lifecycle + char *scheme; + char *host; + char *path; // !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. +- **`libnvmf_uri_new(struct libnvmf_uri **pp)`** — allocates a zeroed instance on the heap. Returns `0` on success, `-EINVAL` if `pp` is `NULL`, or `-ENOMEM` on allocation failure. +- **`libnvmf_uri_free(struct libnvmf_uri *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. +`generate-lifecycle` can appear alongside `generate-accessors` in the same comment. `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; +struct libnvmf_uri { // !generate-lifecycle + char *host; char *borrowed; // !lifecycle:none /* not freed — caller owns this */ }; ``` @@ -165,7 +153,7 @@ struct libnvmf_discovery_args { // !generate-accessors !generate-lifecycle }; ``` -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. +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. The value is emitted verbatim, so any valid C expression — integer literals, macro names, enum constants — is accepted. @@ -177,10 +165,10 @@ The value is emitted verbatim, so any valid C expression — integer literals, m | ------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------- | | `// !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 | +| `// !generate-accessors:read=M` | struct brace | Partial metadata; other axis inherits the built-in default (`generated`) | +| `// !generate-lifecycle` | struct brace | Generate constructor + destructor (no metadata) | | `// !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 | +| `// !access:read=M` | member line | Partial metadata; 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 | @@ -191,148 +179,122 @@ In the table above, `M` is one of `generated`, `custom`, or `none`. ## Example -### Header file (`person.h`) +The following example is based on `struct libnvmf_uri` from +`libnvme/src/nvme/private-fabrics.h`. The struct as defined in that file carries +only `// !generate-accessors`; `!generate-lifecycle` and `// !default:4420` are +added here to illustrate all features in one place. -```c -struct person { // !generate-accessors - char *name; - int age; - const char *id; /* const → getter only, no annotation needed */ - char *secret; // !access:read=none,write=none - char *role; // !access:write=none /* read inherits: generated */ -}; +### Annotated header -struct car { // !generate-accessors - char *model; - int year; - const char *vin; +```c +/* Based on libnvme/src/nvme/private-fabrics.h */ +struct libnvmf_uri { // !generate-accessors !generate-lifecycle + char *scheme; + char *protocol; + char *userinfo; + char *host; + int port; // !default:4420 + char **path_segments; + char *query; + char *fragment; // !access:write=none }; ``` +What each member demonstrates: + +- `scheme`, `protocol`, `userinfo`, `host`, `query` — `char *` with both getter and setter; the setter stores a `strdup()` copy and frees the old value. +- `port` — scalar `int` with `!default:4420`; triggers generation of `libnvmf_uri_init_defaults()`. +- `path_segments` — `char **` NULL-terminated string array; setter deep-copies all elements; destructor frees each element and the container. +- `fragment` — `!access:write=none` yields a getter only; no setter is emitted. +- `!generate-lifecycle` — adds `libnvmf_uri_new()` and `libnvmf_uri_free()`; the constructor calls `init_defaults()` automatically. + ### Command ``` -python3 generate-accessors.py person.h +python3 generate-accessors.py \ + --h-out src/nvme/accessors-fabrics.h \ + --c-out src/nvme/accessors-fabrics.c \ + --ld-out src/accessors-fabrics.ld \ + src/nvme/private-fabrics.h ``` -### Generated `accessors.h` +### Generated `accessors-fabrics.h` ```c /* SPDX-License-Identifier: LGPL-2.1-or-later */ /* ... banner ... */ -#ifndef _ACCESSORS_H_ -#define _ACCESSORS_H_ - -#include -#include -#include -#include +#ifndef _ACCESSORS_FABRICS_H_ +#define _ACCESSORS_FABRICS_H_ -#include +/* ... standard includes ... */ /* Forward declarations. These are internal (opaque) structs. */ -struct person; -struct car; +struct libnvmf_uri; /**************************************************************************** - * Accessors for: struct person + * Accessors for: struct libnvmf_uri ****************************************************************************/ /** - * person_set_name() - Set name. - * @p: The &struct person instance to update. - * @name: New string; a copy is stored. Pass NULL to clear. - */ -void person_set_name(struct person *p, const char *name); - -/** - * person_get_name() - Get name. - * @p: The &struct person instance to query. + * libnvmf_uri_new() - Allocate and initialise a libnvmf_uri object. + * @pp: On success, *pp is set to the newly allocated object. * - * Return: The value of the name field, or NULL if not set. + * Allocates a zeroed &struct libnvmf_uri on the heap. + * The caller must release it with libnvmf_uri_free(). + * + * Return: 0 on success, -EINVAL if @pp is NULL, + * -ENOMEM if allocation fails. */ -const char *person_get_name(const struct person *p); +int libnvmf_uri_new(struct libnvmf_uri **pp); /** - * person_set_age() - Set age. - * @p: The &struct person instance to update. - * @age: Value to assign to the age field. + * libnvmf_uri_free() - Release a libnvmf_uri object. + * @p: Object previously returned by libnvmf_uri_new(). + * A NULL pointer is silently ignored. */ -void person_set_age(struct person *p, int age); +void libnvmf_uri_free(struct libnvmf_uri *p); /** - * person_get_age() - Get age. - * @p: The &struct person instance to query. + * libnvmf_uri_init_defaults() - Apply default values to a libnvmf_uri instance. + * @p: The &struct libnvmf_uri instance to initialise. * - * Return: The value of the age field. + * Sets each field that carries a !default annotation to its compile-time + * default value. Called automatically by libnvmf_uri_new() but may also be + * called directly to reset an instance to its defaults. */ -int person_get_age(const struct person *p); +void libnvmf_uri_init_defaults(struct libnvmf_uri *p); -/** - * person_get_id() - Get id. - * @p: The &struct person instance to query. - * - * Return: The value of the id field, or NULL if not set. - */ -const char *person_get_id(const struct person *p); +void libnvmf_uri_set_scheme(struct libnvmf_uri *p, const char *scheme); +const char *libnvmf_uri_get_scheme(const struct libnvmf_uri *p); -/** - * person_get_role() - Get role. - * @p: The &struct person instance to query. - * - * Return: The value of the role field, or NULL if not set. - */ -const char *person_get_role(const struct person *p); +void libnvmf_uri_set_protocol(struct libnvmf_uri *p, const char *protocol); +const char *libnvmf_uri_get_protocol(const struct libnvmf_uri *p); -/**************************************************************************** - * Accessors for: struct car - ****************************************************************************/ +void libnvmf_uri_set_userinfo(struct libnvmf_uri *p, const char *userinfo); +const char *libnvmf_uri_get_userinfo(const struct libnvmf_uri *p); -/** - * car_set_model() - Set model. - * @p: The &struct car instance to update. - * @model: New string; a copy is stored. Pass NULL to clear. - */ -void car_set_model(struct car *p, const char *model); +void libnvmf_uri_set_host(struct libnvmf_uri *p, const char *host); +const char *libnvmf_uri_get_host(const struct libnvmf_uri *p); -/** - * car_get_model() - Get model. - * @p: The &struct car instance to query. - * - * Return: The value of the model field, or NULL if not set. - */ -const char *car_get_model(const struct car *p); +void libnvmf_uri_set_port(struct libnvmf_uri *p, int port); +int libnvmf_uri_get_port(const struct libnvmf_uri *p); -/** - * car_set_year() - Set year. - * @p: The &struct car instance to update. - * @year: Value to assign to the year field. - */ -void car_set_year(struct car *p, int year); +void libnvmf_uri_set_path_segments(struct libnvmf_uri *p, + const char *const *path_segments); +const char *const *libnvmf_uri_get_path_segments( + const struct libnvmf_uri *p); -/** - * car_get_year() - Get year. - * @p: The &struct car instance to query. - * - * Return: The value of the year field. - */ -int car_get_year(const struct car *p); +void libnvmf_uri_set_query(struct libnvmf_uri *p, const char *query); +const char *libnvmf_uri_get_query(const struct libnvmf_uri *p); -/** - * car_get_vin() - Get vin. - * @p: The &struct car instance to query. - * - * Return: The value of the vin field, or NULL if not set. - */ -const char *car_get_vin(const struct car *p); +/* fragment: getter only — !access:write=none suppresses the setter */ +const char *libnvmf_uri_get_fragment(const struct libnvmf_uri *p); -#endif /* _ACCESSORS_H_ */ +#endif /* _ACCESSORS_FABRICS_H_ */ ``` -> **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` +### Generated `accessors-fabrics.c` ```c // SPDX-License-Identifier: LGPL-2.1-or-later @@ -340,271 +302,154 @@ const char *car_get_vin(const struct car *p); #include #include -#include "accessors.h" - -#include "person.h" +#include "accessors-fabrics.h" +#include "private-fabrics.h" #include "compiler-attributes.h" /**************************************************************************** - * Accessors for: struct person + * Accessors for: struct libnvmf_uri ****************************************************************************/ -__public void person_set_name(struct person *p, const char *name) +__public int libnvmf_uri_new(struct libnvmf_uri **pp) { - free(p->name); - p->name = name ? strdup(name) : NULL; -} - -__public const char *person_get_name(const struct person *p) -{ - return p->name; + if (!pp) + return -EINVAL; + *pp = calloc(1, sizeof(struct libnvmf_uri)); + if (!*pp) + return -ENOMEM; + libnvmf_uri_init_defaults(*pp); + return 0; } -__public void person_set_age(struct person *p, int age) +__public void libnvmf_uri_free(struct libnvmf_uri *p) { - p->age = age; + if (!p) + return; + free(p->scheme); + free(p->protocol); + free(p->userinfo); + free(p->host); + if (p->path_segments) { + size_t i; + for (i = 0; p->path_segments[i]; i++) + free(p->path_segments[i]); + free(p->path_segments); + } + free(p->query); + free(p->fragment); + free(p); } -__public int person_get_age(const struct person *p) +__public void libnvmf_uri_init_defaults(struct libnvmf_uri *p) { - return p->age; + if (!p) + return; + p->port = 4420; } -__public const char *person_get_id(const struct person *p) +__public void libnvmf_uri_set_scheme(struct libnvmf_uri *p, + const char *scheme) { - return p->id; + free(p->scheme); + p->scheme = scheme ? strdup(scheme) : NULL; } -__public const char *person_get_role(const struct person *p) +__public const char *libnvmf_uri_get_scheme(const struct libnvmf_uri *p) { - return p->role; + return p->scheme; } -/**************************************************************************** - * Accessors for: struct car - ****************************************************************************/ +/* ... similar implementations for protocol, userinfo, host, query ... */ -__public void car_set_model(struct car *p, const char *model) +__public void libnvmf_uri_set_port(struct libnvmf_uri *p, int port) { - free(p->model); - p->model = model ? strdup(model) : NULL; + p->port = port; } -__public const char *car_get_model(const struct car *p) +__public int libnvmf_uri_get_port(const struct libnvmf_uri *p) { - return p->model; + return p->port; } -__public void car_set_year(struct car *p, int year) +__public void libnvmf_uri_set_path_segments(struct libnvmf_uri *p, + const char *const *path_segments) { - p->year = year; -} + char **new_array = NULL; + size_t i; + + if (path_segments) { + for (i = 0; path_segments[i]; i++) + ; + + new_array = calloc(i + 1, sizeof(char *)); + if (new_array != NULL) { + for (i = 0; path_segments[i]; i++) { + new_array[i] = strdup(path_segments[i]); + if (!new_array[i]) { + while (i > 0) + free(new_array[--i]); + free(new_array); + new_array = NULL; + break; + } + } + } + } -__public int car_get_year(const struct car *p) -{ - return p->year; + for (i = 0; p->path_segments && p->path_segments[i]; i++) + free(p->path_segments[i]); + free(p->path_segments); + p->path_segments = new_array; } -__public const char *car_get_vin(const struct car *p) +__public const char *const *libnvmf_uri_get_path_segments( + const struct libnvmf_uri *p) { - return p->vin; + return (const char *const *)p->path_segments; } -``` - -### Generated `accessors.ld` - -``` -# SPDX-License-Identifier: LGPL-2.1-or-later -/* ... banner ... */ - -LIBNVME_ACCESSORS_3 { - global: - person_get_name; - person_set_name; - person_get_age; - person_set_age; - person_get_id; - person_get_role; - car_get_model; - car_set_model; - car_get_year; - car_set_year; - car_get_vin; -}; -``` -> **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. - ------- - -## 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; // !access:read=none,write=none - char *role; // !access:write=none /* read inherits: generated */ -}; -``` - -### 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) +__public const char *libnvmf_uri_get_fragment(const struct libnvmf_uri *p) { - 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); + return p->fragment; } ``` > **Notes:** -> - `id` is `const char *` — the destructor never frees `const` members. -> - `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. +> - `fragment` has no setter — `!access:write=none` suppresses it — but the destructor still frees it because `!lifecycle:none` was not set. +> - `path_segments` (`char **`) setter: builds the new deep-copy first, then frees the old array. This ensures the struct is always in a valid state even if `strdup` fails partway through. +> - `port` (`int`) receives a direct assignment in `init_defaults()`. For `char *` members with a string default, `init_defaults()` uses a compare-before-replace pattern: if the current value already matches the default (`strcmp`), nothing happens; otherwise the old value is freed and the default is `strdup()`'d. +> - The constructor calls `init_defaults()` after `calloc()`, so freshly allocated objects start at their defined defaults rather than zero. -### Additional entries in `accessors.ld` +### Generated `accessors-fabrics.ld` ``` - person_new; - person_free; -``` - ------- - -## Defaults example +# SPDX-License-Identifier: LGPL-2.1-or-later +/* ... banner ... */ -```c -struct conn_opts { // !generate-accessors !generate-lifecycle - int port; // !default:4420 - char *transport; // !default:"tcp" - const char *trsvcid; // !default:"4420" +LIBNVMF_ACCESSORS_3 { + global: + libnvmf_uri_new; + libnvmf_uri_free; + libnvmf_uri_init_defaults; + libnvmf_uri_get_scheme; + libnvmf_uri_set_scheme; + libnvmf_uri_get_protocol; + libnvmf_uri_set_protocol; + libnvmf_uri_get_userinfo; + libnvmf_uri_set_userinfo; + libnvmf_uri_get_host; + libnvmf_uri_set_host; + libnvmf_uri_get_port; + libnvmf_uri_set_port; + libnvmf_uri_get_path_segments; + libnvmf_uri_set_path_segments; + libnvmf_uri_get_query; + libnvmf_uri_set_query; + libnvmf_uri_get_fragment; }; ``` -### 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; -``` +> **Note:** `fragment` has no `set` entry because `!access:write=none` suppresses the setter. The version node name (`LIBNVMF_ACCESSORS_3`) is assigned by the maintainer in the `.ld` file; the generator reports symbol drift but does not overwrite it. ------ @@ -613,25 +458,3 @@ __public void conn_opts_free(struct conn_opts *p) - `typedef struct` is not supported. - Nested structs (a `struct` member whose type is also a `struct`) are skipped. - Only `char *` and `char **` pointer members are supported; other pointer types are skipped. - ------- - -## Notes - -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** — 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. -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 9c8f47c976..398447b69d 100755 --- a/libnvme/tools/generator/generate-accessors.py +++ b/libnvme/tools/generator/generate-accessors.py @@ -17,7 +17,8 @@ - Does not support struct within struct. Annotations use // line-comment style. After '//', each '!keyword' token -(optionally followed by ':spec' or ':VALUE') is a command. Multiple +(optionally followed by ':metadata') is a command. The ':metadata' portion +carries extra parameters such as 'read=generated,write=none'. Multiple annotations can appear in one comment: struct nvme_ctrl { // !generate-accessors !generate-lifecycle @@ -121,6 +122,7 @@ SPDX_C = "// SPDX-License-Identifier: LGPL-2.1-or-later" SPDX_H = "/* SPDX-License-Identifier: LGPL-2.1-or-later */" +SPDX_I = SPDX_C SPDX_LD = "# SPDX-License-Identifier: LGPL-2.1-or-later" BANNER = ( @@ -317,17 +319,21 @@ class Member: '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. + Only 'generated' produces C accessor output in this generator. Members + with at least one non-'none' axis are retained for the SWIG emitter. + + py_visible: False when annotated with ``// !python:none``. + py_alias: alternate Python attribute name from ``// !python:alias=NAME``, + or None to use the C member name. """ __slots__ = ('name', 'type', 'read_mode', 'write_mode', 'is_char_array', 'is_char_ptr_array', 'is_scalar_array', - 'array_size') + 'array_size', 'py_visible', 'py_alias') def __init__(self, name, type_str, read_mode, write_mode, - is_char_array, is_char_ptr_array, is_scalar_array, array_size): + is_char_array, is_char_ptr_array, is_scalar_array, array_size, + py_visible=True, py_alias=None): self.name = name self.type = type_str # e.g. "const char *", "int", "__u32" self.read_mode = read_mode # 'generated' | 'custom' | 'none' @@ -336,6 +342,25 @@ def __init__(self, name, type_str, read_mode, write_mode, self.is_char_ptr_array = is_char_ptr_array self.is_scalar_array = is_scalar_array self.array_size = array_size # for fixed-size arrays (char[N] or type[N]) + self.py_visible = py_visible # False → excluded from SWIG fragment + self.py_alias = py_alias # str → rename Python attribute; None → use C name + + @property + def has_accessor(self): + """True when at least one axis has a real accessor (generated or custom).""" + return self.read_mode != 'none' or self.write_mode != 'none' + + @property + def is_custom_accessor(self): + return self.read_mode == 'custom' or self.write_mode == 'custom' + + @property + def gen_getter(self): + return self.read_mode == 'generated' + + @property + def gen_setter(self): + return self.write_mode == 'generated' # --------------------------------------------------------------------------- @@ -352,9 +377,13 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): 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. + Members where both axes are 'none' are dropped entirely. Members with + at least one non-'none' axis are retained so the SWIG fragment emitter + can see 'custom' axes alongside 'generated' ones. + + Per-member Python hints: + ``// !python:none`` → Member.py_visible = False (exclude from SWIG) + ``// !python:alias=NAME`` → Member.py_alias = 'NAME' (rename in Python) Annotations are detected on the **raw** (un-stripped) line so that comment masking cannot hide them. Comments are stripped only afterwards, @@ -363,6 +392,9 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): struct_read, struct_write = struct_defaults members = [] + _py_none_re = re.compile(r'!python:none(?=[\s!]|$)') + _py_alias_re = re.compile(r'!python:alias=(\w+)(?=[\s!]|$)') + for raw_line in raw_body.splitlines(): # ---------------------------------------------------------------- # Annotation checks on the raw line — BEFORE stripping comments. @@ -375,12 +407,17 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): read_mode = override.get('read', struct_read) write_mode = override.get('write', struct_write) - # 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': + # Retain members that have any accessor (generated or custom) so the + # SWIG emitter can see them. Only drop members with no accessor at + # all on either axis. + if read_mode == 'none' and write_mode == 'none': continue + comment = _comment_text(raw_line) or '' + py_visible = _py_none_re.search(comment) is None + m_alias = _py_alias_re.search(comment) + py_alias = m_alias.group(1) if m_alias else None + # ---------------------------------------------------------------- # Strip comments for member-declaration parsing. # ---------------------------------------------------------------- @@ -406,6 +443,8 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): is_char_ptr_array=False, is_scalar_array=False, array_size=m.group(3), + py_visible=py_visible, + py_alias=py_alias, )) continue @@ -422,6 +461,8 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): is_char_ptr_array=False, is_scalar_array=True, array_size=m.group(4), + py_visible=py_visible, + py_alias=py_alias, )) continue @@ -455,6 +496,8 @@ def parse_members(struct_name, raw_body, struct_defaults, verbose): is_char_ptr_array=(ptr_depth == 2), is_scalar_array=False, array_size=None, + py_visible=py_visible, + py_alias=py_alias, )) return members @@ -558,17 +601,42 @@ def parse_access_override(raw_line): def parse_lifecycle_annotation(raw_body): - """Return True when *raw_body* carries a ``!generate-lifecycle`` annotation. + """Return True if ``!generate-lifecycle`` is present, else None. - The annotation must appear inside a ``//`` comment on the struct's opening - brace line. It may share the comment with other annotations:: + Recognises (on the struct's opening brace line):: - struct foo { // !generate-accessors !generate-lifecycle + // !generate-lifecycle → True (emit constructor + destructor) + + Returns None when the annotation is absent. """ + _lc_re = re.compile(r'!generate-lifecycle(?=[\s!]|$)') for line in raw_body.splitlines(): - if has_annotation(line, 'generate-lifecycle'): + comment = _comment_text(line) + if comment is None: + continue + if _lc_re.search(comment): return True - return False + return None + + +_GEN_PYTHON_RE = re.compile(r'!generate-python(?::alias=(\w+))?(?=[\s!]|$)') + + +def parse_generate_python(raw_body): + """Return ``(emit_py, alias)`` from a ``// !generate-python[:alias=NAME]`` annotation. + + *emit_py* is True when ``!generate-python`` is present on any line of + *raw_body*. *alias* is the NAME string from ``:alias=NAME``, or None + when the option is absent. + """ + for line in raw_body.splitlines(): + comment = _comment_text(line) + if comment is None: + continue + m = _GEN_PYTHON_RE.search(comment) + if m: + return True, m.group(1) + return False, None # --------------------------------------------------------------------------- @@ -720,11 +788,13 @@ def parse_members_for_defaults(raw_body): def parse_file(text, verbose): """Return list of (struct_name, [Member], [LifecycleMember], - [DefaultMember]) tuples. + [DefaultMember], emit_py_fragment) tuples. 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. + + *emit_py_fragment* is True when the struct also carries ``!generate-python``. """ result = [] @@ -732,38 +802,50 @@ def parse_file(text, verbose): struct_name = match.group(1) raw_body = match.group(2) - struct_defaults = parse_struct_annotation(raw_body) - want_lifecycle = parse_lifecycle_annotation(raw_body) + struct_defaults = parse_struct_annotation(raw_body) + lifecycle_mode = parse_lifecycle_annotation(raw_body) + emit_py_fragment, struct_alias = parse_generate_python(raw_body) - if struct_defaults is None and not want_lifecycle: + if struct_defaults is None and lifecycle_mode is None and not emit_py_fragment: continue + # If the struct has !generate-python but no !generate-accessors, parse + # members with default mode (none, none) so the SWIG emitter can see + # any explicit !access: overrides on individual members. members = [] - if struct_defaults is not None: + acc_defaults = struct_defaults if struct_defaults is not None else ('none', 'none') + if struct_defaults is not None or emit_py_fragment: members = parse_members( - struct_name, raw_body, struct_defaults, verbose) + struct_name, raw_body, acc_defaults, verbose) lc_members = None - if want_lifecycle: + if lifecycle_mode: lc_members = parse_members_for_lifecycle(raw_body) default_members = parse_members_for_defaults(raw_body) - if verbose and (members or lc_members is not None or default_members): - if members: + if verbose and (members or lc_members is not None or default_members + or emit_py_fragment): + if struct_defaults is not None and members: sr, sw = struct_defaults acc = (f"{len(members)} members " f"[defaults: read={sr}, write={sw}]") - else: + elif struct_defaults is not None: acc = "no accessors" - lc = (f"{len(lc_members)} lifecycle members" - if lc_members is not None else "no lifecycle") + else: + acc = "no accessors (python-only)" + if lifecycle_mode: + lc = f"{len(lc_members)} lifecycle members" + else: + lc = "no lifecycle" df = (f"{len(default_members)} defaults" if default_members else "no defaults") - print(f"Found struct: {struct_name} — {acc}, {lc}, {df}") + py = "generate-python" if emit_py_fragment else "no python" + print(f"Found struct: {struct_name} — {acc}, {lc}, {df}, {py}") - if members or lc_members is not None or default_members: - result.append((struct_name, members, lc_members, default_members)) + if members or lc_members is not None or default_members or emit_py_fragment: + result.append((struct_name, members, lc_members, default_members, + emit_py_fragment, struct_alias)) return result @@ -1344,6 +1426,165 @@ def generate_ld(f, prefix, struct_name, members, lc_members, default_members): f.write(f'\t\t{_set_name(prefix, struct_name, member.name)};\n') +# --------------------------------------------------------------------------- +# SWIG fragment emitters +# --------------------------------------------------------------------------- + +def generate_swig_prelude(f): + """Emit the shared ``_nvme_guarded_setattr`` helper. + + Written once at the top of the first (common) generated fragment. + The fabrics fragment imports it from the common module at runtime. + """ + f.write( + '%pythoncode %{\n' + 'def _nvme_guarded_setattr(self, name, value):\n' + ' """Reject writes to unknown attributes.\n\n' + ' Typos like ``ctrl.nqn = x`` (should be ``ctrl.subsysnqn``) are\n' + ' silently ignored by default Python ``__setattr__``. This guard\n' + ' raises ``AttributeError`` for any name not already present on the\n' + ' object, keeping the struct-like API strict.\n' + ' """\n' + ' if name.startswith(\'_\') or name in (\'this\', \'thisown\') or hasattr(type(self), name):\n' + ' object.__setattr__(self, name, value)\n' + ' else:\n' + ' raise AttributeError(\n' + ' f"{type(self).__name__!r} has no attribute {name!r}")\n' + '%}\n\n' + ) + + +def generate_swig_fragment(f, prefix, struct_name, members, errors, + struct_alias=None): + """Emit SWIG struct decl with per-axis read/write routing. + + Access routing per member: + + is_custom_accessor (read==custom OR write==custom) + → member goes inside ``%extend {}`` so SWIG calls the hand-written + accessor function. A ``%rename`` directive maps the function to + SWIG's expected ``prefix_name_get`` / ``prefix_name_set`` name. + + all-generated (neither axis is custom, at least one is non-none) + → plain struct field declaration (outside ``%extend``); SWIG reads/ + writes the field directly via ``p->member``. + + write == none (on either kind) + → ``%immutable name;`` immediately before the field declaration + makes the attribute read-only. + + both axes == none → member is not Python-visible; already excluded + by the ``has_accessor`` filter. + + SWIG does not support mixed mechanisms (direct read + accessor write) on + a single member, so any ``custom`` axis forces the whole member into + ``%extend``. + + ``%extend {}`` is omitted entirely when no member needs it. + + Struct-level naming: + struct_alias=NAME → emits ``%rename(NAME) struct_name;`` before the + struct body, and uses NAME in ``%pythoncode``. + struct_alias=None → no struct-level ``%rename``; C struct name used + everywhere. + + Invariant: the number of ``%rename`` directives emitted equals the number + of ``custom`` axes among Python-visible members, plus one when struct_alias + is set. + """ + py_class = struct_alias or struct_name + pre = f'{prefix}{struct_name}' + f.write(f'/* struct {struct_name} */\n') + if struct_alias: + f.write(f'%rename({struct_alias}) {struct_name};\n') + + # Collect Python-visible members (has accessor AND py_visible flag set). + visible = [m for m in members if m.py_visible and m.has_accessor] + + # Collision detection — report all collisions before emitting anything. + seen = {} + for m in visible: + py_name = m.py_alias or m.name + if py_name in seen: + errors.append( + f"error: struct {struct_name}: Python name '{py_name}' " + f"is used by both '{seen[py_name]}' and '{m.name}'") + else: + seen[py_name] = m.name + + # Pass 1 — %rename directives. Emitted ONLY for 'custom' axes. + for m in visible: + if not m.is_custom_accessor: + continue + py_name = m.py_alias or m.name + if m.read_mode == 'custom': + f.write(f'%rename({pre}_{py_name}_get) {pre}_get_{m.name};\n') + if m.write_mode == 'custom': + f.write(f'%rename({pre}_{py_name}_set) {pre}_set_{m.name};\n') + + # Pass 1.5 — #define bridges for custom members. + # SWIG generates wrapper code that calls __get/set (SWIG + # naming convention), but the hand-written C accessors follow the libnvme + # convention _get/set_. A #define makes the generated C + # code compile without requiring the hand-written functions to be renamed. + bridges = [] + for m in visible: + if not m.is_custom_accessor: + continue + py_name = m.py_alias or m.name + if m.read_mode == 'custom': + bridges.append( + f'#define {pre}_{py_name}_get {pre}_get_{m.name}') + if m.write_mode == 'custom': + bridges.append( + f'#define {pre}_{py_name}_set {pre}_set_{m.name}') + if bridges: + f.write('%{\n') + for bridge in bridges: + f.write(f'\t{bridge}\n') + f.write('%}\n') + + # Pass 2 — struct body. + f.write(f'struct {struct_name} {{\n') + + # Generated members: plain struct fields — SWIG accesses p->member directly. + for m in visible: + if m.is_custom_accessor: + continue + py_name = m.py_alias or m.name + if m.write_mode == 'none': + f.write(f'\t%immutable {py_name};\n') + if m.is_scalar_array: + f.write(f'\t{m.type} {py_name}[{m.array_size}];\n') + else: + f.write(f'\t{m.type} {py_name};\n') + + # Custom members: inside %extend — SWIG calls the hand-written accessor. + # Members with read=none are excluded: SWIG always generates a getter for + # %extend members, and there is no C function to call for a read=none axis. + custom = [m for m in visible if m.is_custom_accessor and m.read_mode != 'none'] + if custom: + f.write('\t%extend {\n') + for m in custom: + py_name = m.py_alias or m.name + if m.write_mode == 'none': + f.write(f'\t\t%immutable {py_name};\n') + if m.is_scalar_array: + f.write(f'\t\t{m.type} {py_name}[{m.array_size}];\n') + else: + f.write(f'\t\t{m.type} {py_name};\n') + f.write('\t}\n') + + f.write('};\n\n') + + # Install __setattr__ guard at module import time. + f.write( + f'%pythoncode %{{\n' + f'{py_class}.__setattr__ = _nvme_guarded_setattr\n' + f'%}}\n\n' + ) + + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -1362,6 +1603,9 @@ def main(): parser.add_argument('-l', '--ld-out', default='accessors.ld', dest='l_fname', metavar='FILE', help='Generated *.ld file. Default: accessors.ld') + parser.add_argument('-s', '--swig-out', default=None, + dest='s_fname', metavar='FILE', + help='Generated SWIG fragment (*.i). Omit to skip.') parser.add_argument('-p', '--prefix', default='', dest='prefix', metavar='STR', help='Prefix prepended to every generated function name.') @@ -1395,6 +1639,12 @@ def main(): hdr_parts = [] # fragments for accessors.h src_parts = [] # fragments for accessors.c ld_parts = [] # fragments for accessors.ld + swig_parts = [] # fragments for accessors.i (if --swig-out given) + swig_errors = [] # deferred SWIG validation errors + swig_aliases = set() # alias names seen so far — uniqueness guard + + emit_swig = args.s_fname is not None + first_swig = True # prelude emitted once, before first struct for in_hdr in header_files: if args.verbose: @@ -1416,42 +1666,73 @@ def main(): files_to_include.append(os.path.basename(in_hdr)) - for struct_name, members, lc_members, default_members in structs: - forward_declares.append(struct_name) + for struct_name, members, lc_members, default_members, emit_py, struct_alias in structs: + has_c_output = bool(members or lc_members is not None + or default_members) - section_banner = ( - f'/****************************************************************************\n' - f' * Accessors for: struct {struct_name}\n' - f' ****************************************************************************/\n' - f'\n' - ) + if has_c_output: + forward_declares.append(struct_name) + + section_banner = ( + f'/****************************************************************************\n' + f' * Accessors for: struct {struct_name}\n' + f' ****************************************************************************/\n' + f'\n' + ) - 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, - lc_members, default_members) - ld_parts.append(ld_buf.getvalue()) + 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, + lc_members, default_members) + ld_parts.append(ld_buf.getvalue()) + + if emit_swig and emit_py: + if struct_alias is not None: + if not struct_alias.isidentifier(): + swig_errors.append( + f"error: struct {struct_name}: " + f"alias={struct_alias!r} is not a valid " + f"Python identifier") + elif struct_alias in swig_aliases: + swig_errors.append( + f"error: struct {struct_name}: " + f"alias={struct_alias!r} is already used by " + f"another struct") + else: + swig_aliases.add(struct_alias) + swig_buf = io.StringIO() + if first_swig: + generate_swig_prelude(swig_buf) + first_swig = False + generate_swig_fragment(swig_buf, args.prefix, struct_name, + members, swig_errors, struct_alias) + swig_parts.append(swig_buf.getvalue()) + + if swig_errors: + for msg in swig_errors: + print(msg, file=sys.stderr) + sys.exit(1) # ----------------------------------------------------------------------- # Pass 2 — write output files. @@ -1517,8 +1798,21 @@ def main(): f.write(''.join(ld_parts)) f.write('};\n') + # --- accessors.i (SWIG fragment) --------------------------------------- + if emit_swig and args.s_fname: + makedirs_for(args.s_fname) + with open(args.s_fname, 'w') as f: + f.write( + f'{SPDX_I}\n' + f'\n' + f'{BANNER}\n' + ) + f.write(''.join(swig_parts)) + if args.verbose: print(f"\nGenerated {args.h_fname} and {args.c_fname}") + if emit_swig and args.s_fname: + print(f"Generated {args.s_fname}") if __name__ == '__main__': diff --git a/libnvme/tools/generator/meson.build b/libnvme/tools/generator/meson.build index dd126da672..45cedf4410 100644 --- a/libnvme/tools/generator/meson.build +++ b/libnvme/tools/generator/meson.build @@ -38,6 +38,7 @@ _tgt_common = run_target( libnvme_src_nvme / 'accessors.h', libnvme_src_nvme / 'accessors.c', libnvme_src / 'accessors.ld', + '--swig-out', libnvme_swig_dir / 'accessors.i', libnvme_src_nvme / 'private.h', ], ) @@ -51,20 +52,10 @@ _tgt_fabrics = run_target( libnvme_src_nvme / 'accessors-fabrics.h', libnvme_src_nvme / 'accessors-fabrics.c', libnvme_src / 'accessors-fabrics.ld', + '--swig-out', libnvme_swig_dir / 'accessors-fabrics.i', libnvme_src_nvme / 'private-fabrics.h', ], ) -_tgt_swig = run_target( - 'update-swig-accessors', - command: [ - _py3, - files('generate-swig-accessors.py'), - '--output', libnvme_swig_dir / 'nvme-swig-accessors.i', - libnvme_src_nvme / 'accessors.h', - libnvme_src_nvme / 'accessors-fabrics.h', - ], -) - # This alias allows generating all accessors in one shot. -alias_target('update-accessors', _tgt_common, _tgt_fabrics, _tgt_swig) +alias_target('update-accessors', _tgt_common, _tgt_fabrics) diff --git a/libnvme/tools/generator/update-accessors.sh b/libnvme/tools/generator/update-accessors.sh index d13182cb8b..b65f804a1d 100755 --- a/libnvme/tools/generator/update-accessors.sh +++ b/libnvme/tools/generator/update-accessors.sh @@ -19,13 +19,14 @@ # or removed so the maintainer knows exactly what to change. # # Arguments (supplied by the Meson run_target): -# $1 path to the python3 interpreter -# $2 path to generate-accessors.py -# $3 full path of the output .h file -# $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 +# $1 path to the python3 interpreter +# $2 path to generate-accessors.py +# $3 full path of the output .h file +# $4 full path of the output .c file +# $5 full path of the output .ld file +# [--swig-out F] optional: full path of the output SWIG fragment (.i file) +# $6 (or $8) ... one or more input headers scanned for +# // !generate-accessors and // !generate-python structs set -euo pipefail @@ -36,6 +37,12 @@ C_OUT="${4:?missing .c output path}" LD_OUT="${5:?missing .ld output path}" shift 5 +SWIG_OUT="" +if [ "${1-}" = "--swig-out" ]; then + SWIG_OUT="${2:?--swig-out requires a path argument}" + shift 2 +fi + if [ $# -eq 0 ]; then echo "error: no input headers provided" >&2 exit 1 @@ -50,6 +57,7 @@ BASE="${LABEL%.h}" # e.g. "accessors" or "nvmf-accessors" TMP_H="$TMPDIR_WORK/${BASE}.h" TMP_C="$TMPDIR_WORK/${BASE}.c" TMP_LD="$TMPDIR_WORK/${BASE}.ld" +TMP_I="$TMPDIR_WORK/${BASE}.i" # --------------------------------------------------------------------------- # Helper: update a source file atomically when content changes. @@ -115,15 +123,21 @@ check_ld_drift() { echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" echo "--- ${BASE}: begin generation ---" echo "" + +SWIG_ARGS=() +[ -n "$SWIG_OUT" ] && SWIG_ARGS=(--swig-out "$TMP_I") + "$PYTHON" "$GENERATOR" \ --h-out "$TMP_H" \ --c-out "$TMP_C" \ --ld-out "$TMP_LD" \ + "${SWIG_ARGS[@]}" \ "$@" CHANGED=0 update_if_changed "$TMP_H" "$H_OUT" update_if_changed "$TMP_C" "$C_OUT" +[ -n "$SWIG_OUT" ] && update_if_changed "$TMP_I" "$SWIG_OUT" echo "" if [ "$CHANGED" -gt 0 ]; then printf "%d file(s) updated in %s\n" "$CHANGED" "$(dirname "$H_OUT")" From bd5d69c1dacde50088f22b2ea264377ddcdae317 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Wed, 29 Apr 2026 16:57:44 -0400 Subject: [PATCH 2/2] =?UTF-8?q?python=20bindings:=20redesign=20Phase=202?= =?UTF-8?q?=20=E2=80=94=20API=20surface=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert connected() and is_registration_supported() to read-only Python properties (connected, registration_supported) - Delete set_symname(); host.hostsymname is already writable via the generated SWIG field descriptor - Rename registration_ctrl() to registration_control() to reflect that the method is a multi-action control interface (register, deregister, update via NVMF_DIM_TAS_*), not a single register operation - Update test-objects.py to match the new API Signed-off-by: Martin Belanger Assisted-by: Claude Sonnet --- libnvme/libnvme/nvme.i | 35 ++++++++++++--------------- libnvme/libnvme/tests/test-objects.py | 28 ++++++++++++++++++--- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index 22ad1a0496..ded7a1991e 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -887,12 +887,6 @@ struct libnvme_ns * libnvme_ctrl_next_ns(struct libnvme_ctrl * c, struct libnvme } -%define SET_SYMNAME_DOCSTRING -"Set or clear the host's symbolic name. - -Args: - hostsymname: Symbolic name string, or None to clear it." -%enddef %pythonappend libnvme_host::libnvme_host(struct libnvme_global_ctx *ctx, const char *hostnqn, @@ -938,11 +932,6 @@ Args: struct libnvme_host* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { return $self; } - %feature("autodoc", SET_SYMNAME_DOCSTRING) set_symname; - void set_symname(const char *hostsymname) { - libnvme_host_set_hostsymname($self, hostsymname); - } - PyObject* __str__() { return PyUnicode_FromFormat("nvme.Host(%s,%s)", STR_OR_NONE($self->hostnqn), STR_OR_NONE($self->hostid)); } @@ -1161,10 +1150,6 @@ Args: return; } } - %feature("autodoc", "Return True if this controller is currently connected.") connected; - bool connected() { - return libnvme_ctrl_get_name($self) != NULL; - } %feature("autodoc", "Rescan this controller and refresh its namespace list.") rescan; void rescan() { libnvme_rescan_ctrl($self); @@ -1190,16 +1175,28 @@ Args: connect_err = 2; } - %feature("autodoc", "Return True if this controller supports explicit host registration.") is_registration_supported; - bool is_registration_supported() { + bool _registration_supported() { return libnvmf_is_registration_supported($self); } + bool _connected() { + return libnvme_ctrl_get_name($self) != NULL; + } + %pythoncode %{ + @property + def connected(self): + """True if this controller is currently connected.""" + return self._connected() + @property + def registration_supported(self): + """True if this controller supports explicit host registration.""" + return self._registration_supported() + %} %feature("autodoc", "Register this controller with the NVMe-oF DIM service.\n" "\n" "Returns:\n" - " None on success, or an error string describing the failure.") registration_ctrl; - PyObject *registration_ctrl(enum nvmf_dim_tas tas) { + " None on success, or an error string describing the failure.") registration_control; + PyObject *registration_control(enum nvmf_dim_tas tas) { __u32 result; int status; diff --git a/libnvme/libnvme/tests/test-objects.py b/libnvme/libnvme/tests/test-objects.py index 3e0077b2ab..3cc1f85e3d 100644 --- a/libnvme/libnvme/tests/test-objects.py +++ b/libnvme/libnvme/tests/test-objects.py @@ -34,6 +34,11 @@ def test_context_manager(self): with nvme.GlobalCtx() as ctx: self.assertIsNotNone(ctx) + def test_context_manager_methods_accessible(self): + with nvme.GlobalCtx() as ctx: + hosts = list(ctx.hosts()) + self.assertIsInstance(hosts, list) + def test_hosts_iterator_returns_list(self): ctx = nvme.GlobalCtx() hosts = list(ctx.hosts()) @@ -82,10 +87,10 @@ def test_creation_with_hostsymname(self): host = nvme.Host(self.ctx, hostnqn=hostnqn, hostsymname=symname) self.assertEqual(host.hostsymname, symname) - def test_set_symname(self): + def test_set_hostsymname(self): hostnqn = 'nqn.2014-08.com.example:test-host-set-symname' host = nvme.Host(self.ctx, hostnqn=hostnqn) - host.set_symname('updated-symname') + host.hostsymname = 'updated-symname' self.assertEqual(host.hostsymname, 'updated-symname') def test_dhchap_host_key_is_none_by_default(self): @@ -106,6 +111,12 @@ def test_context_manager(self): with nvme.Host(self.ctx) as h: self.assertIsNotNone(h) + def test_context_manager_properties_accessible(self): + hostnqn = 'nqn.2014-08.com.example:test-host-ctx-mgr' + with nvme.Host(self.ctx, hostnqn=hostnqn) as h: + self.assertEqual(h.hostnqn, hostnqn) + self.assertIsNone(h.dhchap_host_key) + class TestCtrl(unittest.TestCase): @@ -163,7 +174,7 @@ def test_trsvcid_property(self): def test_connected_returns_false_before_connect(self): ctrl = self._make_loop_ctrl() - self.assertFalse(ctrl.connected()) + self.assertFalse(ctrl.connected) def test_name_is_none_before_connect(self): ctrl = self._make_loop_ctrl() @@ -181,6 +192,15 @@ def test_context_manager(self): }) as c: self.assertIsNotNone(c) + def test_context_manager_properties_accessible(self): + with nvme.Ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'loop', + }) as c: + self.assertEqual(c.transport, 'loop') + self.assertEqual(c.subsysnqn, self.subsysnqn) + self.assertFalse(c.connected) + def test_namespaces_iterator_returns_list(self): ctrl = self._make_loop_ctrl() nss = list(ctrl.namespaces()) @@ -216,7 +236,7 @@ def test_multiple_ctrls_same_ctx(self): ctrls = [self._make_loop_ctrl() for _ in range(5)] self.assertEqual(len(ctrls), 5) for c in ctrls: - self.assertFalse(c.connected()) + self.assertFalse(c.connected) class TestCtrlErrorHandling(unittest.TestCase):