diff options
author | Laurent Pinchart <laurent.pinchart@ideasonboard.com> | 2024-08-08 18:13:00 +0300 |
---|---|---|
committer | Laurent Pinchart <laurent.pinchart@ideasonboard.com> | 2024-08-15 23:59:08 +0300 |
commit | 50c92cc7e2924009ecab3e4004448b01d687707c (patch) | |
tree | c22b49816a3c79dae4727780962aa0928df42b52 /utils/codegen/ipc/generators/mojom_libcamera_generator.py | |
parent | d3bf27180ef1d91b86b7b87a2378e559eaff5455 (diff) |
meson: Move all code generation scripts to utils/codegen/
We have multiple code generation scripts in utils/, mixed with other
miscellaneous utilities, as well as a larger code base based on mojom in
utils/ipc/. To make code sharing easier between the generator scripts,
without creating a mess in the utils/ directory, move all the code
generation code to utils/codegen/.
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
Diffstat (limited to 'utils/codegen/ipc/generators/mojom_libcamera_generator.py')
-rw-r--r-- | utils/codegen/ipc/generators/mojom_libcamera_generator.py | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/utils/codegen/ipc/generators/mojom_libcamera_generator.py b/utils/codegen/ipc/generators/mojom_libcamera_generator.py new file mode 100644 index 00000000..b8209e51 --- /dev/null +++ b/utils/codegen/ipc/generators/mojom_libcamera_generator.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2020, Google Inc. +# +# Author: Paul Elder <paul.elder@ideasonboard.com> +# +# Generates libcamera files from a mojom.Module. + +import argparse +import datetime +import os +import re + +import mojom.fileutil as fileutil +import mojom.generate.generator as generator +import mojom.generate.module as mojom +from mojom.generate.template_expander import UseJinja + + +GENERATOR_PREFIX = 'libcamera' + +_kind_to_cpp_type = { + mojom.BOOL: 'bool', + mojom.INT8: 'int8_t', + mojom.UINT8: 'uint8_t', + mojom.INT16: 'int16_t', + mojom.UINT16: 'uint16_t', + mojom.INT32: 'int32_t', + mojom.UINT32: 'uint32_t', + mojom.FLOAT: 'float', + mojom.INT64: 'int64_t', + mojom.UINT64: 'uint64_t', + mojom.DOUBLE: 'double', +} + +_bit_widths = { + mojom.BOOL: '8', + mojom.INT8: '8', + mojom.UINT8: '8', + mojom.INT16: '16', + mojom.UINT16: '16', + mojom.INT32: '32', + mojom.UINT32: '32', + mojom.FLOAT: '32', + mojom.INT64: '64', + mojom.UINT64: '64', + mojom.DOUBLE: '64', +} + +def ModuleName(path): + return path.split('/')[-1].split('.')[0] + +def ModuleClassName(module): + return re.sub(r'^IPA(.*)Interface$', lambda match: match.group(1), + module.interfaces[0].mojom_name) + +def Capitalize(name): + return name[0].upper() + name[1:] + +def ConstantStyle(name): + return generator.ToUpperSnakeCase(name) + +def Choose(cond, t, f): + return t if cond else f + +def CommaSep(l): + return ', '.join([m for m in l]) + +def ParamsCommaSep(l): + return ', '.join([m.mojom_name for m in l]) + +def GetDefaultValue(element): + if element.default is not None: + return element.default + if type(element.kind) == mojom.ValueKind: + return '0' + if IsFlags(element): + return '' + if mojom.IsEnumKind(element.kind): + return f'static_cast<{element.kind.mojom_name}>(0)' + if isinstance(element.kind, mojom.Struct) and \ + element.kind.mojom_name == 'SharedFD': + return '-1' + return '' + +def HasDefaultValue(element): + return GetDefaultValue(element) != '' + +def HasDefaultFields(element): + return True in [HasDefaultValue(x) for x in element.fields] + +def GetAllTypes(element): + if mojom.IsArrayKind(element): + return GetAllTypes(element.kind) + if mojom.IsMapKind(element): + return GetAllTypes(element.key_kind) + GetAllTypes(element.value_kind) + if isinstance(element, mojom.Parameter): + return GetAllTypes(element.kind) + if mojom.IsEnumKind(element): + return [element.mojom_name] + if not mojom.IsStructKind(element): + return [element.spec] + if len(element.fields) == 0: + return [element.mojom_name] + ret = [GetAllTypes(x.kind) for x in element.fields] + ret = [x for sublist in ret for x in sublist] + return list(set(ret)) + +def GetAllAttrs(element): + if mojom.IsArrayKind(element): + return GetAllAttrs(element.kind) + if mojom.IsMapKind(element): + return {**GetAllAttrs(element.key_kind), **GetAllAttrs(element.value_kind)} + if isinstance(element, mojom.Parameter): + return GetAllAttrs(element.kind) + if mojom.IsEnumKind(element): + return element.attributes if element.attributes is not None else {} + if mojom.IsStructKind(element) and len(element.fields) == 0: + return element.attributes if element.attributes is not None else {} + if not mojom.IsStructKind(element): + if hasattr(element, 'attributes'): + return element.attributes or {} + return {} + attrs = [(x.attributes) for x in element.fields] + ret = {} + for d in attrs: + ret.update(d or {}) + if hasattr(element, 'attributes'): + ret.update(element.attributes or {}) + return ret + +def NeedsControlSerializer(element): + types = GetAllTypes(element) + for type in ['ControlList', 'ControlInfoMap']: + if f'x:{type}' in types: + raise Exception(f'Unknown type "{type}" in {element.mojom_name}, did you mean "libcamera.{type}"?') + return "ControlList" in types or "ControlInfoMap" in types + +def HasFd(element): + attrs = GetAllAttrs(element) + if isinstance(element, mojom.Kind): + types = GetAllTypes(element) + else: + types = GetAllTypes(element.kind) + return "SharedFD" in types or (attrs is not None and "hasFd" in attrs) + +def WithDefaultValues(element): + return [x for x in element if HasDefaultValue(x)] + +def WithFds(element): + return [x for x in element if HasFd(x)] + +def MethodParamInputs(method): + return method.parameters + +def MethodParamOutputs(method): + if method.response_parameters is None: + return [] + + if MethodReturnValue(method) == 'void': + return method.response_parameters + + if len(method.response_parameters) <= 1: + return [] + + return method.response_parameters[1:] + +def MethodParamsHaveFd(parameters): + return len([x for x in parameters if HasFd(x)]) > 0 + +def MethodInputHasFd(method): + return MethodParamsHaveFd(method.parameters) + +def MethodOutputHasFd(method): + return MethodParamsHaveFd(MethodParamOutputs(method)) + +def MethodParamNames(method): + params = [] + for param in method.parameters: + params.append(param.mojom_name) + for param in MethodParamOutputs(method): + params.append(param.mojom_name) + return params + +def MethodParameters(method): + params = [] + for param in method.parameters: + params.append('const %s %s%s' % (GetNameForElement(param), + '' if IsPod(param) or IsEnum(param) else '&', + param.mojom_name)) + for param in MethodParamOutputs(method): + params.append(f'{GetNameForElement(param)} *{param.mojom_name}') + return params + +def MethodReturnValue(method): + if method.response_parameters is None or len(method.response_parameters) == 0: + return 'void' + first_output = method.response_parameters[0] + if ((len(method.response_parameters) == 1 and IsPod(first_output)) or + first_output.kind == mojom.INT32): + return GetNameForElement(first_output) + return 'void' + +def IsAsync(method): + # Events are always async + if re.match("^IPA.*EventInterface$", method.interface.mojom_name): + return True + elif re.match("^IPA.*Interface$", method.interface.mojom_name): + if method.attributes is None: + return False + elif 'async' in method.attributes and method.attributes['async']: + return True + return False + +def IsArray(element): + return mojom.IsArrayKind(element.kind) + +def IsControls(element): + return mojom.IsStructKind(element.kind) and (element.kind.mojom_name == "ControlList" or + element.kind.mojom_name == "ControlInfoMap") + +def IsEnum(element): + return mojom.IsEnumKind(element.kind) + + +# Only works the enum definition, not types +def IsScoped(element): + attributes = getattr(element, 'attributes', None) + if not attributes: + return False + return 'scopedEnum' in attributes + + +def IsEnumScoped(element): + if not IsEnum(element): + return False + return IsScoped(element.kind) + +def IsFd(element): + return mojom.IsStructKind(element.kind) and element.kind.mojom_name == "SharedFD" + + +def IsFlags(element): + attributes = getattr(element, 'attributes', None) + if not attributes: + return False + return 'flags' in attributes + +def IsMap(element): + return mojom.IsMapKind(element.kind) + +def IsPlainStruct(element): + return mojom.IsStructKind(element.kind) and not IsControls(element) and not IsFd(element) + +def IsPod(element): + return element.kind in _kind_to_cpp_type + +def IsStr(element): + return element.kind.spec == 's' + +def BitWidth(element): + if element.kind in _bit_widths: + return _bit_widths[element.kind] + if mojom.IsEnumKind(element.kind): + return '32' + return '' + +def ByteWidthFromCppType(t): + key = None + for mojo_type, cpp_type in _kind_to_cpp_type.items(): + if t == cpp_type: + key = mojo_type + if key is None: + raise Exception('invalid type') + return str(int(_bit_widths[key]) // 8) + +# Get the type name for a given element +def GetNameForElement(element): + # Flags + if IsFlags(element): + return f'Flags<{GetFullNameForElement(element.kind)}>' + # structs + if (mojom.IsEnumKind(element) or + mojom.IsInterfaceKind(element) or + mojom.IsStructKind(element)): + return element.mojom_name + # vectors + if (mojom.IsArrayKind(element)): + elem_name = GetFullNameForElement(element.kind) + return f'std::vector<{elem_name}>' + # maps + if (mojom.IsMapKind(element)): + key_name = GetFullNameForElement(element.key_kind) + value_name = GetFullNameForElement(element.value_kind) + return f'std::map<{key_name}, {value_name}>' + # struct fields and function parameters + if isinstance(element, (mojom.Field, mojom.Method, mojom.Parameter)): + # maps and vectors + if (mojom.IsArrayKind(element.kind) or mojom.IsMapKind(element.kind)): + return GetNameForElement(element.kind) + # strings + if (mojom.IsReferenceKind(element.kind) and element.kind.spec == 's'): + return 'std::string' + # PODs + if element.kind in _kind_to_cpp_type: + return _kind_to_cpp_type[element.kind] + # structs and enums + return element.kind.mojom_name + # PODs that are members of vectors/maps + if (hasattr(element, '__hash__') and element in _kind_to_cpp_type): + return _kind_to_cpp_type[element] + if (hasattr(element, 'spec')): + # strings that are members of vectors/maps + if (element.spec == 's'): + return 'std::string' + # structs that aren't defined in mojom that are members of vectors/maps + if (element.spec[0] == 'x'): + return element.spec.replace('x:', '').replace('.', '::') + if (mojom.IsInterfaceRequestKind(element) or + mojom.IsAssociatedKind(element) or + mojom.IsPendingRemoteKind(element) or + mojom.IsPendingReceiverKind(element) or + mojom.IsUnionKind(element)): + raise Exception('Unsupported element: %s' % element) + raise Exception('Unexpected element: %s' % element) + +def GetFullNameForElement(element): + name = GetNameForElement(element) + namespace_str = '' + if (mojom.IsStructKind(element) or mojom.IsEnumKind(element)): + namespace_str = element.module.mojom_namespace.replace('.', '::') + elif (hasattr(element, 'kind') and + (mojom.IsStructKind(element.kind) or mojom.IsEnumKind(element.kind))): + namespace_str = element.kind.module.mojom_namespace.replace('.', '::') + + if namespace_str == '': + return name + + if IsFlags(element): + return GetNameForElement(element) + + return f'{namespace_str}::{name}' + +def ValidateZeroLength(l, s, cap=True): + if l is None: + return + if len(l) > 0: + raise Exception(f'{s.capitalize() if cap else s} should be empty') + +def ValidateSingleLength(l, s, cap=True): + if len(l) > 1: + raise Exception(f'Only one {s} allowed') + if len(l) < 1: + raise Exception(f'{s.capitalize() if cap else s} is required') + +def GetMainInterface(interfaces): + intf = [x for x in interfaces + if re.match("^IPA.*Interface", x.mojom_name) and + not re.match("^IPA.*EventInterface", x.mojom_name)] + ValidateSingleLength(intf, 'main interface') + return None if len(intf) == 0 else intf[0] + +def GetEventInterface(interfaces): + event = [x for x in interfaces if re.match("^IPA.*EventInterface", x.mojom_name)] + ValidateSingleLength(event, 'event interface') + return None if len(event) == 0 else event[0] + +def ValidateNamespace(namespace): + if namespace == '': + raise Exception('Must have a namespace') + + if not re.match(r'^ipa\.[0-9A-Za-z_]+', namespace): + raise Exception('Namespace must be of the form "ipa.{pipeline_name}"') + +def ValidateInterfaces(interfaces): + # Validate presence of main interface + intf = GetMainInterface(interfaces) + if intf is None: + raise Exception('Must have main IPA interface') + + # Validate presence of event interface + event = GetEventInterface(interfaces) + if intf is None: + raise Exception('Must have event IPA interface') + + # Validate required main interface functions + f_init = [x for x in intf.methods if x.mojom_name == 'init'] + f_start = [x for x in intf.methods if x.mojom_name == 'start'] + f_stop = [x for x in intf.methods if x.mojom_name == 'stop'] + + ValidateSingleLength(f_init, 'init()', False) + ValidateSingleLength(f_start, 'start()', False) + ValidateSingleLength(f_stop, 'stop()', False) + + f_stop = f_stop[0] + + # No need to validate init() and start() as they are customizable + + # Validate parameters to stop() + ValidateZeroLength(f_stop.parameters, 'input parameter to stop()') + ValidateZeroLength(f_stop.parameters, 'output parameter from stop()') + + # Validate that event interface has at least one event + if len(event.methods) < 1: + raise Exception('Event interface must have at least one event') + + # Validate that all async methods don't have return values + intf_methods_async = [x for x in intf.methods if IsAsync(x)] + for method in intf_methods_async: + ValidateZeroLength(method.response_parameters, + f'{method.mojom_name} response parameters', False) + + event_methods_async = [x for x in event.methods if IsAsync(x)] + for method in event_methods_async: + ValidateZeroLength(method.response_parameters, + f'{method.mojom_name} response parameters', False) + +class Generator(generator.Generator): + @staticmethod + def GetTemplatePrefix(): + return 'libcamera_templates' + + def GetFilters(self): + libcamera_filters = { + 'all_types': GetAllTypes, + 'bit_width': BitWidth, + 'byte_width' : ByteWidthFromCppType, + 'cap': Capitalize, + 'choose': Choose, + 'comma_sep': CommaSep, + 'default_value': GetDefaultValue, + 'has_default_fields': HasDefaultFields, + 'has_fd': HasFd, + 'is_async': IsAsync, + 'is_array': IsArray, + 'is_controls': IsControls, + 'is_enum': IsEnum, + 'is_enum_scoped': IsEnumScoped, + 'is_fd': IsFd, + 'is_flags': IsFlags, + 'is_map': IsMap, + 'is_plain_struct': IsPlainStruct, + 'is_pod': IsPod, + 'is_scoped': IsScoped, + 'is_str': IsStr, + 'method_input_has_fd': MethodInputHasFd, + 'method_output_has_fd': MethodOutputHasFd, + 'method_param_names': MethodParamNames, + 'method_param_inputs': MethodParamInputs, + 'method_param_outputs': MethodParamOutputs, + 'method_parameters': MethodParameters, + 'method_return_value': MethodReturnValue, + 'name': GetNameForElement, + 'name_full': GetFullNameForElement, + 'needs_control_serializer': NeedsControlSerializer, + 'params_comma_sep': ParamsCommaSep, + 'with_default_values': WithDefaultValues, + 'with_fds': WithFds, + } + return libcamera_filters + + def _GetJinjaExports(self): + return { + 'cmd_enum_name': '_%sCmd' % self.module_name, + 'cmd_event_enum_name': '_%sEventCmd' % self.module_name, + 'consts': self.module.constants, + 'enums': self.module.enums, + 'has_array': len([x for x in self.module.kinds.keys() if x[0] == 'a']) > 0, + 'has_map': len([x for x in self.module.kinds.keys() if x[0] == 'm']) > 0, + 'has_namespace': self.module.mojom_namespace != '', + 'interface_event': GetEventInterface(self.module.interfaces), + 'interface_main': GetMainInterface(self.module.interfaces), + 'interface_name': 'IPA%sInterface' % self.module_name, + 'module_name': ModuleName(self.module.path), + 'namespace': self.module.mojom_namespace.split('.'), + 'namespace_str': self.module.mojom_namespace.replace('.', '::') if + self.module.mojom_namespace is not None else '', + 'proxy_name': 'IPAProxy%s' % self.module_name, + 'proxy_worker_name': 'IPAProxy%sWorker' % self.module_name, + 'structs_nonempty': [x for x in self.module.structs if len(x.fields) > 0], + } + + def _GetJinjaExportsForCore(self): + return { + 'consts': self.module.constants, + 'enums_gen_header': [x for x in self.module.enums if x.attributes is None or 'skipHeader' not in x.attributes], + 'has_array': len([x for x in self.module.kinds.keys() if x[0] == 'a']) > 0, + 'has_map': len([x for x in self.module.kinds.keys() if x[0] == 'm']) > 0, + 'structs_gen_header': [x for x in self.module.structs if x.attributes is None or 'skipHeader' not in x.attributes], + 'structs_gen_serializer': [x for x in self.module.structs if x.attributes is None or 'skipSerdes' not in x.attributes], + } + + @UseJinja('core_ipa_interface.h.tmpl') + def _GenerateCoreHeader(self): + return self._GetJinjaExportsForCore() + + @UseJinja('core_ipa_serializer.h.tmpl') + def _GenerateCoreSerializer(self): + return self._GetJinjaExportsForCore() + + @UseJinja('module_ipa_interface.h.tmpl') + def _GenerateDataHeader(self): + return self._GetJinjaExports() + + @UseJinja('module_ipa_serializer.h.tmpl') + def _GenerateSerializer(self): + return self._GetJinjaExports() + + @UseJinja('module_ipa_proxy.cpp.tmpl') + def _GenerateProxyCpp(self): + return self._GetJinjaExports() + + @UseJinja('module_ipa_proxy.h.tmpl') + def _GenerateProxyHeader(self): + return self._GetJinjaExports() + + @UseJinja('module_ipa_proxy_worker.cpp.tmpl') + def _GenerateProxyWorker(self): + return self._GetJinjaExports() + + def GenerateFiles(self, unparsed_args): + parser = argparse.ArgumentParser() + parser.add_argument('--libcamera_generate_core_header', action='store_true') + parser.add_argument('--libcamera_generate_core_serializer', action='store_true') + parser.add_argument('--libcamera_generate_header', action='store_true') + parser.add_argument('--libcamera_generate_serializer', action='store_true') + parser.add_argument('--libcamera_generate_proxy_cpp', action='store_true') + parser.add_argument('--libcamera_generate_proxy_h', action='store_true') + parser.add_argument('--libcamera_generate_proxy_worker', action='store_true') + parser.add_argument('--libcamera_output_path') + args = parser.parse_args(unparsed_args) + + if not args.libcamera_generate_core_header and \ + not args.libcamera_generate_core_serializer: + ValidateNamespace(self.module.mojom_namespace) + ValidateInterfaces(self.module.interfaces) + self.module_name = ModuleClassName(self.module) + + fileutil.EnsureDirectoryExists(os.path.dirname(args.libcamera_output_path)) + + gen_funcs = [ + [args.libcamera_generate_core_header, self._GenerateCoreHeader], + [args.libcamera_generate_core_serializer, self._GenerateCoreSerializer], + [args.libcamera_generate_header, self._GenerateDataHeader], + [args.libcamera_generate_serializer, self._GenerateSerializer], + [args.libcamera_generate_proxy_cpp, self._GenerateProxyCpp], + [args.libcamera_generate_proxy_h, self._GenerateProxyHeader], + [args.libcamera_generate_proxy_worker, self._GenerateProxyWorker], + ] + + for pair in gen_funcs: + if pair[0]: + self.Write(pair[1](), args.libcamera_output_path) |