Re: [PATCH 03/17] tools/arch/x86/pmtctl: Add libpmtctl internal logging and utility functions
From: Ilpo Järvinen
Date: Tue May 26 2026 - 06:00:19 EST
On Mon, 25 May 2026, David E. Box wrote:
> Add the internal logging infrastructure and shared utility helpers used by
> libpmtctl_core.
>
> The logging layer provides consistent level-filtered diagnostics across the
> library, while the utility helpers centralize common sysfs access, string
> handling, and scoped resource cleanup functionality used across backend
> implementations.
>
> Logging levels mirror the public pmtctl_log_level enum and can be used to
> control library verbosity.
>
> Assisted-by: GitHub-Copilot:claude-sonnet-4.6
> Signed-off-by: David E. Box <david.e.box@xxxxxxxxxxxxxxx>
> ---
> tools/arch/x86/pmtctl/include/lib/common.h | 34 ++++
> tools/arch/x86/pmtctl/include/lib/log.h | 80 ++++++++
> tools/arch/x86/pmtctl/include/lib/pmt_guid.h | 63 ++++++
> tools/arch/x86/pmtctl/lib/common.c | 178 +++++++++++++++++
> tools/arch/x86/pmtctl/lib/log.c | 80 ++++++++
> tools/arch/x86/pmtctl/lib/pmt_guid.c | 200 +++++++++++++++++++
> 6 files changed, 635 insertions(+)
> create mode 100644 tools/arch/x86/pmtctl/include/lib/common.h
> create mode 100644 tools/arch/x86/pmtctl/include/lib/log.h
> create mode 100644 tools/arch/x86/pmtctl/include/lib/pmt_guid.h
> create mode 100644 tools/arch/x86/pmtctl/lib/common.c
> create mode 100644 tools/arch/x86/pmtctl/lib/log.c
> create mode 100644 tools/arch/x86/pmtctl/lib/pmt_guid.c
>
> diff --git a/tools/arch/x86/pmtctl/include/lib/common.h b/tools/arch/x86/pmtctl/include/lib/common.h
> new file mode 100644
> index 000000000000..cf1540326ec6
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/common.h
> @@ -0,0 +1,34 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_COMMON_H
> +#define PMTCTL_COMMON_H
> +
> +#include <stdlib.h>
> +#include <stdio.h>
> +#include <stdint.h>
> +
> +#ifndef __GNUC__
> +#error "pmtctl: cleanup helpers require GCC/Clang (__GNUC__)."
> +#endif
> +
> +static inline void freep(void *p)
> +{
> + void **ptr = (void **)p;
> +
> + if (*ptr) {
> + free(*ptr);
> + *ptr = NULL;
> + }
> +}
> +
> +#define auto_free __attribute__((cleanup(freep)))
> +
> +#ifndef ARRAY_SIZE
> +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
> +#endif
> +
> +char *xstrdup(const char *s);
> +int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen);
> +int read_optional_int_attr(const char *devpath, const char *attr, int *out_id);
> +int read_int_attr(const char *devpath, const char *attr, int *out_id);
> +int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid);
> +#endif
> diff --git a/tools/arch/x86/pmtctl/include/lib/log.h b/tools/arch/x86/pmtctl/include/lib/log.h
> new file mode 100644
> index 000000000000..32f4de05f9de
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/log.h
> @@ -0,0 +1,80 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_LOG_H
> +#define PMTCTL_LOG_H
> +
> +#include <linux/compiler.h>
> +#include "pmtctl_types.h"
> +
> +#define PMTCTL_EXIT_USER 1
> +#define PMTCTL_EXIT_SYSTEM 2
> +#define PMTCTL_EXIT_BUG 3
> +
> +enum {
> + PMTCTL_ERR_PARSE = 1, /* malformed JSON, invalid numbers, missing fields */
> + PMTCTL_ERR_NOTFOUND, /* metric/device/group not found */
> + PMTCTL_ERR_BAD_ARG,
> + PMTCTL_ERR_CMD_PARSE,
> + PMTCTL_ERR_CMD_LIST,
> + PMTCTL_ERR_CMD_STAT,
> + PMTCTL_ERR_NOMETRICS,
> + PMTCTL_ERR_METRICS,
> + PMTCTL_ERR_INVALID, /* bad selector, bad arguments, bad config */
> + PMTCTL_ERR_DEVICE, /* device mismatch or device internal failure */
> + PMTCTL_ERR_BINDING, /* binding table construction failed */
> + PMTCTL_ERR_UNSUPPORTED, /* metric/feature not supported on this system */
> +};
> +
> +#ifdef LOG_PREFIX
> +#define _LOG_PREFIXED(fmt) LOG_PREFIX ": " fmt
> +#else
> +#define _LOG_PREFIXED(fmt) fmt
> +#endif
> +
> +#define LOG_ERROR PMTCTL_LOG_ERROR
> +#define LOG_WARN PMTCTL_LOG_WARN
> +#define LOG_INFO PMTCTL_LOG_INFO
> +#define LOG_DEBUG PMTCTL_LOG_DEBUG
> +
> +void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
> + __printf(3, 4);
> +void log_set_level(enum pmtctl_log_level lvl);
> +
> +#define log_bug_and_exit(fmt, ...) \
> + log_bug_impl(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
> +
> +#define log_err(err, fmt, ...) \
> + log_impl(LOG_ERROR, (err), _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#define log_warn(fmt, ...) \
> + log_impl(LOG_WARN, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#define log_info(fmt, ...) \
> + log_impl(LOG_INFO, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +
> +#ifdef DEBUG
> +#define log_debug(fmt, ...) \
> + log_impl(LOG_DEBUG, 0, _LOG_PREFIXED(fmt), ##__VA_ARGS__)
> +#else
> +#define log_debug(fmt, ...) \
> + ((void)(0 ? log_info(fmt, ##__VA_ARGS__) : 0))
> +#endif
> +
> +int log_ret(int ret, const char *fmt, ...);
> +
> +__noreturn
> +void log_bug_impl(const char *file, int line, const char *fmt, ...);
> +
> +#define LOG_ONCE(level, fmt, ...) \
> + do { \
> + static int __done_##__LINE__; \
> + if (!__done_##__LINE__) { \
> + level(fmt, ##__VA_ARGS__); \
> + __done_##__LINE__ = 1; \
> + } \
> + } while (0)
> +
> +#define log_err_once(fmt, ...) LOG_ONCE(log_err, fmt, ##__VA_ARGS__)
> +#define log_warn_once(fmt, ...) LOG_ONCE(log_warn, fmt, ##__VA_ARGS__)
> +#define log_info_once(fmt, ...) LOG_ONCE(log_info, fmt, ##__VA_ARGS__)
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/include/lib/pmt_guid.h b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
> new file mode 100644
> index 000000000000..d21bda5bc71b
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/include/lib/pmt_guid.h
> @@ -0,0 +1,63 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef PMTCTL_PMT_GUID_H
> +#define PMTCTL_PMT_GUID_H
> +
> +#include <stdbool.h>
> +#include <stdint.h>
> +
> +/*
> + * Per-GUID metadata derived from the Intel-PMT pmt.xml top-level mapping
> + * table. The compiled-in built-in table (builtin_guids[]) is generated by
> + * scripts/gen_builtin_defs.py. Runtime-loaded providers may register
> + * additional entries via pmt_guid_register().
> + *
> + * A global intern registry ensures pointer-equality across pmt_device and
> + * pmt_metric_def: two references to the same GUID always yield the same
> + * struct pmt_guid pointer.
> + */
> +struct pmt_guid {
> + uint32_t guid;
> + const char *name; /* basedir lowercased with '/' -> '_'; may be NULL */
> + const char *description; /* may be NULL */
> +};
> +
> +/*
> + * Register an array of pmt_guid entries with the global registry. The
> + * pointer must remain valid for the registry's lifetime. Entries with a
> + * GUID already present in the registry are skipped (first wins).
> + *
> + * Returns 0 on success, negative errno on failure.
> + */
> +int pmt_guid_register(const struct pmt_guid *entries, int count);
> +
> +/*
> + * Same as pmt_guid_register(), but the registry takes ownership of `block`
> + * (typically the heap allocation that backs `entries` and any pooled
> + * strings the entries point into). The block is freed by pmt_guid_cleanup().
> + *
> + * `block` may equal `entries` when the array sits at the head of the
> + * allocation. Passing a NULL block degrades to pmt_guid_register().
> + *
> + * Returns 0 on success, negative errno on failure. On failure the caller
> + * retains ownership of `block` and must free it.
> + */
> +int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count);
> +
> +/*
> + * Lookup the registered pmt_guid for the given numeric GUID. Returns NULL
> + * if no entry has been registered or interned for that GUID.
> + */
> +const struct pmt_guid *pmt_guid_lookup(uint32_t guid);
> +
> +/*
> + * Resolve numeric GUID to a stable struct pmt_guid pointer. If no entry is
> + * registered, a synthetic entry (name = NULL, description = NULL) is
> + * allocated and returned; subsequent calls with the same GUID return the
> + * same pointer. Returns NULL only on allocation failure.
> + */
> +const struct pmt_guid *pmt_guid_intern(uint32_t guid);
> +
> +/* Free all synthetic (interned) entries and clear registrations. */
> +void pmt_guid_cleanup(void);
> +
> +#endif
> diff --git a/tools/arch/x86/pmtctl/lib/common.c b/tools/arch/x86/pmtctl/lib/common.c
> new file mode 100644
> index 000000000000..42931bf79480
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/common.c
> @@ -0,0 +1,178 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <stdlib.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "lib/common.h"
> +#include "lib/log.h"
> +
> +char *xstrdup(const char *s)
> +{
> + char *p;
> +
> + if (!s)
> + return NULL;
> +
> + p = strdup(s);
> + if (!p) {
> + log_err(errno, "out of memory");
> + exit(EXIT_FAILURE);
> + }
> +
> + return p;
> +}
> +
> +static void trim_newline(char *s)
> +{
> + size_t len;
> + char *end;
> +
> + if (!s)
> + return;
> +
> + len = strlen(s);
> + if (!len)
> + return;
> +
> + end = s + len - 1;
> +
> + while (end >= s && (*end == '\n' || *end == '\r')) {
Is it beneficial to trim just newlines or would it be better to
trim anything for which isspace() is true?
> + *end = '\0';
> + end--;
> + }
> +}
> +
> +int read_attr_text(const char *dir, const char *name, char *buf, size_t buflen)
> +{
> + char path[PATH_MAX];
> + int fd;
> + ssize_t n;
> +
> + if (!dir || !name || !buf || buflen == 0)
> + return -EINVAL;
> +
> + if (snprintf(path, sizeof(path), "%s/%s", dir, name) >= (int)sizeof(path))
> + return -ENAMETOOLONG;
> +
> + fd = open(path, O_RDONLY | O_CLOEXEC);
Why is O_CLOEXEC required? (The is closed just a few lines down.)
> + if (fd == -1)
> + return -errno;
> +
> + n = read(fd, buf, buflen - 1);
> + close(fd);
> +
> + if (n == -1)
> + return -errno;
> +
> + buf[n] = '\0';
> + trim_newline(buf);
> +
> + return 0;
> +}
> +
> +int read_optional_int_attr(const char *devpath, const char *attr, int *out_id)
> +{
> + char buf[64];
> + char *end;
> + long v;
> + int ret;
> +
> + if (!out_id || !devpath || !attr)
> + return -EINVAL;
> +
> + ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> + if (ret != 0) {
> + if (ret == -ENOENT) {
> + /* file not found */
> + *out_id = -1;
> + return 0;
> + }
> +
> + return ret;
> + }
> +
> + errno = 0;
> +
This belongs tightly together with strtol() so I don't know why there's
empty in between.
> + /* accept decimal or 0x-prefixed hex */
> + v = strtol(buf, &end, 0);
> + if (errno)
> + return -errno;
> +
> + if (end == buf || *end != '\0')
> + return -EINVAL;
> +
> + if (v < INT_MIN || v > INT_MAX)
> + return -ERANGE;
> +
> + *out_id = (int)v;
> +
> + return 0;
> +}
> +
> +int read_int_attr(const char *devpath, const char *attr, int *out_id)
> +{
> + char buf[64];
> + char *end;
> + long v;
> + int ret;
> +
> + if (!out_id || !devpath || !attr)
> + return -EINVAL;
> +
> + ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> + if (ret != 0)
> + return ret;
> +
> + errno = 0;
> +
> + /* accept decimal or 0x-prefixed hex */
> + v = strtol(buf, &end, 0);
> + if (errno)
> + return -errno;
> +
> + if (end == buf || *end != '\0')
> + return -EINVAL;
> +
> + if (v < INT_MIN || v > INT_MAX)
> + return -ERANGE;
> +
> + *out_id = (int)v;
> +
> + return 0;
> +}
Lots of code duplication.
> +
> +int read_u32_hex_attr(const char *devpath, const char *attr, uint32_t *out, int err_invalid)
> +{
> + char buf[64];
> + char *end;
> + unsigned long v;
> + int ret;
> +
> + if (!out || !devpath || !attr)
> + return -EINVAL;
> +
> + ret = read_attr_text(devpath, attr, buf, sizeof(buf));
> + if (ret != 0)
> + return ret;
> +
> + errno = 0;
> + v = strtoul(buf, &end, 16);
> + if (errno)
> + return -errno;
> +
> + if (end == buf || *end != '\0')
> + return err_invalid;
> +
> + if (v > UINT32_MAX)
> + return err_invalid;
> +
> + *out = (uint32_t)v;
> +
> + return 0;
> +}
> diff --git a/tools/arch/x86/pmtctl/lib/log.c b/tools/arch/x86/pmtctl/lib/log.c
> new file mode 100644
> index 000000000000..2ce9edacc97a
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/log.c
> @@ -0,0 +1,80 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <stdarg.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <errno.h>
> +
> +#include "lib/log.h"
> +
> +static enum pmtctl_log_level g_log_level = LOG_INFO;
> +
> +void log_set_level(enum pmtctl_log_level lvl)
> +{
> + if (lvl < LOG_ERROR || lvl > LOG_DEBUG)
> + g_log_level = LOG_INFO;
> + else
> + g_log_level = lvl;
> +}
> +
> +static void log_vmsg(enum pmtctl_log_level lvl, int err, const char *fmt,
> + va_list ap)
> +{
> + if (lvl > g_log_level)
> + return;
> +
> + const char *tag =
> + (lvl == LOG_ERROR) ? "error" :
> + (lvl == LOG_WARN) ? "warning" :
> + (lvl == LOG_INFO) ? "info" :
Aligning is off.
> + "debug";
> +
> + fprintf(stderr, "%s: ", tag);
> + vfprintf(stderr, fmt, ap);
> +
> + if (err != 0) {
> + int e = (err < 0) ? -err : err;
???
> +
> + if (err < 0)
???
> + fprintf(stderr, ": %s", strerror(e));
> + }
> +
> + fputc('\n', stderr);
> +}
> +
> +void log_impl(enum pmtctl_log_level lvl, int err, const char *fmt, ...)
> +{
> + va_list ap;
> +
> + va_start(ap, fmt);
> + log_vmsg(lvl, err, fmt, ap);
> + va_end(ap);
> +}
> +
> +int log_ret(int ret, const char *fmt, ...)
> +{
> + if (ret == 0)
> + return 0;
> +
> + va_list ap;
> +
> + va_start(ap, fmt);
> + log_vmsg(LOG_ERROR, ret, fmt, ap);
> + va_end(ap);
> +
> + return ret;
> +}
> +
> +__noreturn
> +void log_bug_impl(const char *file, int line, const char *fmt, ...)
> +{
> + va_list ap;
> +
> + fprintf(stderr, "pmtctl: BUG at %s:%d: ", file, line);
> + va_start(ap, fmt);
> + vfprintf(stderr, fmt, ap);
> + va_end(ap);
> + fputc('\n', stderr);
> +
> + exit(PMTCTL_EXIT_BUG);
> +}
> diff --git a/tools/arch/x86/pmtctl/lib/pmt_guid.c b/tools/arch/x86/pmtctl/lib/pmt_guid.c
> new file mode 100644
> index 000000000000..f4d74fc7a977
> --- /dev/null
> +++ b/tools/arch/x86/pmtctl/lib/pmt_guid.c
> @@ -0,0 +1,200 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include "lib/pmt_guid.h"
> +
> +/*
> + * Global intern registry for struct pmt_guid.
> + *
> + * Two sources feed this table:
> + * 1. Pre-registered blocks (e.g. builtin_guids[] from generated code,
> + * or JSON-loaded pmt_guids.json from the runtime provider). Pointers
> + * to those entries are stored verbatim and assumed to outlive the
> + * registry's use.
> + * 2. Synthetic entries allocated on demand by pmt_guid_intern() for
> + * GUIDs that no provider knows about. These have name = NULL and
> + * description = NULL.
> + *
> + * A simple linear-scan array is used; the registry holds at most a few
> + * hundred entries on any realistic system.
> + */
This seems largely irrelevant to what it comments about (struct
guid_slot is definitely not a "global intern registry" nor a "table").
> +struct guid_slot {
> + const struct pmt_guid *entry; /* points into registered block or synth_storage */
> + struct pmt_guid *synth; /* non-NULL iff this slot owns the entry */
> +};
> +
> +static struct guid_slot *g_slots;
> +static int g_nslots;
> +static int g_cap;
> +
> +/*
> + * Heap blocks handed to pmt_guid_register_owned(). The registry frees
> + * them in pmt_guid_cleanup() after disposing of the slot array, so any
> + * pointers the slots held into these blocks are gone by then.
> + */
> +static void **g_owned_blocks;
> +static int g_nowned;
> +static int g_owned_cap;
> +
> +static int reserve_one(void)
> +{
> + struct guid_slot *tmp;
> + int new_cap;
> +
> + if (g_nslots < g_cap)
> + return 0;
> +
> + new_cap = g_cap ? g_cap * 2 : 32;
> + tmp = realloc(g_slots, (size_t)new_cap * sizeof(*tmp));
Why is new_cap int if it is needed as size_t ??
And a follow up from that, why are g_cap and g_nslots signed if only
positive values are acceptable?
> + if (!tmp)
> + return -ENOMEM;
> +
> + g_slots = tmp;
> + g_cap = new_cap;
> + return 0;
> +}
> +
> +const struct pmt_guid *pmt_guid_lookup(uint32_t guid)
> +{
> + for (int i = 0; i < g_nslots; i++) {
> + if (g_slots[i].entry && g_slots[i].entry->guid == guid)
> + return g_slots[i].entry;
> + }
> + return NULL;
> +}
> +
> +int pmt_guid_register(const struct pmt_guid *entries, int count)
> +{
> + if (!entries || count <= 0)
> + return -EINVAL;
> +
> + for (int i = 0; i < count; i++) {
> + const struct pmt_guid *e = &entries[i];
> + bool already_known = false;
> + int ret;
> +
> + /*
> + * If a slot already exists for this GUID, upgrade it when the
> + * existing entry is a synthetic placeholder (created earlier
> + * by pmt_guid_intern() before any provider was loaded) and the
> + * new entry carries real metadata. The synthetic struct must
> + * be kept alive because earlier callers (e.g. devices) hold
> + * pointers to it; patch its fields in place instead of
> + * swapping the slot entry.
> + */
> + for (int s = 0; s < g_nslots; s++) {
> + struct guid_slot *slot = &g_slots[s];
> +
> + if (!slot->entry || slot->entry->guid != e->guid)
> + continue;
> +
> + if (slot->synth && e->name && *e->name) {
> + slot->synth->name = e->name;
> + slot->synth->description = e->description;
> + }
> + already_known = true;
> + break;
> + }
> +
> + if (already_known)
> + continue;
> +
> + ret = reserve_one();
> + if (ret < 0)
> + return ret;
> +
> + g_slots[g_nslots].entry = e;
> + g_slots[g_nslots].synth = NULL;
> + g_nslots++;
So does reserve_one() and this function, instead of returning the index,
communicate using g_nslots variable???
> + }
> +
> + return 0;
> +}
> +
> +int pmt_guid_register_owned(void *block, const struct pmt_guid *entries, int count)
> +{
> + void **tmp;
> + int ret;
> +
> + /*
> + * Grow g_owned_blocks first, before registering anything. If this
> + * fails, no entries have been added to g_slots yet, so the caller
> + * can safely free(block) on error without leaving dangling slot
> + * pointers behind.
> + */
> + if (block && g_nowned == g_owned_cap) {
> + int new_cap = g_owned_cap ? g_owned_cap * 2 : 8;
> +
> + tmp = realloc(g_owned_blocks, (size_t)new_cap * sizeof(*tmp));
Same typing problem as above but it also implies this duplicating code.
> + if (!tmp)
> + return -ENOMEM;
> +
> + g_owned_blocks = tmp;
> + g_owned_cap = new_cap;
> + }
> +
> + ret = pmt_guid_register(entries, count);
> + if (ret < 0)
> + return ret;
> +
> + if (block)
> + g_owned_blocks[g_nowned++] = block;
> +
> + return 0;
> +}
> +
> +const struct pmt_guid *pmt_guid_intern(uint32_t guid)
> +{
> + const struct pmt_guid *existing;
> + struct pmt_guid *synth;
> + int ret;
> +
> + existing = pmt_guid_lookup(guid);
> + if (existing)
> + return existing;
> +
> + ret = reserve_one();
> + if (ret < 0)
> + return NULL;
> +
> + synth = calloc(1, sizeof(*synth));
> + if (!synth)
> + return NULL;
> +
> + synth->guid = guid;
> + synth->name = NULL;
> + synth->description = NULL;
> +
> + g_slots[g_nslots].entry = synth;
> + g_slots[g_nslots].synth = synth;
> + g_nslots++;
> +
> + return synth;
> +}
> +
> +void pmt_guid_cleanup(void)
> +{
> + for (int i = 0; i < g_nslots; i++)
> + free(g_slots[i].synth);
> +
> + free(g_slots);
> + g_slots = NULL;
> + g_nslots = 0;
> + g_cap = 0;
> +
> + /*
> + * Owned blocks are freed last: slot entries may have pointed into
> + * them, but those pointers are gone now that g_slots is released.
> + */
> + for (int i = 0; i < g_nowned; i++)
> + free(g_owned_blocks[i]);
> +
> + free(g_owned_blocks);
> + g_owned_blocks = NULL;
> + g_nowned = 0;
> + g_owned_cap = 0;
> +}
>
--
i.