From: Sasha Levin <sashal@kernel.org>
To: linux-api@vger.kernel.org, linux-kernel@vger.kernel.org
Cc: linux-doc@vger.kernel.org, linux-fsdevel@vger.kernel.org,
linux-kbuild@vger.kernel.org, linux-kselftest@vger.kernel.org,
workflows@vger.kernel.org, tools@kernel.org, x86@kernel.org,
Thomas Gleixner <tglx@kernel.org>,
"Paul E . McKenney" <paulmck@kernel.org>,
Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Jonathan Corbet <corbet@lwn.net>,
Dmitry Vyukov <dvyukov@google.com>,
Randy Dunlap <rdunlap@infradead.org>,
Cyril Hrubis <chrubis@suse.cz>, Kees Cook <kees@kernel.org>,
Jake Edge <jake@lwn.net>,
David Laight <david.laight.linux@gmail.com>,
Askar Safin <safinaskar@zohomail.com>,
Gabriele Paoloni <gpaoloni@redhat.com>,
Mauro Carvalho Chehab <mchehab@kernel.org>,
Christian Brauner <brauner@kernel.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Andrew Morton <akpm@linux-foundation.org>,
Masahiro Yamada <masahiroy@kernel.org>,
Shuah Khan <skhan@linuxfoundation.org>,
Ingo Molnar <mingo@redhat.com>, Arnd Bergmann <arnd@arndb.de>,
Sasha Levin <sashal@kernel.org>
Subject: [PATCH v3 2/9] kernel/api: enable kerneldoc-based API specifications
Date: Fri, 24 Apr 2026 12:51:22 -0400 [thread overview]
Message-ID: <20260424165130.2306833-3-sashal@kernel.org> (raw)
In-Reply-To: <20260424165130.2306833-1-sashal@kernel.org>
This patch adds support for extracting API specifications from
kernel-doc comments and generating C macro invocations for the
kernel API specification framework.
Changes include:
- New kdoc_apispec.py module for generating API spec macros
- Updates to kernel-doc.py to support -apispec output format
- Build system integration in Makefile.build
- Generator script for collecting all API specifications
- Support for API-specific sections in kernel-doc comments
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
Documentation/dev-tools/kernel-api-spec.rst | 11 +
scripts/Makefile.build | 31 +
scripts/Makefile.clean | 3 +
tools/docs/kernel-doc | 5 +
tools/lib/python/kdoc/kdoc_apispec.py | 1249 +++++++++++++++++++
tools/lib/python/kdoc/kdoc_output.py | 9 +-
tools/lib/python/kdoc/kdoc_parser.py | 86 +-
7 files changed, 1389 insertions(+), 5 deletions(-)
create mode 100644 tools/lib/python/kdoc/kdoc_apispec.py
diff --git a/Documentation/dev-tools/kernel-api-spec.rst b/Documentation/dev-tools/kernel-api-spec.rst
index 395c2294d5209..479bc78797ba8 100644
--- a/Documentation/dev-tools/kernel-api-spec.rst
+++ b/Documentation/dev-tools/kernel-api-spec.rst
@@ -239,6 +239,17 @@ execution context, and return values. Parameter violations are reported via
``pr_warn_ratelimited`` and return value violations via ``WARN_ONCE`` to avoid
flooding the kernel log.
+.. warning::
+
+ Userspace errno is affected when this option is on. For syscalls that
+ violate their parameter specification, KAPI short-circuits the call and
+ returns ``-EINVAL`` from the validator **before** the real handler runs.
+ That errno can differ from what the real handler would have produced for
+ the same condition (for example, ``-ENOMEM`` from an allocation path or
+ ``-EFAULT`` from a deeper copy-in). ``CONFIG_KAPI_RUNTIME_CHECKS`` is a
+ debug-only option; do not enable it on production kernels or in
+ userspace-visible test environments where error-code fidelity matters.
+
Custom Validators
-----------------
diff --git a/scripts/Makefile.build b/scripts/Makefile.build
index 3652b85be5459..ef203e490c797 100644
--- a/scripts/Makefile.build
+++ b/scripts/Makefile.build
@@ -174,6 +174,37 @@ ifneq ($(KBUILD_EXTRA_WARN),)
endif
endif
+# Generate API spec headers from kernel-doc comments
+ifeq ($(CONFIG_KAPI_SPEC),y)
+# Function to check if a file has API specifications
+has-apispec = $(shell grep -qE '^\s*\*\s*context-flags:' $(src)/$(1) 2>/dev/null && echo $(1))
+
+# Get base names without directory prefix
+c-objs-base := $(notdir $(real-obj-y) $(real-obj-m))
+# Filter to only .o files with corresponding .c source files
+c-files := $(foreach o,$(c-objs-base),$(if $(wildcard $(src)/$(o:.o=.c)),$(o:.o=.c)))
+# Also check for any additional .c files that contain API specs but are included
+extra-c-files := $(shell find $(src) -maxdepth 1 -name "*.c" -exec grep -l '^\s*\*\s*\(long-desc\|context-flags\|state-trans\):' {} \; 2>/dev/null | xargs -r basename -a)
+# Combine both lists and remove duplicates
+all-c-files := $(sort $(c-files) $(extra-c-files))
+# Only include files that actually have API specifications
+apispec-files := $(foreach f,$(all-c-files),$(call has-apispec,$(f)))
+# Generate apispec targets with proper directory prefix
+apispec-y := $(addprefix $(obj)/,$(apispec-files:.c=.apispec.h))
+always-y += $(apispec-y)
+targets += $(apispec-y)
+
+quiet_cmd_apispec = APISPEC $@
+ cmd_apispec = PYTHONDONTWRITEBYTECODE=1 $(KERNELDOC) -apispec \
+ $(KDOCFLAGS) $< > $@ || rm -f $@
+
+$(obj)/%.apispec.h: $(src)/%.c $(KERNELDOC) FORCE
+ $(call if_changed,apispec)
+
+# Source files that include their own apispec.h need to depend on it
+$(foreach f,$(apispec-files),$(eval $(obj)/$(f:.c=.o): $(obj)/$(f:.c=.apispec.h)))
+endif
+
# Compile C sources (.c)
# ---------------------------------------------------------------------------
diff --git a/scripts/Makefile.clean b/scripts/Makefile.clean
index 6ead00ec7313b..f78dbbe637f27 100644
--- a/scripts/Makefile.clean
+++ b/scripts/Makefile.clean
@@ -35,6 +35,9 @@ __clean-files := $(filter-out $(no-clean-files), $(__clean-files))
__clean-files := $(wildcard $(addprefix $(obj)/, $(__clean-files)))
+# Also clean generated apispec headers (computed dynamically in Makefile.build)
+__clean-files += $(wildcard $(obj)/*.apispec.h)
+
# ==========================================================================
# To make this rule robust against "Argument list too long" error,
diff --git a/tools/docs/kernel-doc b/tools/docs/kernel-doc
index aed09f9a54dd1..e71e663d9b7c0 100755
--- a/tools/docs/kernel-doc
+++ b/tools/docs/kernel-doc
@@ -253,6 +253,8 @@ def main():
help="Output reStructuredText format (default).")
out_fmt.add_argument("-N", "-none", "--none", action="store_true",
help="Do not output documentation, only warnings.")
+ out_fmt.add_argument("-apispec", "--apispec", action="store_true",
+ help="Output C macro invocations for kernel API specifications.")
#
# Output selection mutually-exclusive group
@@ -323,11 +325,14 @@ def main():
#
from kdoc.kdoc_files import KernelFiles # pylint: disable=C0415
from kdoc.kdoc_output import RestFormat, ManFormat # pylint: disable=C0415
+ from kdoc.kdoc_apispec import ApiSpecFormat # pylint: disable=C0415
if args.man:
out_style = ManFormat(modulename=args.modulename)
elif args.none:
out_style = None
+ elif args.apispec:
+ out_style = ApiSpecFormat()
else:
out_style = RestFormat()
diff --git a/tools/lib/python/kdoc/kdoc_apispec.py b/tools/lib/python/kdoc/kdoc_apispec.py
new file mode 100644
index 0000000000000..b718c6a02c4a4
--- /dev/null
+++ b/tools/lib/python/kdoc/kdoc_apispec.py
@@ -0,0 +1,1249 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+
+"""
+Generate C macro invocations for kernel API specifications from kernel-doc comments.
+
+This module creates C header files with API specification macros that match
+the kernel API specification framework introduced in commit 9688de5c25bed.
+"""
+
+from kdoc.kdoc_output import OutputFormat
+import re
+import sys
+
+
+# Maximum string lengths (from kernel_api_spec.h)
+KAPI_MAX_DESC_LEN = 512
+# Note: long_description, notes, and examples are stored as pointers to
+# .rodata in the generated spec, so they are not truncated by this tool.
+KAPI_MAX_NAME_LEN = 128
+KAPI_MAX_SIGNAL_NAME_LEN = 32
+
+# Valid KAPI effect types
+VALID_EFFECT_TYPES = {
+ 'KAPI_EFFECT_NONE', 'KAPI_EFFECT_MODIFY_STATE', 'KAPI_EFFECT_PROCESS_STATE',
+ 'KAPI_EFFECT_IRREVERSIBLE', 'KAPI_EFFECT_SCHEDULE', 'KAPI_EFFECT_FILESYSTEM',
+ 'KAPI_EFFECT_HARDWARE', 'KAPI_EFFECT_ALLOC_MEMORY', 'KAPI_EFFECT_FREE_MEMORY',
+ 'KAPI_EFFECT_SIGNAL_SEND', 'KAPI_EFFECT_FILE_POSITION', 'KAPI_EFFECT_LOCK_ACQUIRE',
+ 'KAPI_EFFECT_LOCK_RELEASE', 'KAPI_EFFECT_RESOURCE_CREATE', 'KAPI_EFFECT_RESOURCE_DESTROY',
+ 'KAPI_EFFECT_NETWORK'
+}
+
+# DSL aliases mapping short tokens to their canonical KAPI_* C
+# identifier. Unknown tokens pass through unchanged.
+_CTX_ALIASES = {
+ 'process': 'KAPI_CTX_PROCESS',
+ 'softirq': 'KAPI_CTX_SOFTIRQ',
+ 'hardirq': 'KAPI_CTX_HARDIRQ',
+ 'nmi': 'KAPI_CTX_NMI',
+ 'atomic': 'KAPI_CTX_ATOMIC',
+ 'sleepable': 'KAPI_CTX_SLEEPABLE',
+ 'preempt_disabled': 'KAPI_CTX_PREEMPT_DISABLED',
+ 'irq_disabled': 'KAPI_CTX_IRQ_DISABLED',
+}
+_TYPE_ALIASES = {
+ 'int': 'KAPI_TYPE_INT',
+ 'uint': 'KAPI_TYPE_UINT',
+ 'ptr': 'KAPI_TYPE_PTR',
+ 'struct': 'KAPI_TYPE_STRUCT',
+ 'union': 'KAPI_TYPE_UNION',
+ 'enum': 'KAPI_TYPE_ENUM',
+ 'func_ptr': 'KAPI_TYPE_FUNC_PTR',
+ 'array': 'KAPI_TYPE_ARRAY',
+ 'fd': 'KAPI_TYPE_FD',
+ 'user_ptr': 'KAPI_TYPE_USER_PTR',
+ 'uptr': 'KAPI_TYPE_USER_PTR',
+ 'path': 'KAPI_TYPE_PATH',
+ 'custom': 'KAPI_TYPE_CUSTOM',
+}
+_FLAG_ALIASES = {
+ 'input': 'KAPI_PARAM_IN',
+ 'in': 'KAPI_PARAM_IN',
+ 'output': 'KAPI_PARAM_OUT',
+ 'out': 'KAPI_PARAM_OUT',
+ 'inout': 'KAPI_PARAM_INOUT',
+ 'optional': 'KAPI_PARAM_OPTIONAL',
+ 'const': 'KAPI_PARAM_CONST',
+ 'volatile': 'KAPI_PARAM_VOLATILE',
+ 'user': 'KAPI_PARAM_USER',
+ 'dma': 'KAPI_PARAM_DMA',
+ 'aligned': 'KAPI_PARAM_ALIGNED',
+}
+
+
+def _canon_token(tok, table):
+ """Look up `tok` (case-insensitive) in `table`. Unknown tokens
+ pass through verbatim."""
+ t = tok.strip()
+ if not t:
+ return ''
+ return table.get(t.lower(), t)
+
+
+def _canon_context_expr(expr):
+ """Canonicalise a context flag expression. Accepts '|'- or
+ ','-joined tokens; returns a '|'-joined string of KAPI_CTX_*
+ identifiers ready for KAPI_CONTEXT()."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, _CTX_ALIASES) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+def _canon_flags_expr(expr):
+ """Canonicalise a parameter flags expression. Accepts '|'- or
+ ','-joined KAPI_PARAM_* tokens or their aliases; returns a
+ '|'-joined canonical string."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, _FLAG_ALIASES) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+# Alias tables for enum families used as block-attribute values
+# (lock type, signal direction/action/timing, return check type) and
+# for the top-level `side-effect:` bitmask.
+_LOCK_TYPE_ALIASES = {
+ 'none': 'KAPI_LOCK_NONE',
+ 'mutex': 'KAPI_LOCK_MUTEX',
+ 'spinlock': 'KAPI_LOCK_SPINLOCK',
+ 'rwlock': 'KAPI_LOCK_RWLOCK',
+ 'seqlock': 'KAPI_LOCK_SEQLOCK',
+ 'rcu': 'KAPI_LOCK_RCU',
+ 'semaphore': 'KAPI_LOCK_SEMAPHORE',
+ 'custom': 'KAPI_LOCK_CUSTOM',
+}
+_SIGNAL_DIR_ALIASES = {
+ 'receive': 'KAPI_SIGNAL_RECEIVE',
+ 'send': 'KAPI_SIGNAL_SEND',
+ 'handle': 'KAPI_SIGNAL_HANDLE',
+ 'block': 'KAPI_SIGNAL_BLOCK',
+ 'ignore': 'KAPI_SIGNAL_IGNORE',
+}
+_SIGNAL_ACTION_ALIASES = {
+ 'default': 'KAPI_SIGNAL_ACTION_DEFAULT',
+ 'terminate': 'KAPI_SIGNAL_ACTION_TERMINATE',
+ 'coredump': 'KAPI_SIGNAL_ACTION_COREDUMP',
+ 'stop': 'KAPI_SIGNAL_ACTION_STOP',
+ 'continue': 'KAPI_SIGNAL_ACTION_CONTINUE',
+ 'custom': 'KAPI_SIGNAL_ACTION_CUSTOM',
+ 'return': 'KAPI_SIGNAL_ACTION_RETURN',
+ 'restart': 'KAPI_SIGNAL_ACTION_RESTART',
+ 'queue': 'KAPI_SIGNAL_ACTION_QUEUE',
+ 'discard': 'KAPI_SIGNAL_ACTION_DISCARD',
+ 'transform': 'KAPI_SIGNAL_ACTION_TRANSFORM',
+}
+_SIGNAL_TIMING_ALIASES = {
+ 'before': 'KAPI_SIGNAL_TIME_BEFORE',
+ 'during': 'KAPI_SIGNAL_TIME_DURING',
+ 'after': 'KAPI_SIGNAL_TIME_AFTER',
+}
+_EFFECT_ALIASES = {
+ 'none': 'KAPI_EFFECT_NONE',
+ 'alloc_memory': 'KAPI_EFFECT_ALLOC_MEMORY',
+ 'free_memory': 'KAPI_EFFECT_FREE_MEMORY',
+ 'modify_state': 'KAPI_EFFECT_MODIFY_STATE',
+ 'signal_send': 'KAPI_EFFECT_SIGNAL_SEND',
+ 'file_position': 'KAPI_EFFECT_FILE_POSITION',
+ 'lock_acquire': 'KAPI_EFFECT_LOCK_ACQUIRE',
+ 'lock_release': 'KAPI_EFFECT_LOCK_RELEASE',
+ 'resource_create': 'KAPI_EFFECT_RESOURCE_CREATE',
+ 'resource_destroy': 'KAPI_EFFECT_RESOURCE_DESTROY',
+ 'schedule': 'KAPI_EFFECT_SCHEDULE',
+ 'hardware': 'KAPI_EFFECT_HARDWARE',
+ 'network': 'KAPI_EFFECT_NETWORK',
+ 'filesystem': 'KAPI_EFFECT_FILESYSTEM',
+ 'process_state': 'KAPI_EFFECT_PROCESS_STATE',
+ 'irreversible': 'KAPI_EFFECT_IRREVERSIBLE',
+}
+_RETURN_CHECK_ALIASES = {
+ 'exact': 'KAPI_RETURN_EXACT',
+ 'range': 'KAPI_RETURN_RANGE',
+ 'error_check': 'KAPI_RETURN_ERROR_CHECK',
+ 'fd': 'KAPI_RETURN_FD',
+ 'custom': 'KAPI_RETURN_CUSTOM',
+ 'no_return': 'KAPI_RETURN_NO_RETURN',
+}
+
+
+def _canon_bitmask_expr(expr, table):
+ """Canonicalise a bitmask expression (e.g. signal direction/timing,
+ side-effect flags). Accepts `|`- or `,`-joined tokens and returns a
+ `|`-joined canonical KAPI_* string."""
+ if not expr:
+ return expr
+ sep = ',' if ',' in expr and '|' not in expr else '|'
+ tokens = [_canon_token(t, table) for t in expr.split(sep)]
+ return ' | '.join(t for t in tokens if t)
+
+
+# Types that carry user-space pointer semantics. A param with one of
+# these types implicitly gets KAPI_PARAM_USER.
+_IMPLIES_USER_FLAG = {'KAPI_TYPE_USER_PTR', 'KAPI_TYPE_PATH'}
+
+
+def _split_type_line(value):
+ """Split a 'type:' line into (type, [flags...]).
+
+ Accepts a single-token value (e.g. 'KAPI_TYPE_UINT' or 'uint')
+ leaving flags empty, or a comma-separated form
+ (e.g. 'uint, input, user') where the first token is the type and
+ subsequent tokens are flag aliases.
+
+ When the type is user-space (user_ptr, path), KAPI_PARAM_USER is
+ added to the flags list if not already present."""
+ parts = [p.strip() for p in value.split(',') if p.strip()]
+ if not parts:
+ return None, []
+ ty = _canon_token(parts[0], _TYPE_ALIASES)
+ flags = [_canon_token(f, _FLAG_ALIASES) for f in parts[1:]]
+ if ty in _IMPLIES_USER_FLAG and 'KAPI_PARAM_USER' not in flags:
+ flags.append('KAPI_PARAM_USER')
+ return ty, flags
+
+
+def _split_constraint_expr(value):
+ """Parse a constraint expression into (canonical_type, extras).
+
+ Shapes:
+ NAME e.g. 'user_path', 'nonzero'
+ NAME ( ARG (, ARG)* ) e.g. 'range(0, 4096)', 'buffer(2)'
+
+ Returns None for free text. Otherwise returns
+ (constraint_type, {aux_field: value, ...}) where the aux fields map
+ onto the matching param-range / param-mask / param-size /
+ param-enum-values / param-constraint slots.
+ """
+ t = value.strip()
+ if not t:
+ return None
+ # Split NAME ( ARGS )
+ lp = t.find('(')
+ rp = t.rfind(')')
+ if lp > 0 and rp > lp:
+ name = t[:lp].strip()
+ args_raw = t[lp + 1:rp].strip()
+ elif lp < 0:
+ name = t
+ args_raw = None
+ else:
+ return None
+ # Bareword must be a single identifier; multi-word values are free text.
+ if not name or any(c.isspace() for c in name):
+ return None
+ key = name.lower()
+ table = {
+ 'range': ('KAPI_CONSTRAINT_RANGE', 'param-range'),
+ 'mask': ('KAPI_CONSTRAINT_MASK', 'param-mask'),
+ 'enum': ('KAPI_CONSTRAINT_ENUM', 'param-enum-values'),
+ 'alignment': ('KAPI_CONSTRAINT_ALIGNMENT', 'param-alignment'),
+ 'align': ('KAPI_CONSTRAINT_ALIGNMENT', 'param-alignment'),
+ 'power_of_two': ('KAPI_CONSTRAINT_POWER_OF_TWO', None),
+ 'page_aligned': ('KAPI_CONSTRAINT_PAGE_ALIGNED', None),
+ 'nonzero': ('KAPI_CONSTRAINT_NONZERO', None),
+ 'user_string': ('KAPI_CONSTRAINT_USER_STRING', 'param-size'),
+ 'user_path': ('KAPI_CONSTRAINT_USER_PATH', None),
+ 'user_ptr': ('KAPI_CONSTRAINT_USER_PTR', None),
+ 'buffer': ('KAPI_CONSTRAINT_BUFFER', 'param-size-param'),
+ 'custom': ('KAPI_CONSTRAINT_CUSTOM', 'param-constraint'),
+ }
+ if key not in table:
+ return None
+ ctype, aux_key = table[key]
+ extras = {}
+ if aux_key and args_raw is not None:
+ extras[aux_key] = args_raw
+ return ctype, extras
+
+
+class ApiSpecFormat(OutputFormat):
+ """Generate C macro invocations for kernel API specifications"""
+
+ def __init__(self):
+ super().__init__()
+ self.header_written = False
+
+ def msg(self, fname, name, args):
+ """Handles a single entry from kernel-doc parser"""
+ if not self.header_written:
+ header = self._generate_header()
+ self.header_written = True
+ else:
+ header = ""
+
+ self.data = ""
+ result = super().msg(fname, name, args)
+ return header + (result if result else self.data)
+
+ def _generate_header(self):
+ """Generate the file header"""
+ return (
+ "/* SPDX-License-Identifier: GPL-2.0 */\n"
+ "/* Auto-generated from kerneldoc annotations - DO NOT EDIT */\n\n"
+ "#include <linux/kernel_api_spec.h>\n"
+ "#include <linux/errno.h>\n\n"
+ )
+
+ def _format_macro_param(self, value, max_len=KAPI_MAX_DESC_LEN):
+ """Format a value for use in C macro parameter.
+
+ Pass max_len=0 (or a false-y value) to disable truncation -- used for
+ free-form fields like long_description/notes/examples where the kernel
+ stores a pointer to .rodata rather than a fixed-size buffer.
+ """
+ if value is None:
+ return '""'
+ value = str(value).replace('\\', '\\\\').replace('"', '\\"')
+ value = value.replace('\n', ' ').replace('\t', ' ').replace('\r', '')
+ value = value.replace('\0', '')
+ if max_len and len(value) > max_len - 1:
+ sys.stderr.write(
+ f"kdoc_apispec: truncating field to {max_len} chars "
+ f"(original length {len(value)})\n"
+ )
+ value = value[:max_len - 4] + '...'
+ return f'"{value}"'
+
+ def _get_section(self, sections, key):
+ """Get first line from sections, checking with and without @ prefix and case variants"""
+ for variant in [key, key.capitalize(), key.title()]:
+ for prefix in ['', '@']:
+ full_key = prefix + variant
+ if full_key in sections:
+ content = sections[full_key].strip()
+ # Return only first line to avoid mixing sections
+ return content.split('\n')[0].strip() if content else ''
+ return None
+
+ def _get_raw_section(self, sections, key):
+ """Get full section content, checking with and without @ prefix and case variants"""
+ for variant in [key, key.capitalize(), key.title()]:
+ for prefix in ['', '@']:
+ full_key = prefix + variant
+ if full_key in sections:
+ return sections[full_key]
+ return ''
+
+ def _get_multiline_section(self, sections, key):
+ """Get full multi-line section content, joined into a single string.
+
+ This is used for fields like notes, long-desc, and examples that
+ can span multiple lines in the kerneldoc comment.
+ """
+ content = self._get_raw_section(sections, key)
+ if not content:
+ return None
+
+ # Split into lines, strip each, and join with space
+ lines = content.strip().split('\n')
+ # Join lines, preserving paragraph breaks (double newlines become single space)
+ result = ' '.join(line.strip() for line in lines if line.strip())
+ return result if result else None
+
+ def _parse_indented_items(self, section_content, item_parser):
+ """Generic parser for indented items.
+
+ Args:
+ section_content: Raw section content
+ item_parser: Function that takes (lines, start_index) and returns (item, next_index)
+
+ Returns:
+ List of parsed items
+ """
+ if not section_content:
+ return []
+
+ items = []
+ lines = section_content.strip().split('\n')
+ i = 0
+
+ while i < len(lines):
+ if not lines[i].strip():
+ i += 1
+ continue
+
+ # Check if this is a main item (not indented)
+ if not lines[i].startswith((' ', '\t')):
+ item, i = item_parser(lines, i)
+ if item:
+ items.append(item)
+ else:
+ i += 1
+
+ return items
+
+ def _parse_subfields(self, lines, start_idx):
+ """Parse indented subfields starting from start_idx+1.
+
+ Returns: (dict of subfields, next index)
+ """
+ subfields = {}
+ i = start_idx + 1
+
+ current_key = None
+ while i < len(lines) and (lines[i].startswith((' ', '\t'))):
+ line = lines[i].strip()
+ if ':' in line:
+ key, value = line.split(':', 1)
+ current_key = key.strip()
+ subfields[current_key] = value.strip()
+ elif current_key and line:
+ subfields[current_key] += ' ' + line
+ i += 1
+
+ return subfields, i
+
+ def _parse_signal_item(self, lines, i):
+ """Parse a single signal specification"""
+ signal = {'name': lines[i].strip()}
+ subfields, next_i = self._parse_subfields(lines, i)
+
+ # `direction` and `timing` are bitmasks of KAPI_SIGNAL_* /
+ # KAPI_SIGNAL_TIME_* values; `action` is a single
+ # KAPI_SIGNAL_ACTION_* enum. All three canonicalise aliases to
+ # their KAPI_* spelling.
+ raw_direction = subfields.get('direction', 'KAPI_SIGNAL_RECEIVE')
+ raw_action = subfields.get('action', 'KAPI_SIGNAL_ACTION_RETURN')
+ raw_timing = subfields.get('timing')
+ # `errno:` carries the signal's errno-on-return. The plain
+ # `error:` spelling cannot be used inside a signal block
+ # because kerneldoc promotes it to a top-level `error:` section.
+ signal.update({
+ 'direction': _canon_bitmask_expr(raw_direction, _SIGNAL_DIR_ALIASES),
+ 'action': _canon_token(raw_action, _SIGNAL_ACTION_ALIASES),
+ 'condition': subfields.get('condition'),
+ 'desc': subfields.get('desc'),
+ 'error': subfields.get('errno'),
+ 'timing': _canon_bitmask_expr(raw_timing, _SIGNAL_TIMING_ALIASES)
+ if raw_timing else None,
+ 'priority': subfields.get('priority'),
+ 'restartable': subfields.get('restartable', '').lower() == 'yes',
+ 'interruptible': subfields.get('interruptible', '').lower() == 'yes',
+ 'number': subfields.get('number', '0'),
+ # Additional struct fields. These are optional; if absent, no
+ # KAPI_SIGNAL_* macro is emitted and the field stays at its
+ # zero-initialised default.
+ 'target': subfields.get('target'),
+ 'queue': subfields.get('queue') or subfields.get('queue_behavior'),
+ 'transform': subfields.get('transform') or subfields.get('transform_to')
+ or subfields.get('transform-to'),
+ 'sa_flags_required': subfields.get('sa_flags_required')
+ or subfields.get('sa-flags-required'),
+ 'sa_flags_forbidden': subfields.get('sa_flags_forbidden')
+ or subfields.get('sa-flags-forbidden'),
+ 'state_required': subfields.get('state_required')
+ or subfields.get('state-required'),
+ 'state_forbidden': subfields.get('state_forbidden')
+ or subfields.get('state-forbidden'),
+ })
+
+ return signal, next_i
+
+ def _parse_error_item(self, lines, i):
+ """Parse a single error specification"""
+ line = lines[i].strip()
+
+ # Skip desc: lines
+ if line.startswith('desc:'):
+ return None, i + 1
+
+ # Check for error pattern
+ if not re.match(r'^[A-Z][A-Z0-9_]+,', line):
+ return None, i + 1
+
+ error = {'line': line, 'desc': ''}
+
+ # Look for desc: continuation
+ i += 1
+ desc_lines = []
+ while i < len(lines):
+ next_line = lines[i].strip()
+ if next_line.startswith('desc:'):
+ desc_lines.append(next_line[5:].strip())
+ i += 1
+ elif not next_line:
+ break
+ elif not desc_lines and re.match(r'^[A-Z][A-Z0-9_]+,', next_line):
+ # New error entry, but only if we haven't started a desc block
+ break
+ else:
+ desc_lines.append(next_line)
+ i += 1
+
+ if desc_lines:
+ error['desc'] = ' '.join(desc_lines)
+
+ return error, i
+
+ def _parse_lock_item(self, lines, i):
+ """Parse a single lock specification.
+
+ Two shapes are accepted:
+ * inline `NAME, TYPE` on the main line; or
+ * `NAME` on the main line with `type:` as an indented
+ subfield.
+ Lock-type values are canonicalised to KAPI_LOCK_* spellings.
+ """
+ head = lines[i].strip()
+ if not head:
+ return None, i + 1
+
+ parts = head.split(',', 1)
+ subfields, next_i = self._parse_subfields(lines, i)
+
+ name = parts[0].strip()
+ type_raw = (parts[1].strip() if len(parts) >= 2
+ else subfields.get('type', '').strip())
+ if not name or not type_raw:
+ return None, next_i
+
+ lock = {
+ 'name': name,
+ 'type': _canon_token(type_raw, _LOCK_TYPE_ALIASES),
+ }
+
+ for field in ['acquired', 'released', 'held-on-entry', 'held-on-exit']:
+ if subfields.get(field, '').lower() in ('true', 'yes'):
+ lock[field] = True
+
+ lock['desc'] = subfields.get('desc', '')
+
+ return lock, next_i
+
+ def _parse_constraint_item(self, lines, i):
+ """Parse a single constraint specification"""
+ line = lines[i].strip()
+
+ # Check for old format with comma
+ if ',' in line:
+ parts = line.split(',', 1)
+ constraint = {
+ 'name': parts[0].strip(),
+ 'desc': parts[1].strip() if len(parts) > 1 else '',
+ 'expr': None
+ }
+ else:
+ constraint = {'name': line, 'desc': '', 'expr': None}
+
+ subfields, next_i = self._parse_subfields(lines, i)
+
+ if 'desc' in subfields:
+ constraint['desc'] = (constraint['desc'] + ' ' + subfields['desc']).strip()
+ constraint['expr'] = subfields.get('expr')
+
+ return constraint, next_i
+
+ def _parse_side_effect_item(self, lines, i):
+ """Parse a single side effect specification"""
+ line = lines[i].strip()
+
+ # Default to new format
+ effect = {
+ 'type': line,
+ 'target': '',
+ 'desc': '',
+ 'condition': None,
+ 'reversible': False
+ }
+
+ # Check for old format with commas
+ if ',' in line:
+ # Handle condition and reversible flags
+ cond_match = re.search(r',\s*condition=([^,]+?)(?:\s*,\s*reversible=(yes|no)\s*)?$', line)
+ if cond_match:
+ effect['condition'] = cond_match.group(1).strip()
+ effect['reversible'] = cond_match.group(2) == 'yes'
+ line = line[:cond_match.start()]
+ elif ', reversible=yes' in line:
+ effect['reversible'] = True
+ line = line.replace(', reversible=yes', '')
+ elif ', reversible=no' in line:
+ line = line.replace(', reversible=no', '')
+
+ parts = line.split(',', 2)
+ if len(parts) >= 1:
+ effect['type'] = parts[0].strip()
+ if len(parts) >= 2:
+ effect['target'] = parts[1].strip()
+ if len(parts) >= 3:
+ effect['desc'] = parts[2].strip()
+ else:
+ # Multi-line format with subfields
+ subfields, next_i = self._parse_subfields(lines, i)
+ effect.update({
+ 'target': subfields.get('target', ''),
+ 'desc': subfields.get('desc', ''),
+ 'condition': subfields.get('condition'),
+ 'reversible': subfields.get('reversible', '').lower() == 'yes'
+ })
+ return effect, next_i
+
+ return effect, i + 1
+
+ def _parse_state_trans_item(self, lines, i):
+ """Parse a single state transition specification"""
+ line = lines[i].strip()
+
+ trans = {
+ 'target': line,
+ 'from': '',
+ 'to': '',
+ 'condition': '',
+ 'desc': ''
+ }
+
+ # Check for old format with commas
+ if ',' in line:
+ parts = line.split(',', 3)
+ if len(parts) >= 1:
+ trans['target'] = parts[0].strip()
+ if len(parts) >= 2:
+ trans['from'] = parts[1].strip()
+ if len(parts) >= 3:
+ trans['to'] = parts[2].strip()
+ if len(parts) >= 4:
+ desc_part = parts[3].strip()
+ desc_parts = desc_part.split(',', 1)
+ if len(desc_parts) > 1:
+ trans['condition'] = desc_parts[0].strip()
+ trans['desc'] = desc_parts[1].strip()
+ else:
+ trans['desc'] = desc_part
+ return trans, i + 1
+ else:
+ # Multi-line format with subfields
+ subfields, next_i = self._parse_subfields(lines, i)
+ trans.update({
+ 'from': subfields.get('from', ''),
+ 'to': subfields.get('to', ''),
+ 'condition': subfields.get('condition', ''),
+ 'desc': subfields.get('desc', '')
+ })
+ return trans, next_i
+
+ def _process_parameters(self, sections, parameterlist, parameterdescs, parametertypes):
+ """Process and output parameter specifications"""
+ param_count = len(parameterlist)
+ if param_count > 0:
+ self.data += f"\n\tKAPI_PARAM_COUNT({param_count})\n"
+
+ for param_idx, param in enumerate(parameterlist):
+ param_name = param.strip()
+ param_desc = parameterdescs.get(param_name, '')
+ param_ctype = parametertypes.get(param_name, '')
+
+ # Parse parameter specifications
+ param_section = self._get_raw_section(sections, 'param')
+ param_specs = {}
+ if param_section:
+ param_specs = self._parse_param_spec(param_section, param_name)
+
+ self.data += f"\n\tKAPI_PARAM({param_idx}, {self._format_macro_param(param_name)}, "
+ self.data += f"{self._format_macro_param(param_ctype)}, {self._format_macro_param(param_desc)})\n"
+
+ # Add parameter attributes
+ for key, macro in [
+ ('param-type', 'KAPI_PARAM_TYPE'),
+ ('param-flags', 'KAPI_PARAM_FLAGS'),
+ ('param-size', 'KAPI_PARAM_SIZE'),
+ ('param-alignment', 'KAPI_PARAM_ALIGNMENT'),
+ ]:
+ if key in param_specs:
+ self.data += f"\t\t{macro}({param_specs[key]})\n"
+
+ # Handle constraint type
+ if 'param-constraint-type' in param_specs:
+ ctype = param_specs['param-constraint-type']
+ if ctype == 'KAPI_CONSTRAINT_BITMASK':
+ ctype = 'KAPI_CONSTRAINT_MASK'
+ self.data += f"\t\tKAPI_PARAM_CONSTRAINT_TYPE({ctype})\n"
+
+ # Handle range
+ if 'param-range' in param_specs and ',' in param_specs['param-range']:
+ min_val, max_val = param_specs['param-range'].split(',', 1)
+ self.data += f"\t\tKAPI_PARAM_RANGE({min_val.strip()}, {max_val.strip()})\n"
+
+ # Handle mask
+ if 'param-mask' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_VALID_MASK({param_specs['param-mask']})\n"
+
+ # Handle enum values
+ if 'param-enum-values' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_ENUM_VALUES({param_specs['param-enum-values']})\n"
+
+ # Handle size parameter index
+ if 'param-size-param' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_SIZE_PARAM({param_specs['param-size-param']})\n"
+
+ # Handle constraint description
+ if 'param-constraint' in param_specs:
+ self.data += f"\t\tKAPI_PARAM_CONSTRAINT({self._format_macro_param(param_specs['param-constraint'])})\n"
+
+ self.data += "\t},\n"
+
+ def _parse_param_spec(self, section_content, param_name):
+ """Parse parameter specifications from indented format"""
+ specs = {}
+ lines = section_content.strip().split('\n')
+ current_item = None
+
+ # Map to expected keys
+ field_map = {
+ 'type': 'param-type',
+ 'flags': 'param-flags',
+ 'size': 'param-size',
+ 'constraint-type': 'param-constraint-type',
+ 'constraint': 'param-constraint',
+ 'cdesc': 'param-constraint',
+ 'range': 'param-range',
+ 'mask': 'param-mask',
+ 'valid-mask': 'param-mask',
+ 'valid-values': 'param-enum-values',
+ 'alignment': 'param-alignment',
+ 'size-param': 'param-size-param',
+ 'struct-type': 'param-struct-type',
+ }
+
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+ if not line.strip():
+ i += 1
+ continue
+
+ # Check if this is our parameter (non-indented line)
+ if not line.startswith((' ', '\t')):
+ parts = line.strip().split(',', 1)
+ current_item = param_name if parts[0].strip() == param_name else None
+ if current_item and len(parts) > 1:
+ specs['param-type'] = parts[1].strip()
+ i += 1
+ elif current_item == param_name:
+ # Parse subfield
+ stripped = line.strip()
+ if ':' in stripped:
+ key, value = stripped.split(':', 1)
+ key = key.strip()
+ value = value.strip()
+
+ # Collect continuation lines (indented lines without a colon that
+ # defines a new key, i.e., lines that are pure continuations)
+ i += 1
+ while i < len(lines):
+ next_line = lines[i]
+ # Stop if we hit a non-indented line (new param)
+ if next_line.strip() and not next_line.startswith((' ', '\t')):
+ break
+ next_stripped = next_line.strip()
+ # Stop if we hit a new key (contains colon with known key prefix)
+ if next_stripped and ':' in next_stripped:
+ potential_key = next_stripped.split(':', 1)[0].strip()
+ if potential_key in field_map or potential_key in ['type', 'desc']:
+ break
+ # This is a continuation line
+ if next_stripped:
+ value = value + ' ' + next_stripped
+ i += 1
+
+ if key in field_map:
+ # Clean up the value - remove excessive whitespace
+ value = ' '.join(value.split())
+ mapped = field_map[key]
+ if mapped == 'param-type':
+ # Single token sets the type; additional
+ # comma-separated tokens are flags OR'd
+ # into param-flags.
+ ty, extra_flags = _split_type_line(value)
+ if ty:
+ specs['param-type'] = ty
+ if extra_flags:
+ existing = specs.get('param-flags', '')
+ merged = (existing + ' | ' if existing else '') \
+ + ' | '.join(extra_flags)
+ specs['param-flags'] = merged
+ elif mapped == 'param-flags':
+ specs['param-flags'] = _canon_flags_expr(value)
+ elif mapped == 'param-constraint-type':
+ # Accepts a KAPI_CONSTRAINT_* token or a
+ # function-call expression like
+ # `range(0, 4096)` / `mask(0xff)` /
+ # `buffer(2)` that also populates the
+ # matching aux field.
+ parsed = _split_constraint_expr(value)
+ if parsed is not None:
+ ctype, extras = parsed
+ specs['param-constraint-type'] = ctype
+ for aux_k, aux_v in extras.items():
+ specs[aux_k] = aux_v
+ else:
+ specs['param-constraint-type'] = value
+ else:
+ specs[mapped] = value
+ else:
+ i += 1
+
+ return specs
+
+ def _validate_effect_type(self, effect_type):
+ """Validate and normalize effect type"""
+ if 'KAPI_EFFECT_SCHEDULER' in effect_type:
+ return effect_type.replace('KAPI_EFFECT_SCHEDULER', 'KAPI_EFFECT_SCHEDULE')
+
+ if 'KAPI_EFFECT_' in effect_type and effect_type not in VALID_EFFECT_TYPES:
+ if '|' in effect_type:
+ parts = [p.strip() for p in effect_type.split('|')]
+ valid_parts = []
+ for p in parts:
+ if p in VALID_EFFECT_TYPES:
+ valid_parts.append(p)
+ else:
+ import sys
+ print(f"warning: unrecognized effect type '{p}', "
+ f"defaulting to KAPI_EFFECT_MODIFY_STATE", file=sys.stderr)
+ valid_parts.append('KAPI_EFFECT_MODIFY_STATE')
+ return ' | '.join(valid_parts)
+ import sys
+ print(f"warning: unrecognized effect type '{effect_type}', "
+ f"defaulting to KAPI_EFFECT_MODIFY_STATE", file=sys.stderr)
+ return 'KAPI_EFFECT_MODIFY_STATE'
+
+ return effect_type
+
+ def _has_api_spec(self, sections):
+ """Check if this function has an API specification.
+
+ Returns True if at least 2 KAPI-specific section indicators are present.
+ We require 2+ indicators (not just 1) to avoid false positives from
+ regular kernel-doc comments that happen to use a common section name
+ like 'return' or 'error'. Having multiple KAPI sections strongly
+ suggests intentional API specification rather than coincidence.
+ """
+ indicators = [
+ 'api-type', 'context-flags', 'contexts',
+ 'param-type', 'error-code',
+ 'capability', 'signal', 'lock', 'state-trans', 'constraint',
+ 'side-effect', 'long-desc'
+ ]
+
+ count = sum(1 for ind in indicators
+ if any(key.lower().startswith(ind.lower()) or
+ key.lower().startswith('@' + ind.lower())
+ for key in sections.keys()))
+
+ # Require 2+ indicators to distinguish from regular kernel-doc
+ return count >= 2
+
+ def out_function(self, fname, name, args):
+ """Generate API spec for a function"""
+ function_name = args.get('function', name)
+ sections = args.sections if hasattr(args, 'sections') else args.get('sections', {})
+
+ if not self._has_api_spec(sections):
+ return
+
+ parameterlist = args.parameterlist if hasattr(args, 'parameterlist') else args.get('parameterlist', [])
+ parameterdescs = args.parameterdescs if hasattr(args, 'parameterdescs') else args.get('parameterdescs', {})
+ parametertypes = args.parametertypes if hasattr(args, 'parametertypes') else args.get('parametertypes', {})
+ purpose = args.get('purpose', '')
+
+ # Start macro invocation
+ self.data += f"DEFINE_KERNEL_API_SPEC({function_name})\n"
+
+ # Basic info
+ if purpose:
+ self.data += f"\tKAPI_DESCRIPTION({self._format_macro_param(purpose)})\n"
+
+ long_desc = self._get_multiline_section(sections, 'long-desc')
+ if long_desc:
+ # The kernel stores long_description as a pointer into .rodata,
+ # not a fixed-size buffer, so we do not truncate here.
+ self.data += f"\tKAPI_LONG_DESC({self._format_macro_param(long_desc, 0)})\n"
+
+ # Context flags. `contexts:`, `context-flags:`, and `context:`
+ # all work; tokens canonicalise to KAPI_CTX_* for KAPI_CONTEXT().
+ context = (self._get_section(sections, 'contexts')
+ or self._get_section(sections, 'context-flags')
+ or self._get_section(sections, 'context'))
+ if context:
+ self.data += f"\tKAPI_CONTEXT({_canon_context_expr(context)})\n"
+
+ # Process parameters
+ self._process_parameters(sections, parameterlist, parameterdescs, parametertypes)
+
+ # Process return value
+ self._process_return(sections)
+
+ # Process errors
+ errors = self._parse_indented_items(
+ self._get_raw_section(sections, 'error'),
+ self._parse_error_item
+ )
+
+ if errors:
+ self.data += f"\n\tKAPI_ERROR_COUNT({len(errors)})\n"
+
+ for idx, error in enumerate(errors):
+ self._output_error(idx, error)
+
+ # Process signals
+ signals = self._parse_indented_items(
+ self._get_raw_section(sections, 'signal'),
+ self._parse_signal_item
+ )
+
+ if signals:
+ self.data += f"\n\tKAPI_SIGNAL_COUNT({len(signals)})\n"
+
+ for idx, signal in enumerate(signals):
+ self._output_signal(idx, signal)
+
+ # Process other specifications
+ self._process_locks(sections)
+ self._process_constraints(sections)
+ self._process_side_effects(sections)
+ self._process_state_transitions(sections)
+ self._process_capabilities(sections)
+
+ # Add examples and notes. Like long_description, these are stored as
+ # pointers to .rodata on the kernel side, so no truncation is needed.
+ for key, macro in [
+ ('examples', 'KAPI_EXAMPLES'),
+ ('notes', 'KAPI_NOTES'),
+ ]:
+ value = self._get_multiline_section(sections, key)
+ if value:
+ self.data += f"\n\t{macro}({self._format_macro_param(value, 0)})\n"
+
+ self.data += "\n};\n\n"
+
+ def _process_return(self, sections):
+ """Process the return value specification from kerneldoc annotations"""
+ raw = self._get_raw_section(sections, 'return')
+ if not raw:
+ return
+
+ # Parse subfields from the return section, handling continuation lines
+ lines = raw.strip().split('\n')
+ subfields = {}
+ current_key = None
+ for line in lines:
+ stripped = line.strip()
+ if ':' in stripped and not line.startswith((' ', '\t')):
+ key, value = stripped.split(':', 1)
+ current_key = key.strip()
+ subfields[current_key] = value.strip()
+ elif current_key and stripped:
+ # Continuation line
+ subfields[current_key] += ' ' + stripped
+
+ ret_type = subfields.get('type', '')
+ check_type = subfields.get('check-type', '')
+ desc = subfields.get('desc', '')
+ success = subfields.get('success', '')
+
+ if not ret_type and not desc:
+ return
+
+ # Canonicalise short aliases:
+ # type: int -> KAPI_TYPE_INT
+ # check-type: fd -> KAPI_RETURN_FD
+ if ret_type:
+ ret_type = _canon_token(ret_type, _TYPE_ALIASES)
+ if check_type:
+ check_type = _canon_token(check_type, _RETURN_CHECK_ALIASES)
+
+ self.data += f"\n\tKAPI_RETURN({self._format_macro_param(ret_type)}, "
+ self.data += f"{self._format_macro_param(desc)})\n"
+
+ if ret_type:
+ self.data += f"\t\tKAPI_RETURN_TYPE({ret_type})\n"
+
+ if check_type:
+ self.data += f"\t\tKAPI_RETURN_CHECK_TYPE({check_type})\n"
+
+ if success and check_type == 'KAPI_RETURN_RANGE':
+ self.data += f"\t\tKAPI_RETURN_SUCCESS_RANGE(0, S64_MAX)\n"
+
+ self.data += "\t},\n"
+
+ def _output_error(self, idx, error):
+ """Output a single error specification"""
+ line = error['line']
+ if line.startswith('-'):
+ line = line[1:].strip()
+
+ parts = line.split(',', 2)
+ if len(parts) == 2:
+ # Format: NAME, description
+ name = parts[0].strip()
+ short_desc = parts[1].strip()
+ code = f"-{name}"
+ elif len(parts) >= 3:
+ # Format: code, name, description
+ code = parts[0].strip()
+ name = parts[1].strip()
+ short_desc = parts[2].strip()
+ if not code.startswith('-'):
+ code = f"-{code}"
+ else:
+ return
+
+ long_desc = error.get('desc', '') or short_desc
+
+ self.data += f"\n\tKAPI_ERROR({idx}, {code}, {self._format_macro_param(name)}, "
+ self.data += f"{self._format_macro_param(short_desc)},\n\t\t {self._format_macro_param(long_desc)})\n"
+
+ def _output_signal(self, idx, signal):
+ """Output a single signal specification"""
+ self.data += f"\n\tKAPI_SIGNAL({idx}, {signal['number']}, "
+ self.data += f"{self._format_macro_param(signal['name'], KAPI_MAX_SIGNAL_NAME_LEN)}, "
+ self.data += f"{signal['direction']}, {signal['action']})\n"
+
+ # String-valued subfields emitted as KAPI_SIGNAL_* macros.
+ if signal.get('condition'):
+ self.data += f"\t\tKAPI_SIGNAL_CONDITION({self._format_macro_param(signal['condition'])})\n"
+ if signal.get('desc'):
+ self.data += f"\t\tKAPI_SIGNAL_DESC({self._format_macro_param(signal['desc'])})\n"
+ if signal.get('error'):
+ # KAPI_SIGNAL_ERROR expects a numeric/token expression
+ # (e.g. -EINTR), not a quoted string.
+ self.data += f"\t\tKAPI_SIGNAL_ERROR({signal['error']})\n"
+
+ # Enum-valued subfields emitted as unquoted tokens.
+ if signal.get('timing'):
+ self.data += f"\t\tKAPI_SIGNAL_TIMING({signal['timing']})\n"
+ if signal.get('priority'):
+ self.data += f"\t\tKAPI_SIGNAL_PRIORITY({signal['priority']})\n"
+
+ # Boolean flag subfields.
+ if signal.get('restartable'):
+ self.data += "\t\tKAPI_SIGNAL_RESTARTABLE\n"
+ if signal.get('interruptible'):
+ self.data += "\t\tKAPI_SIGNAL_INTERRUPTIBLE\n"
+
+ # Additional struct fields. Emitted only when present in the
+ # kerneldoc so existing specs keep producing identical output.
+ if signal.get('target'):
+ self.data += f"\t\tKAPI_SIGNAL_TARGET({self._format_macro_param(signal['target'])})\n"
+ if signal.get('queue'):
+ self.data += f"\t\tKAPI_SIGNAL_QUEUE({self._format_macro_param(signal['queue'])})\n"
+ if signal.get('transform'):
+ # Numeric/token expression (e.g. SIGKILL), not a quoted string.
+ self.data += f"\t\tKAPI_SIGNAL_TRANSFORM({signal['transform']})\n"
+ if signal.get('sa_flags_required'):
+ self.data += f"\t\tKAPI_SIGNAL_SA_FLAGS_REQ({signal['sa_flags_required']})\n"
+ if signal.get('sa_flags_forbidden'):
+ self.data += f"\t\tKAPI_SIGNAL_SA_FLAGS_FORBID({signal['sa_flags_forbidden']})\n"
+ if signal.get('state_required'):
+ self.data += f"\t\tKAPI_SIGNAL_STATE_REQ({signal['state_required']})\n"
+ if signal.get('state_forbidden'):
+ self.data += f"\t\tKAPI_SIGNAL_STATE_FORBID({signal['state_forbidden']})\n"
+
+ self.data += "\t},\n"
+
+ def _process_locks(self, sections):
+ """Process lock specifications"""
+ locks = self._parse_indented_items(
+ self._get_raw_section(sections, 'lock'),
+ self._parse_lock_item
+ )
+
+ if locks:
+ self.data += f"\n\tKAPI_LOCK_COUNT({len(locks)})\n"
+
+ for idx, lock in enumerate(locks):
+ self.data += f"\n\tKAPI_LOCK({idx}, {self._format_macro_param(lock['name'])}, {lock['type']})\n"
+
+ # `.scope` is zero-initialised to KAPI_LOCK_INTERNAL
+ # (acquired-and-released). Emit KAPI_LOCK_ACQUIRED /
+ # KAPI_LOCK_RELEASED only when exactly one of the flags
+ # is true; emitting both would double-initialise `.scope`
+ # which breaks `-Werror=override-init` at W=1.
+ acquired = bool(lock.get('acquired'))
+ released = bool(lock.get('released'))
+ if acquired and not released:
+ self.data += "\t\tKAPI_LOCK_ACQUIRED\n"
+ elif released and not acquired:
+ self.data += "\t\tKAPI_LOCK_RELEASED\n"
+
+ if lock.get('desc'):
+ self.data += f"\t\tKAPI_LOCK_DESC({self._format_macro_param(lock['desc'])})\n"
+
+ self.data += "\t},\n"
+
+ def _process_constraints(self, sections):
+ """Process constraint specifications"""
+ constraints = self._parse_indented_items(
+ self._get_raw_section(sections, 'constraint'),
+ self._parse_constraint_item
+ )
+
+ if constraints:
+ self.data += f"\n\tKAPI_CONSTRAINT_COUNT({len(constraints)})\n"
+
+ for idx, constraint in enumerate(constraints):
+ self.data += f"\n\tKAPI_CONSTRAINT({idx}, {self._format_macro_param(constraint['name'])},\n"
+ self.data += f"\t\t\t{self._format_macro_param(constraint['desc'])})\n"
+
+ if constraint.get('expr'):
+ self.data += f"\t\tKAPI_CONSTRAINT_EXPR({self._format_macro_param(constraint['expr'])})\n"
+
+ self.data += "\t},\n"
+
+ def _process_side_effects(self, sections):
+ """Process side effect specifications"""
+ effects = self._parse_indented_items(
+ self._get_raw_section(sections, 'side-effect'),
+ self._parse_side_effect_item
+ )
+
+ if effects:
+ self.data += f"\n\tKAPI_SIDE_EFFECT_COUNT({len(effects)})\n"
+
+ for idx, effect in enumerate(effects):
+ # Canonicalise aliases (alloc_memory, modify_state, …)
+ # to KAPI_EFFECT_*. Accepts '|' or ',' as separators.
+ effect_type = _canon_bitmask_expr(effect['type'], _EFFECT_ALIASES)
+ effect_type = self._validate_effect_type(effect_type)
+
+ self.data += f"\n\tKAPI_SIDE_EFFECT({idx}, {effect_type},\n"
+ self.data += f"\t\t\t {self._format_macro_param(effect['target'])},\n"
+ self.data += f"\t\t\t {self._format_macro_param(effect['desc'])})\n"
+
+ if effect.get('condition'):
+ self.data += f"\t\tKAPI_EFFECT_CONDITION({self._format_macro_param(effect['condition'])})\n"
+
+ if effect.get('reversible'):
+ self.data += "\t\tKAPI_EFFECT_REVERSIBLE\n"
+
+ self.data += "\t},\n"
+
+ def _process_state_transitions(self, sections):
+ """Process state transition specifications"""
+ transitions = self._parse_indented_items(
+ self._get_raw_section(sections, 'state-trans'),
+ self._parse_state_trans_item
+ )
+
+ if transitions:
+ self.data += f"\n\tKAPI_STATE_TRANS_COUNT({len(transitions)})\n"
+
+ for idx, trans in enumerate(transitions):
+ desc = trans['desc']
+ if trans.get('condition'):
+ desc = trans['condition'] + (', ' + desc if desc else '')
+
+ self.data += f"\n\tKAPI_STATE_TRANS({idx}, {self._format_macro_param(trans['target'])}, "
+ self.data += f"{self._format_macro_param(trans['from'])}, {self._format_macro_param(trans['to'])},\n"
+ self.data += f"\t\t\t {self._format_macro_param(desc)})\n"
+ self.data += "\t},\n"
+
+ def _process_capabilities(self, sections):
+ """Process capability specifications"""
+ cap_section = self._get_raw_section(sections, 'capability')
+ if not cap_section:
+ return
+
+ lines = cap_section.strip().split('\n')
+ capabilities = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i].strip()
+ # Skip empty lines and subfield lines (they'll be parsed with their parent)
+ if not line or line.startswith(('allows:', 'without:', 'condition:', 'priority:', 'type:', 'desc:')):
+ i += 1
+ continue
+
+ cap_info = {'line': line}
+
+ # Parse subfields
+ subfields, next_i = self._parse_subfields(lines, i)
+ cap_info.update(subfields)
+ capabilities.append(cap_info)
+ i = next_i
+
+ if capabilities:
+ # Filter out "none" capabilities (no capability required)
+ valid_caps = [cap for cap in capabilities if cap['line'].strip().lower() != 'none']
+
+ if not valid_caps:
+ return
+
+ self.data += f"\n\tKAPI_CAPABILITY_COUNT({len(valid_caps)})\n"
+
+ for idx, cap in enumerate(valid_caps):
+ line = cap['line']
+ parts = line.split(',', 2)
+
+ # Handle both formats:
+ # 1. New format: "CAP_NAME" with type/desc as subfields
+ # 2. Old format: "CAP_NAME, TYPE, description"
+ if len(parts) >= 2:
+ # Old comma-separated format
+ cap_name = parts[0].strip()
+ cap_type = parts[1].strip()
+ cap_desc = parts[2].strip() if len(parts) > 2 else cap.get('desc', cap_name)
+ else:
+ # New subfield format - capability name on main line
+ cap_name = line.strip()
+ cap_type = cap.get('type', 'KAPI_CAP_PERFORM_OPERATION')
+ cap_desc = cap.get('desc', cap_name)
+
+ # Map capability type aliases to KAPI_CAP_* enum values.
+ cap_type_map = {
+ 'KAPI_CAP_REQUIRED': 'KAPI_CAP_PERFORM_OPERATION',
+ 'required': 'KAPI_CAP_PERFORM_OPERATION',
+ 'bypass': 'KAPI_CAP_BYPASS_CHECK',
+ 'grant': 'KAPI_CAP_GRANT_PERMISSION',
+ 'override': 'KAPI_CAP_OVERRIDE_RESTRICTION',
+ 'access': 'KAPI_CAP_ACCESS_RESOURCE',
+ 'modify': 'KAPI_CAP_MODIFY_BEHAVIOR',
+ 'limit': 'KAPI_CAP_INCREASE_LIMIT',
+ 'bypass_check': 'KAPI_CAP_BYPASS_CHECK',
+ 'increase_limit': 'KAPI_CAP_INCREASE_LIMIT',
+ 'override_restriction': 'KAPI_CAP_OVERRIDE_RESTRICTION',
+ 'grant_permission': 'KAPI_CAP_GRANT_PERMISSION',
+ 'modify_behavior': 'KAPI_CAP_MODIFY_BEHAVIOR',
+ 'access_resource': 'KAPI_CAP_ACCESS_RESOURCE',
+ 'perform_operation': 'KAPI_CAP_PERFORM_OPERATION',
+ }
+ cap_type = cap_type_map.get(cap_type, cap_type)
+
+ # Fix common type issues
+ if 'BYPASS' in cap_type and cap_type != 'KAPI_CAP_BYPASS_CHECK':
+ cap_type = 'KAPI_CAP_BYPASS_CHECK'
+
+ # Ensure cap_type is a valid enum
+ valid_types = [
+ 'KAPI_CAP_BYPASS_CHECK', 'KAPI_CAP_INCREASE_LIMIT',
+ 'KAPI_CAP_OVERRIDE_RESTRICTION', 'KAPI_CAP_GRANT_PERMISSION',
+ 'KAPI_CAP_MODIFY_BEHAVIOR', 'KAPI_CAP_ACCESS_RESOURCE',
+ 'KAPI_CAP_PERFORM_OPERATION'
+ ]
+ if cap_type not in valid_types:
+ cap_type = 'KAPI_CAP_PERFORM_OPERATION'
+
+ self.data += f"\n\tKAPI_CAPABILITY({idx}, {cap_name}, {self._format_macro_param(cap_desc)}, {cap_type})\n"
+
+ for key, macro in [
+ ('allows', 'KAPI_CAP_ALLOWS'),
+ ('without', 'KAPI_CAP_WITHOUT'),
+ ('condition', 'KAPI_CAP_CONDITION'),
+ ('priority', 'KAPI_CAP_PRIORITY'),
+ ]:
+ if cap.get(key):
+ value = self._format_macro_param(cap[key]) if key != 'priority' else cap[key]
+ self.data += f"\t\t{macro}({value})\n"
+
+ self.data += "\t},\n"
+
+ # Skip output methods for non-function types
+ def out_enum(self, fname, name, args): pass
+ def out_typedef(self, fname, name, args): pass
+ def out_struct(self, fname, name, args): pass
+ def out_doc(self, fname, name, args): pass
diff --git a/tools/lib/python/kdoc/kdoc_output.py b/tools/lib/python/kdoc/kdoc_output.py
index 4210b91dde5f1..cd91a4f59f275 100644
--- a/tools/lib/python/kdoc/kdoc_output.py
+++ b/tools/lib/python/kdoc/kdoc_output.py
@@ -129,8 +129,13 @@ class OutputFormat:
Output warnings for identifiers that will be displayed.
"""
- for log_msg in args.warnings:
- self.config.warning(log_msg)
+ warnings = getattr(args, 'warnings', [])
+
+ for log_msg in warnings:
+ # Skip numeric warnings (line numbers) which are false positives
+ # from parameter-specific sections like "param-constraint: name, value"
+ if not isinstance(log_msg, int):
+ self.config.warning(log_msg)
def check_doc(self, name, args):
"""Check if DOC should be output."""
diff --git a/tools/lib/python/kdoc/kdoc_parser.py b/tools/lib/python/kdoc/kdoc_parser.py
index ca00695b47b31..daf17f535b1ee 100644
--- a/tools/lib/python/kdoc/kdoc_parser.py
+++ b/tools/lib/python/kdoc/kdoc_parser.py
@@ -28,6 +28,23 @@ from kdoc.kdoc_item import KdocItem
# Allow whitespace at end of comment start.
doc_start = KernRe(r'^/\*\*\s*$', cache=False)
+# Sections that are allowed to be duplicated for API specifications
+# These represent lists of items (multiple errors, signals, etc.)
+ALLOWED_DUPLICATE_SECTIONS = {
+ 'param', '@param',
+ 'error', '@error',
+ 'signal', '@signal',
+ 'lock', '@lock',
+ 'side-effect', '@side-effect',
+ 'state-trans', '@state-trans',
+ 'capability', '@capability',
+ 'constraint', '@constraint',
+ 'validation-group', '@validation-group',
+ 'validation-rule', '@validation-rule',
+ 'validation-flag', '@validation-flag',
+ 'struct-field', '@struct-field',
+}
+
doc_end = KernRe(r'\*/', cache=False)
doc_com = KernRe(r'\s*\*\s*', cache=False)
doc_com_body = KernRe(r'\s*\* ?', cache=False)
@@ -40,10 +57,71 @@ doc_decl = doc_com + KernRe(r'(\w+)', cache=False)
# @{section-name}:
# while trying to not match literal block starts like "example::"
#
+# Base kernel-doc section names
known_section_names = 'description|context|returns?|notes?|examples?'
-known_sections = KernRe(known_section_names, flags = re.I)
+
+# API specification section names (for KAPI spec framework)
+# Format: (base_name, has_count_variant, has_other_variants)
+# Sections with has_count_variant=True need negative lookahead in doc_sect
+# to avoid matching 'error' when 'error-count' is intended
+_kapi_base_sections = [
+ # (name, needs_lookahead, additional_variants)
+ ('api-type', False, []),
+ ('api-version', False, []),
+ ('param', True, []), # has param-count
+ ('struct', True, ['struct-type', 'struct-field', 'struct-field-[a-z\\-]+']),
+ ('validation-group', False, []),
+ ('validation-policy', False, []),
+ ('validation-flag', False, []),
+ ('validation-rule', False, []),
+ ('error', True, ['error-code', 'error-condition']),
+ ('capability', True, []),
+ ('signal', True, []),
+ ('lock', True, []),
+ ('context-flags', False, []),
+ ('contexts', False, []),
+ ('return', True, ['return-type', 'return-check', 'return-check-type',
+ 'return-success', 'return-desc']),
+ ('long-desc', False, []),
+ ('constraint', True, []),
+ ('side-effect', True, []),
+ ('state-trans', True, []),
+]
+
+def _build_kapi_patterns():
+ """Build KAPI section patterns from the base definitions."""
+ validation_parts = [] # For known_sections (simple validation)
+ parsing_parts = [] # For doc_sect (with negative lookaheads)
+
+ for name, has_count, variants in _kapi_base_sections:
+ # Add base name (with optional @ prefix)
+ validation_parts.append(f'@?{name}')
+ if has_count:
+ # Need negative lookahead to not match 'name-count' or 'name-*'
+ parsing_parts.append(f'@?{name}(?!-)')
+ validation_parts.append(f'@?{name}-count')
+ parsing_parts.append(f'@?{name}-count')
+ else:
+ parsing_parts.append(f'@?{name}')
+
+ # Add variants
+ for variant in variants:
+ validation_parts.append(f'@?{variant}')
+ parsing_parts.append(f'@?{variant}')
+
+ # Add catch-all for kapi-* extensions
+ validation_parts.append(r'@?kapi-.*')
+ parsing_parts.append(r'@?kapi-.*')
+
+ return '|'.join(validation_parts), '|'.join(parsing_parts)
+
+_kapi_validation_pattern, _kapi_parsing_pattern = _build_kapi_patterns()
+
+known_sections = KernRe(known_section_names + '|' + _kapi_validation_pattern,
+ flags=re.I)
doc_sect = doc_com + \
- KernRe(r'\s*(@[.\w]+|@\.\.\.|' + known_section_names + r')\s*:([^:].*)?$',
+ KernRe(r'\s*(@[.\w\-]+|@\.\.\.|' + known_section_names + '|' +
+ _kapi_parsing_pattern + r')\s*:([^:].*)?$',
flags=re.I, cache=False)
doc_content = doc_com_body + KernRe(r'(.*)', cache=False)
@@ -349,7 +427,9 @@ class KernelEntry:
else:
if name in self.sections and self.sections[name] != "":
# Only warn on user-specified duplicate section names
- if name != SECTION_DEFAULT:
+ # Skip warning for sections that are expected to have duplicates
+ # (like error, param, signal, etc. for API specifications)
+ if name != SECTION_DEFAULT and name not in ALLOWED_DUPLICATE_SECTIONS:
self.emit_msg(self.new_start_line,
f"duplicate section name '{name}'")
# Treat as a new paragraph - add a blank line
--
2.53.0
next prev parent reply other threads:[~2026-04-24 16:51 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-24 16:51 [PATCH v3 0/9] Kernel API Specification Framework Sasha Levin
2026-04-24 16:51 ` [PATCH v3 1/9] kernel/api: introduce kernel API specification framework Sasha Levin
2026-04-24 16:51 ` Sasha Levin [this message]
2026-04-24 16:51 ` [PATCH v3 3/9] kernel/api: add debugfs interface for kernel API specifications Sasha Levin
2026-04-24 16:51 ` [PATCH v3 4/9] tools/kapi: add kernel API specification extraction tool Sasha Levin
2026-04-24 16:51 ` [PATCH v3 5/9] kernel/api: add API specification for sys_open Sasha Levin
2026-04-24 16:51 ` [PATCH v3 6/9] kernel/api: add API specification for sys_close Sasha Levin
2026-04-24 16:51 ` [PATCH v3 7/9] kernel/api: add API specification for sys_read Sasha Levin
2026-04-24 16:51 ` [PATCH v3 8/9] kernel/api: add API specification for sys_write Sasha Levin
2026-04-24 16:51 ` [PATCH v3 9/9] kernel/api: add runtime verification selftest Sasha Levin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260424165130.2306833-3-sashal@kernel.org \
--to=sashal@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=arnd@arndb.de \
--cc=brauner@kernel.org \
--cc=chrubis@suse.cz \
--cc=corbet@lwn.net \
--cc=david.laight.linux@gmail.com \
--cc=dvyukov@google.com \
--cc=gpaoloni@redhat.com \
--cc=gregkh@linuxfoundation.org \
--cc=jake@lwn.net \
--cc=kees@kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=masahiroy@kernel.org \
--cc=mchehab@kernel.org \
--cc=mingo@redhat.com \
--cc=paulmck@kernel.org \
--cc=rdunlap@infradead.org \
--cc=safinaskar@zohomail.com \
--cc=skhan@linuxfoundation.org \
--cc=tglx@kernel.org \
--cc=tools@kernel.org \
--cc=viro@zeniv.linux.org.uk \
--cc=workflows@vger.kernel.org \
--cc=x86@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox