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: 18+ 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-27 3:37 ` Nathan Chancellor
2026-04-29 16:32 ` Nicolas Schier
2026-05-05 7:45 ` Sasha Levin
2026-05-05 7:45 ` Sasha Levin
2026-04-29 16:43 ` Nicolas Schier
2026-05-05 7:45 ` Sasha Levin
2026-04-24 16:51 ` Sasha Levin [this message]
2026-04-30 14:47 ` [PATCH v3 2/9] kernel/api: enable kerneldoc-based API specifications Nicolas Schier
2026-05-05 7:45 ` Sasha Levin
2026-04-24 16:51 ` [PATCH v3 3/9] kernel/api: add debugfs interface for kernel " 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.