From: Peter Krempa <pkrempa@redhat.com> The script analyzes the driver implementation source file and generates an include file which describes the analyzed driver in terms of: - supported APIs - flags supported for the API (by looking at virCheckFlags) The generated structure then will be used to generate the introspection XML. The script goes through the 'virHypervisorDriver' struct, finds all callbacks corresponding to public APIs and then goes through the functions finding the 'virCheckFlags' to collect supported flags per API. Since the migration APIs are public but use internal functions which don't map directly, the script tries to find the best matching internal API and then infers the flags for the public migration APIs from the detected flags. The script works only with the contemporary coding style for functions due to regex usage so any driver impl file needs to be modernized first. As first example, introspection of qemu driver is generated. An excerpt from the generated data (which is for internal use, and will be used to generate XML): static const virIntrospectionData driver_api_introspection[] = { { .api = "virConnectBaselineCPU", .flags_arg = true, .flags = VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES | VIR_CONNECT_BASELINE_CPU_MIGRATABLE, }, { .api = "virConnectBaselineHypervisorCPU", .flags_arg = true, .flags = VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES | VIR_CONNECT_BASELINE_CPU_MIGRATABLE | VIR_CONNECT_BASELINE_CPU_IGNORE_HOST, }, { .api = "virConnectClose", .flags_arg = false, }, Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- scripts/genintrospection.py | 220 ++++++++++++++++++++++++++++++++++++ scripts/meson.build | 1 + src/qemu/meson.build | 18 +++ 3 files changed, 239 insertions(+) create mode 100644 scripts/genintrospection.py diff --git a/scripts/genintrospection.py b/scripts/genintrospection.py new file mode 100644 index 0000000000..25a17e2b0c --- /dev/null +++ b/scripts/genintrospection.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: LGPL-2.1-or-later + +import argparse +import re +import sys + + +# driver callbacks needed to infer the introspection for public migration APIs +# which do not map directly to any driver API callback. We need a representative +# sample of APIs supporting typed parameters and flags +# The APIs are ordered acco +migration_driver_symbols = [ + "domainMigrateBegin3Params", + "domainMigrateBegin3", + "domainMigratePrepare2", + "domainMigratePrepare", +] + +migration_public_api = { + "virDomainMigrate": {}, + "virDomainMigrateToURI": {}, + "virDomainMigrate2": {}, + "virDomainMigrateToURI2": {}, + "virDomainMigrate3": {"params": True}, + "virDomainMigrateToURI3": {"params": True}, +} + + +def load_public_symbols(filename): + """load the public symbol file and return all APIs""" + symbols = [] + + with open(filename, "r") as symfile: + while True: + line = symfile.readline() + + if not line: + break + + m = re.match(r"\s+(?P<sym>vir\w+);", line) + + if not m: + continue + + symbols.append(m.group("sym")) + + return symbols + + +def parse_api(filename, syms): + with open(filename, "r") as f: + content = f.read() + + # parse the definition of current driver's virHypervisorDriver struct + driverdef = re.search( + r"virHypervisorDriver.*?=\s+{(?P<apis>[^}]+)", + content, + flags=re.DOTALL | re.MULTILINE, + ) + + # parse all functions + # requires that the input file is well formed: + # - types of return value are on separate line including pointer symbol + # - function name starts on separate line followed by opening parenthesis for arguments + # - opening brace for body is on start of a separate line + # - closing brace for body is on start of a separate line + funciter = re.finditer( + r"^(?P<name>\w+)\((?P<args>[^)]*)\)\n^{(?P<impl>.*?)^}", + content, + flags=re.DOTALL | re.MULTILINE, + ) + + if not driverdef: + raise Exception(f"'virHypervisorDriver' definition not found in '{filename}'") + + # create mapping from public API names to local driver's callback names + apis = {} + # .connectOpen = qemuConnectOpen -> apis['qemuConnectOpen'] = 'virConnectOpen' + for api in re.finditer( + r"\.(?P<field>\w+)\s+=\s+(?P<callback>\w+)", driverdef.group("apis") + ): + # skip hypervisor driver name definition + if api.group("field") == "name": + continue + + # skip APIs that have had their implementation removed + if api.group("callback") == "NULL": + continue + + # connectOpen -> virConnectOpen + # for migration API we use the name in virHypervisorDriver + name = api.group("field") + apiname = None + + if name in migration_driver_symbols: + apis[api.group("callback")] = name + else: + apiname = "vir" + name[0:1].upper() + name[1:] + apis[api.group("callback")] = apiname + + flagmap = {} + migr_data = {} + # parse exported functions, presence of 'flags' argument and the 'virCheckFlags' expression + for f in funciter: + apiname = apis.get(f.group("name"), None) + migrationapi = False + + # process exported public APIs or migration handlers; migration handlers + # will be at the end handled separately + if not apiname: + continue + + if apiname in migration_driver_symbols: + migrationapi = True + elif apiname not in syms: + continue + + data = { + "callback": f.group("name"), + "flags_arg": False, + "flags_supported": None, + } + + flagarg = re.search(r"\bflags\b", f.group("args")) + if flagarg: + data["flags_arg"] = True + + flagcheck = re.search( + r"virCheckFlags(?:Goto)?\((?P<flags>[^,]+),", + f.group("impl"), + flags=re.DOTALL | re.MULTILINE, + ) + + if flagcheck: + data["flags_supported"] = re.sub(r"\s+", " ", flagcheck.group("flags")) + + if migrationapi: + migr_data[apiname] = data + else: + flagmap[apiname] = data + + # populate migration API data. To do this we want to find which internal + # APIs are supported and then populate the list based on the detected + # information + for m in migration_driver_symbols: + if m in migr_data: + for apiname, pub in migration_public_api.items(): + data = migr_data[m].copy() + flagmap[apiname] = data + + break + + return flagmap + + +parser = argparse.ArgumentParser( + description="Tool to generate data for 'virConnectIntrospect'" +) +parser.add_argument( + "--symfile", action="append", required=True, help="public symbol file(s)" +) +parser.add_argument("--driverfile", required=True, help="driver implementation file") +parser.add_argument("--output", required=True, help="output file") +args = parser.parse_args() + +syms = [] +for sf in args.symfile: + syms += load_public_symbols(sf) + +introspection = parse_api(args.driverfile, syms) + +fail = False + +with open(args.output, "w") as outfile: + outfile.write( + f"""/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Generated data for introspection of '{args.driverfile}'. + * This file is generated by '{parser.prog}' + */ +""" + ) + + outfile.write( + """ +static const virIntrospectionData driver_api_introspection[] = +{ +""" + ) + + for api in sorted(introspection.keys()): + data = introspection[api] + + outfile.write(f' {{ .api = "{api}",\n') + if data.get("flags_arg", False): + outfile.write(" .flags_arg = true,\n") + outfile.write(f" .flags = {data.get("flags_supported", 0)},\n") + else: + outfile.write(" .flags_arg = false,\n") + + outfile.write(" },\n") + + epilogue = """ { .api = NULL } +}; +""" + outfile.write(epilogue) + +for api, data in introspection.items(): + if ( + data.get("flags_arg", False) is True + and data.get("flags_supported", None) is None + ): + print(f"failed to parse flags for '{api}' in '{data.get("callback", "")}'") + fail = True + +if fail: + sys.exit(1) diff --git a/scripts/meson.build b/scripts/meson.build index 15a23e8738..bfb3eb7bf4 100644 --- a/scripts/meson.build +++ b/scripts/meson.build @@ -13,6 +13,7 @@ scripts = [ 'dtrace2systemtap.py', 'esx_vi_generator.py', 'genaclperms.py', + 'genintrospection.py', 'genpolkit.py', 'gensystemtap.py', 'group-qemu-caps.py', diff --git a/src/qemu/meson.build b/src/qemu/meson.build index b4fb62f14f..a6cc3d1a85 100644 --- a/src/qemu/meson.build +++ b/src/qemu/meson.build @@ -45,6 +45,23 @@ qemu_driver_sources = [ 'qemu_virtiofs.c', ] +introspection_files = custom_target( + 'qemu_introspection.inc.h', + output: 'qemu_introspection.inc.h', + input: [ + '../libvirt_public.syms', + '../libvirt_qemu.syms', + 'qemu_driver.c', + ], + command: [ + meson_python_prog, python3_prog, genintrospection_prog, + '--symfile', meson.project_source_root() / 'src' / 'libvirt_public.syms', + '--symfile', meson.project_source_root() / 'src' / 'libvirt_qemu.syms', + '--driverfile', meson.project_source_root() / 'src' / 'qemu' / 'qemu_driver.c', + '--output', '@OUTPUT@' + ] +) + driver_source_files += files(qemu_driver_sources) stateful_driver_source_files += files(qemu_driver_sources) @@ -95,6 +112,7 @@ if conf.has('WITH_QEMU') [ qemu_driver_sources, qemu_dtrace_gen_headers, + introspection_files, ], dependencies: [ access_dep, -- 2.54.0