scripts/bpf_doc.py: implement json output format
authorIhor Solodrai <isolodrai@meta.com>
Thu, 8 May 2025 20:37:08 +0000 (13:37 -0700)
committerAlexei Starovoitov <ast@kernel.org>
Fri, 9 May 2025 18:31:23 +0000 (11:31 -0700)
bpf_doc.py parses bpf.h header to collect information about various
API elements (such as BPF helpers) and then dump them in one of the
supported formats: rst docs and a C header.

It's useful for external tools to be able to consume this information
in an easy-to-parse format such as JSON. Implement JSON printers and
add --json command line argument.

v3->v4: refactor attrs to only be a helper's field
v2->v3: nit cleanup
v1->v2: add json printer for syscall target

v3: https://lore.kernel.org/bpf/20250507203034.270428-1-isolodrai@meta.com/
v2: https://lore.kernel.org/bpf/20250507182802.3833349-1-isolodrai@meta.com/
v1: https://lore.kernel.org/bpf/20250506000605.497296-1-isolodrai@meta.com/

Signed-off-by: Ihor Solodrai <isolodrai@meta.com>
Tested-by: Quentin Monnet <qmo@kernel.org>
Reviewed-by: Quentin Monnet <qmo@kernel.org>
Link: https://lore.kernel.org/r/20250508203708.2520847-1-isolodrai@meta.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
scripts/bpf_doc.py

index e74a01a850706d24496a65d363de09f15526d035..c77dc40f7689eaf85987714a48ac67f159c54361 100755 (executable)
@@ -8,6 +8,7 @@
 from __future__ import print_function
 
 import argparse
+import json
 import re
 import sys, os
 import subprocess
@@ -37,11 +38,17 @@ class APIElement(object):
     @desc: textual description of the symbol
     @ret: (optional) description of any associated return value
     """
-    def __init__(self, proto='', desc='', ret='', attrs=[]):
+    def __init__(self, proto='', desc='', ret=''):
         self.proto = proto
         self.desc = desc
         self.ret = ret
-        self.attrs = attrs
+
+    def to_dict(self):
+        return {
+            'proto': self.proto,
+            'desc': self.desc,
+            'ret': self.ret
+        }
 
 
 class Helper(APIElement):
@@ -51,8 +58,9 @@ class Helper(APIElement):
     @desc: textual description of the helper function
     @ret: description of the return value of the helper function
     """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def __init__(self, proto='', desc='', ret='', attrs=[]):
+        super().__init__(proto, desc, ret)
+        self.attrs = attrs
         self.enum_val = None
 
     def proto_break_down(self):
@@ -81,6 +89,12 @@ class Helper(APIElement):
 
         return res
 
+    def to_dict(self):
+        d = super().to_dict()
+        d["attrs"] = self.attrs
+        d.update(self.proto_break_down())
+        return d
+
 
 ATTRS = {
     '__bpf_fastcall': 'bpf_fastcall'
@@ -675,7 +689,7 @@ COMMANDS
         self.print_elem(command)
 
 
-class PrinterHelpers(Printer):
+class PrinterHelpersHeader(Printer):
     """
     A printer for dumping collected information about helpers as C header to
     be included from BPF program.
@@ -896,6 +910,43 @@ class PrinterHelpers(Printer):
         print(') = (void *) %d;' % helper.enum_val)
         print('')
 
+
+class PrinterHelpersJSON(Printer):
+    """
+    A printer for dumping collected information about helpers as a JSON file.
+    @parser: A HeaderParser with Helper objects
+    """
+
+    def __init__(self, parser):
+        self.elements = parser.helpers
+        self.elem_number_check(
+            parser.desc_unique_helpers,
+            parser.define_unique_helpers,
+            "helper",
+            "___BPF_FUNC_MAPPER",
+        )
+
+    def print_all(self):
+        helper_dicts = [helper.to_dict() for helper in self.elements]
+        out_dict = {'helpers': helper_dicts}
+        print(json.dumps(out_dict, indent=4))
+
+
+class PrinterSyscallJSON(Printer):
+    """
+    A printer for dumping collected syscall information as a JSON file.
+    @parser: A HeaderParser with APIElement objects
+    """
+
+    def __init__(self, parser):
+        self.elements = parser.commands
+        self.elem_number_check(parser.desc_syscalls, parser.enum_syscalls, 'syscall', 'bpf_cmd')
+
+    def print_all(self):
+        syscall_dicts = [syscall.to_dict() for syscall in self.elements]
+        out_dict = {'syscall': syscall_dicts}
+        print(json.dumps(out_dict, indent=4))
+
 ###############################################################################
 
 # If script is launched from scripts/ from kernel tree and can access
@@ -905,9 +956,17 @@ script = os.path.abspath(sys.argv[0])
 linuxRoot = os.path.dirname(os.path.dirname(script))
 bpfh = os.path.join(linuxRoot, 'include/uapi/linux/bpf.h')
 
+# target -> output format -> printer
 printers = {
-        'helpers': PrinterHelpersRST,
-        'syscall': PrinterSyscallRST,
+    'helpers': {
+        'rst': PrinterHelpersRST,
+        'json': PrinterHelpersJSON,
+        'header': PrinterHelpersHeader,
+    },
+    'syscall': {
+        'rst': PrinterSyscallRST,
+        'json': PrinterSyscallJSON
+    },
 }
 
 argParser = argparse.ArgumentParser(description="""
@@ -917,6 +976,8 @@ rst2man utility.
 """)
 argParser.add_argument('--header', action='store_true',
                        help='generate C header file')
+argParser.add_argument('--json', action='store_true',
+                       help='generate a JSON')
 if (os.path.isfile(bpfh)):
     argParser.add_argument('--filename', help='path to include/uapi/linux/bpf.h',
                            default=bpfh)
@@ -924,17 +985,35 @@ else:
     argParser.add_argument('--filename', help='path to include/uapi/linux/bpf.h')
 argParser.add_argument('target', nargs='?', default='helpers',
                        choices=printers.keys(), help='eBPF API target')
-args = argParser.parse_args()
-
-# Parse file.
-headerParser = HeaderParser(args.filename)
-headerParser.run()
 
-# Print formatted output to standard output.
-if args.header:
-    if args.target != 'helpers':
-        raise NotImplementedError('Only helpers header generation is supported')
-    printer = PrinterHelpers(headerParser)
-else:
-    printer = printers[args.target](headerParser)
-printer.print_all()
+def error_die(message: str):
+    argParser.print_usage(file=sys.stderr)
+    print('Error: {}'.format(message), file=sys.stderr)
+    exit(1)
+
+def parse_and_dump():
+    args = argParser.parse_args()
+
+    # Parse file.
+    headerParser = HeaderParser(args.filename)
+    headerParser.run()
+
+    if args.header and args.json:
+        error_die('Use either --header or --json, not both')
+
+    output_format = 'rst'
+    if args.header:
+        output_format = 'header'
+    elif args.json:
+        output_format = 'json'
+
+    try:
+        printer = printers[args.target][output_format](headerParser)
+        # Print formatted output to standard output.
+        printer.print_all()
+    except KeyError:
+        error_die('Unsupported target/format combination: "{}", "{}"'
+                    .format(args.target, output_format))
+
+if __name__ == "__main__":
+    parse_and_dump()