From 587658f01e2006c2e42d946fde3ba72cccf73af9 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 25 Feb 2026 07:52:18 -0800 Subject: [PATCH 01/19] psg changes --- PluginSkeletonGenerator/CombinedPlugins.sh | 19 +- .../core/GeneratorCoordinator.py | 3 +- .../core/PluginBlueprint.py | 21 +- PluginSkeletonGenerator/data/FileData.py | 848 +++++++++++++----- .../generators/PluginRepositoryGenerator.py | 3 +- PluginSkeletonGenerator/menu/Menu.py | 150 ++-- PluginSkeletonGenerator/parser/Parser.py | 39 +- .../templates/.plugin_header.txt | 2 +- .../templates/.plugin_implementation.txt | 6 +- .../templates/.plugin_source.txt | 2 + PluginSkeletonGenerator/utils/FileUtils.py | 7 +- 11 files changed, 797 insertions(+), 303 deletions(-) diff --git a/PluginSkeletonGenerator/CombinedPlugins.sh b/PluginSkeletonGenerator/CombinedPlugins.sh index 2f0fb0ff..e313238b 100755 --- a/PluginSkeletonGenerator/CombinedPlugins.sh +++ b/PluginSkeletonGenerator/CombinedPlugins.sh @@ -56,15 +56,16 @@ mkdir -p "$TARGET_DIR" for i in $(seq 1 "$COUNT"); do plugin="Plugin$i" - # Answers: - # 1) $plugin - # 2) N - # 3) N - # 4) $IFACE_ABS - # 5) - # 6) N - # 7) - printf '%s\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py" + # Answers (one per prompt): + # 1) plugin name + # 2) output directory (empty => current directory) + # 3) out of process (N) + # 4) custom config (N) + # 5) interface path + # 6) done adding interfaces (empty) + # 7) relies on subsystems (N) + # 8) include location for the interface (empty => default) + printf '%s\n\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py" done # Create CMakeLists.txt with one add_subdirectory line per plugin diff --git a/PluginSkeletonGenerator/core/GeneratorCoordinator.py b/PluginSkeletonGenerator/core/GeneratorCoordinator.py index e52c8e72..a40c52e6 100644 --- a/PluginSkeletonGenerator/core/GeneratorCoordinator.py +++ b/PluginSkeletonGenerator/core/GeneratorCoordinator.py @@ -12,7 +12,7 @@ class GenerationTask: class GeneratorCoordinator: def __init__(self, name, out_of_process, configuration, parsed_data, header_lookup, locations, - preconditions=None, terminations=None, controls=None): + preconditions=None, terminations=None, controls=None, output_dir=None): self.m_blueprint = PluginBlueprint( name=name, out_of_process=out_of_process, @@ -23,6 +23,7 @@ def __init__(self, name, out_of_process, configuration, parsed_data, header_look preconditions=preconditions, terminations=terminations, controls=controls, + output_dir=output_dir, ) def generateAll(self): diff --git a/PluginSkeletonGenerator/core/PluginBlueprint.py b/PluginSkeletonGenerator/core/PluginBlueprint.py index 0d24c16b..33312f4a 100644 --- a/PluginSkeletonGenerator/core/PluginBlueprint.py +++ b/PluginSkeletonGenerator/core/PluginBlueprint.py @@ -10,6 +10,7 @@ def __init__(self, parsed_data: Dict[str, Tuple]): self._jsonrpc_interfaces: List[str] = [] self._notification_interfaces: List[str] = [] self._notification_entries: List[Tuple[str, object]] = [] + self._event_notification_entries: List[Tuple[str, object]] = [] self.processInterfaces(parsed_data) @@ -53,6 +54,9 @@ def gatherNotificationEntries(self, full_name: str, cls_data): if "Notification" in current.m_name: fq = f"{exchange_ns}::{iface}::{current.m_name}" self._notification_entries.append((fq, current)) + # Only mark the notification struct itself is tagged @event? + if "event" in getattr(current, "m_tags", []): + self._event_notification_entries.append((fq, current)) for child in current.m_children.values(): stack.append((child, iface)) @@ -79,6 +83,10 @@ def file_interface_map(self) -> Dict[str, str]: def notification_entries(self) -> List[Tuple[str, object]]: return self._notification_entries + @property + def event_notification_entries(self) -> List[Tuple[str, object]]: + return self._event_notification_entries + def __init__(self, name, out_of_process, @@ -88,13 +96,15 @@ def __init__(self, locations, preconditions=None, terminations=None, - controls=None): + controls=None, + output_dir=None): self._name = name self._out_of_process = out_of_process self._configuration = configuration self._parsed_data = parsed_data self._header_lookup = header_lookup self._locations = locations + self._output_dir = output_dir preconditions = preconditions or [] terminations = terminations or [] @@ -111,6 +121,7 @@ def __init__(self, self._notification_interfaces = parsed_info.notification_interfaces self._file_interface_map = parsed_info.file_interface_map self._notification_entries = parsed_info.collectNotificationEntries() + self._event_notification_entries = parsed_info.event_notification_entries @property def name(self) -> str: @@ -156,6 +167,10 @@ def file_interface_map(self) -> Dict[str, str]: def notification_entries(self) -> List[Tuple[str, object]]: return self._notification_entries + @property + def event_notification_entries(self) -> List[Tuple[str, object]]: + return self._event_notification_entries + @property def preconditions(self) -> List[str]: return self._PRECONDITIONS @@ -168,5 +183,9 @@ def terminations(self) -> List[str]: def controls(self) -> List[str]: return self._CONTROLS + @property + def output_dir(self) -> str: + return self._output_dir + def isJsonRpcPlugin(self) -> bool: return any("json" in cls_data.m_tags for cls_data, _ in self._parsed_data.values()) \ No newline at end of file diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 15787886..4e0e3901 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -29,6 +29,19 @@ def __init__(self, blueprint: PluginBlueprint) -> None: self.m_preconditions = blueprint._PRECONDITIONS self.m_terminations = blueprint._TERMINATIONS self.m_controls = blueprint._CONTROLS + self.m_event_notification_entries = getattr(blueprint, "event_notification_entries", []) + + @staticmethod + def _dedupe_preserve_order(items: List[str]) -> List[str]: + """Return a new list with duplicates removed while preserving the original order. + """ + seen = set() + out: List[str] = [] + for item in items: + if item not in seen: + seen.add(item) + out.append(item) + return out def _extractStrippedNamespace(self, full_name: str) -> str: parts = full_name.split("::") @@ -37,7 +50,7 @@ def _extractStrippedNamespace(self, full_name: str) -> str: return "::".join(parts[:-1]) # for interfaces that might not have Thunder::, I don't know if they exist def resolveFullName(self, short_name: str): - # Basically a look up, needed for getting full name for namespaces... + """Resolve a short interface name to its parsed fully-qualified name.""" for full_name in self.m_parsed: if full_name.endswith(f"::{short_name}"): return full_name @@ -45,28 +58,181 @@ def resolveFullName(self, short_name: str): @staticmethod def commentParamnames(params: str): - if not params.strip(): + """Avoid unused parameter warnings.""" + if not params or not params.strip(): return "", [] + parts = [s.strip() for s in params.split(",")] - out_parts, names = [], [] + out_parts: List[str] = [] + names: List[str] = [] for chunk in parts: - # some params have tags like /* @out */... - no_comments = re.sub(r'/\*.*?\*/', '', chunk) - token = no_comments.strip().split() - if not token: + no_comments = re.sub(r"/\*.*?\*/", "", chunk) + no_default = no_comments.split("=")[0].strip() + # Examples of named params: + # `const string& msg` + # `bool enabled` + # `Exchange::IFoo* foo` + # Examples of anonymous params: + # `const string&` + # `uint32_t` + tokens = no_default.split() + if len(tokens) < 2: + out_parts.append(chunk) + continue + + candidate = tokens[-1] + candidate_stripped = candidate.lstrip("*&").rstrip("&*") + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", candidate_stripped): out_parts.append(chunk) continue - name = token[-1] + name = candidate_stripped names.append(name) + last = chunk.rfind(name) if last != -1: - chunk = chunk[:last] + f"/* {name} */" + chunk[last+len(name):] + chunk = chunk[:last] + f"/* {name} */" + chunk[last + len(name):] out_parts.append(chunk) + return ", ".join(out_parts), names + def _collect_known_types_for_notification(self, notification_fq: str, notif_cls_data) -> Tuple[str, set, set]: + """ + Notification methods often reference nested enums/structs from: + 1) the notification scope itself (e.g. INotification::Source), and + 2) the owning interface scope (e.g. IAudioStream::streamstate). + + return both sets so downstream logic can choose the correct qualification target. + """ + fq_parts = notification_fq.split("::") + owner_iface_fq = "::".join(fq_parts[:-1]) + + notif_types = set(getattr(notif_cls_data, "m_usings", {}).keys()) + notif_types |= set(getattr(notif_cls_data, "m_children", {}).keys()) + notif_types |= set(getattr(notif_cls_data, "m_types", set())) + + owner_types: set = set() + owner_iface = fq_parts[-2] if len(fq_parts) >= 2 else None + if owner_iface: + owner_full = self.resolveFullName(owner_iface) + if owner_full and owner_full in self.m_parsed: + owner_cls, _ = self.m_parsed[owner_full] + owner_types |= set(getattr(owner_cls, "m_usings", {}).keys()) + owner_types |= set(getattr(owner_cls, "m_children", {}).keys()) + owner_types |= set(getattr(owner_cls, "m_types", set())) + + return owner_iface_fq, notif_types, owner_types + + def _qualify_known_type_token( + self, + token: str, + owner_iface_fq: str, + notification_fq: str, + notif_types: set, + owner_types: set, + *, + prefer_unqualified_owner: bool, + ) -> str: + """ + - Types declared in the notification scope are not visible from the generated sink class + unless fully qualified, so we always qualify those. + - Types declared in the owning interface may be visible via inheritance (implementation + classes) and can remain unqualified for readability unless the current generation + context requires explicit qualification. + """ + if token in notif_types: + return f"{notification_fq}::{token}" + if token in owner_types and not prefer_unqualified_owner: + return f"{owner_iface_fq}::{token}" + return token + + def _qualify_template_args(self, text: str, owner_iface_fq: str, notification_fq: str, notif_types: set, owner_types: set) -> str: + """ + This focuses on the common Thunder patterns like: + Core::OptionalType + where the nested type appears as a template argument and unqualified lookup might fail + depending on the surrounding scope. + """ + def repl_angle(m): + inner = m.group(1) + items = [x.strip() for x in inner.split(",")] + qualified_items: List[str] = [] + for x in items: + if "::" in x: + qualified_items.append(x) + continue + base = x.strip().strip("*&") + qualified = self._qualify_known_type_token( + base, owner_iface_fq, notification_fq, notif_types, owner_types, + prefer_unqualified_owner=False, # inside templates, qualify owner types for safety + ) + if qualified != base: + qualified_items.append(x.replace(base, qualified, 1)) + else: + qualified_items.append(x) + return "<" + ", ".join(qualified_items) + ">" + + return re.sub(r"<\s*([^>]+?)\s*>", repl_angle, text) + + def _qualify_first_type_token_in_param( + self, + param: str, + owner_iface_fq: str, + notification_fq: str, + notif_types: set, + owner_types: set, + *, + prefer_unqualified_owner: bool, + ) -> str: + """ + Parameter strings are preserved as much as possible; we only rewrite the leading + type identifier when it is a known nested type + """ + if "::" in param: + return param + + analysis = re.sub(r"/\*.*?\*/", "", param) + analysis = analysis.split("=")[0].strip() + tokens = analysis.split() + qualifiers = {"const", "volatile", "signed", "unsigned", "struct", "class", "enum"} + + type_token = next((t for t in tokens if t not in qualifiers), None) + if not type_token: + return param + + stripped = type_token.strip("*&") + qualified = self._qualify_known_type_token( + stripped, owner_iface_fq, notification_fq, notif_types, owner_types, + prefer_unqualified_owner=prefer_unqualified_owner, + ) + if qualified == stripped: + return param + + return param.replace(type_token, qualified, 1) + + def _qualifyParamTypes(self, params: str, notification_fq: str, cls_data, *, prefer_unqualified_owner: bool = True) -> str: + if not params or not params.strip() or not cls_data: + return params + + owner_iface_fq, notif_types, owner_types = self._collect_known_types_for_notification(notification_fq, cls_data) + if not notif_types and not owner_types: + return params + + parts = [p.strip() for p in params.split(",") if p.strip()] + out_parts: List[str] = [] + + for p in parts: + p2 = self._qualify_template_args(p, owner_iface_fq, notification_fq, notif_types, owner_types) + p3 = self._qualify_first_type_token_in_param( + p2, owner_iface_fq, notification_fq, notif_types, owner_types, + prefer_unqualified_owner=prefer_unqualified_owner, + ) + out_parts.append(p3) + + return ", ".join(out_parts) + def dynamic_keys(self) -> Dict[str, str]: return { "{{PLUGIN_NAME}}": self.m_plugin_name, @@ -84,6 +250,64 @@ def prepare(self) -> None: self.m_keywords = {**self.dynamic_keys(), **self.static_keys()} self.m_keywords.update(self.extra_keys()) + def _iface_namespace(self, iface_short: str) -> str: + """Returns the stripped namespace for an interface (without the trailing type name)""" + full_name = self.resolveFullName(iface_short) + return self._extractStrippedNamespace(full_name) if full_name else "" + + def notification_registers(self, interface: str) -> str: + name = interface[1:].lower() if interface.startswith('I') else interface.lower() + return generateSimpleText([ + "ASSERT(notification != nullptr);", + "", + "_adminLock.Lock();", + f"auto item = std::find(_{name}Notification.begin(), _{name}Notification.end(), notification);", + f"ASSERT(item == _{name}Notification.end());", + "", + f"if (item == _{name}Notification.end()) {{", + " notification->AddRef();", + f" _{name}Notification.push_back(notification);", + "}", + "", + "_adminLock.Unlock();", + ], remove_empty=False, sep="\n") + + def notification_unregisters(self, interface: str) -> str: + name = interface[1:].lower() if interface.startswith('I') else interface.lower() + return generateSimpleText([ + "ASSERT(notification != nullptr);", + "", + "_adminLock.Lock();", + f"auto item = std::find(_{name}Notification.begin(), _{name}Notification.end(), notification);", + f"ASSERT(item != _{name}Notification.end());", + "", + f"if (item != _{name}Notification.end()) {{", + f" _{name}Notification.erase(item);", + " notification->Release();", + "}", + "_adminLock.Unlock();", + ], remove_empty=False, sep="\n") + + @staticmethod + def _defaultReturnStatement(return_type: str) -> List[str]: + rt = (return_type or "").strip() + if (rt == "void"): + return [] + + if rt == "string" or rt.endswith("::string"): + return [" return string();"] + + if rt == "bool": + return [" return false;"] + + if "*" in rt: + return [" return nullptr;"] + + if rt in ("Core::hresult", "uint32_t", "int32_t", "uint8_t", "uint16_t", "uint64_t", "int64_t", "long", "unsigned", "unsigned int"): + return [" return Core::ERROR_NONE;"] + + return [f" return {rt}();"] + class HeaderData(FileData): class HeaderType(Enum): HEADER = 1 @@ -105,8 +329,10 @@ def static_keys(self): "{{NOTIFICATION_ENTRY}}": self._generateNotifEntry(), "{{NOTIFICATION_FUNCTION}}": self._generateNotifFunction(), "{{HEADER_USING_CONTAINER}}": self._generateHeaderUsingContainer(), + "{{HEADER_USING_EXTERNAL}}": self._generateHeaderUsingExternal(), "{{ADD_PRIVATE_FIELD}}": self._generatePrivateField(), "{{ADD_PUBLIC_FIELD}}": self._generatePublicField(), + "{{ADD_PRIVATE_MEMBERS_FIELD}}": self._generatePrivateMembersField(), "{{HEADER_DEFINITIONS}}": self._generateHeaderDefinitions(), "{{HEADER_STATIC_TIMEOUT}}": self._generateHeaderTimeout(), "{{HEADER_INCLUDES}}": self._generateHeaderIncludes(), @@ -115,7 +341,8 @@ def static_keys(self): "{{HEADER_MEMBERS}}": self._generateHeaderMembers(), "{{HEADER_NOTIFY}}": self._generateHeaderNotify(), "{{HEADER_DEACTIVATE_METHOD}}": self._generateHeaderDeactivateMethod(), - "{{HEADER_DANGLING_METHOD}}": self._generateHeaderDanglingMethod() + "{{HEADER_DANGLING_METHOD}}": self._generateHeaderDanglingMethod(), + "{{CONFIGURE_OOP}}": self._generateConfigureOOPMethod(), } def extra_keys(self): @@ -154,39 +381,66 @@ def _generateNotifEntry(self): return generateSimpleText(lines) def _generateNotifFunction(self): - entries = self.m_blueprint.notification_entries + all_entries = list(getattr(self.m_blueprint, "notification_entries", [])) + event_entries = {fq: cls for fq, cls in (self.m_event_notification_entries if self.m_jsonrpc else [])} + result = [] - if entries: - result.append("void Dangling(const Core::IUnknown* remote, const uint32_t interfaceId) override {" - "\n_parent.Dangling(remote, interfaceId);" - "\n}") - - for fq_name, cls_data in entries: - namespace = fq_name.split("::")[1] - jprefix = f"J{namespace[1:]}" if namespace.startswith("I") else f"J{namespace}" + if all_entries: + result.append( + "void Dangling(const Core::IUnknown* remote, const uint32_t interfaceId) override {\n" + " _parent.Dangling(remote, interfaceId);\n" + "}" + ) + + for fq_name, cls_data in all_entries: + root_iface = fq_name.split("::")[-2] + jprefix = f"J{root_iface[1:]}" if root_iface.startswith("I") else f"J{root_iface}" for m in cls_data.m_methods: - _, param_names = self.commentParamnames(m.m_params) + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=False) + _, param_names = self.commentParamnames(qualified_params) param_str = ", ".join(param_names) add_const = " const" if getattr(m, "m_is_const", False) else "" - result.extend([ - f"void {m.m_name}({m.m_params}) override {{", - f"Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};" if self.m_jsonrpc else '', - "}" - ]) - result.append("") # for spacing between functions + + if fq_name in event_entries: + result.extend([ + f"void {m.m_name}({qualified_params}) override {{", + f" Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};", + "}", + "", + ]) + else: + result.extend([ + f"void {m.m_name}({qualified_params}) override {{", + " // Intentionally left blank (non-@event notification).", + "}", + "", + ]) return generateSimpleText(result, remove_empty=True, sep="\n") def _generateHeaderDefinitions(self): return '#include ' if self.m_jsonrpc else '' + + def _generateConfigureOOPMethod(self): + if not self.m_plugin_config: + return '' + template = FileUtils.readFile(GlobalVariables.CONFIGURE_METHOD) + return template if template else '' def _generateHeaderNotify(self) -> str: - if not self.m_notification_interfaces: return '' + if self.m_type == self.HeaderType.HEADER and not self.m_out_of_process: + result = [] + for fq_name, cls_data in self.m_blueprint.notification_entries: + for m in cls_data.m_methods: + paramsNoNames, _ = self.commentParamnames(m.m_params) + result.append(f"void Notify{m.m_name}({paramsNoNames}) const;") + return generateSimpleText(result) + if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION: result = [] for fq_name, cls_data in self.m_blueprint.notification_entries: @@ -195,11 +449,12 @@ def _generateHeaderNotify(self) -> str: container = f"_{no_i.lower()}Notification" for m in cls_data.m_methods: - _, param_names = self.commentParamnames(m.m_params) + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=True) + _, param_names = self.commentParamnames(qualified_params) param_str = ", ".join(param_names) result.extend([ - f"void Notify{m.m_name}({m.m_params}) const {{", + f"void Notify{m.m_name}({qualified_params}) const {{", " _adminLock.Lock();", f" for (auto* notification : {container}) {{", f" notification->{m.m_name}({param_str if param_str else ''});", @@ -224,11 +479,9 @@ def _generateHeaderInterfaceAggregate(self) -> str: lines = [] for iface in self.m_comrpc_interfaces: - full_name = self.resolveFullName(iface) - if full_name: - namespace = self._extractStrippedNamespace(full_name) - full_iface = f"{namespace}::{iface}" if namespace else iface - lines.append(f"INTERFACE_AGGREGATE({full_iface}, _impl{convertToBaseName(iface)})") + ns = self._iface_namespace(iface) + full_iface = f"{ns}::{iface}" if ns else iface + lines.append(f"INTERFACE_AGGREGATE({full_iface}, _impl{convertToBaseName(iface)})") return generateSimpleText(lines) @@ -240,10 +493,18 @@ def _generateHeaderJSONRPCEvent(self): def _generatePrivateField(self): if not self.m_out_of_process and not self.m_plugin_config: - return "private:" + return "" + return "private:" def _generatePublicField(self): + if not self.m_out_of_process and not self.m_plugin_config: + return "" return "public:" + + def _generatePrivateMembersField(self): + if not self.m_out_of_process and not self.m_plugin_config: + return "" + return "private:" def _generateHeaderMembers(self) -> str: lines = [] @@ -254,9 +515,8 @@ def _generateHeaderMembers(self) -> str: lines.append("uint32_t _connectionId;") for iface in self.m_comrpc_interfaces: - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - lines.append(f"{namespace}::{iface}* _impl{convertToBaseName(iface)};") + ns = self._iface_namespace(iface) + lines.append(f"{ns}::{iface}* _impl{convertToBaseName(iface)};") lines.append("Core::SinkType _notification;") @@ -282,15 +542,16 @@ def _generateHeaderInterfaceEntry(self) -> str: if self.m_jsonrpc: entries.append("INTERFACE_ENTRY(PluginHost::IDispatcher)") + if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION and self.m_plugin_config: + entries.append(f"INTERFACE_ENTRY(Exchange::IConfiguration)") + if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION or (not self.m_out_of_process and self.m_type == self.HeaderType.HEADER): for iface in self.m_comrpc_interfaces: - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - entries.append(f"INTERFACE_ENTRY({namespace}::{iface})") + ns = self._iface_namespace(iface) + entries.append(f"INTERFACE_ENTRY({ns}::{iface})") return generateSimpleText(entries) - def _generateHeaderConfigClass(self): if not self.m_plugin_config: return '' @@ -306,50 +567,53 @@ def _generateHeaderConfigClass(self): return "\n".join(parts).strip() def _generateHeaderIncludes(self): - includes = [] - includes.append('#include "Module.h"') + includes: List[str] = ['#include "Module.h"'] - # Includes for all IDL Header files for iface in self.m_comrpc_interfaces: for filename, mapped_iface in self.m_file_interface_map.items(): if mapped_iface == iface: location = self.m_locations.get(iface, "interfaces") includes.append(f'#include <{location}/{filename}>') - # Includes JSON variant of IDL Header files if applicable - if self.m_type == self.HeaderType.HEADER and self.m_out_of_process: - for json_iface in self.m_jsonrpc_interfaces: - comrpc_iface = convertToCOMRPC(json_iface) - - if comrpc_iface in self.m_comrpc_interfaces: - # Find corresponding filename - filename = next( - (fname for fname, iface in self.m_file_interface_map.items() - if iface == comrpc_iface), - None - ) - if filename: - location = self.m_locations.get(comrpc_iface, "interfaces") - includes.append(f'#include <{location}/json/J{filename[1:]}>') + # JSON includes in the *header* are only needed for JSONRPC event dispatch methods + # that are emitted into Notification (OOP). So only include J-headers for @event notifications. + if self.m_type == self.HeaderType.HEADER and self.m_out_of_process and self.m_jsonrpc: + needed_roots: List[str] = [] + seen = set() + for fq_name, _ in getattr(self.m_blueprint, "event_notification_entries", []): + root_iface = fq_name.split("::")[-2] + if root_iface not in seen: + seen.add(root_iface) + needed_roots.append(root_iface) + + for iface in needed_roots: + json_header = f"J{iface[1:]}" if iface.startswith('I') else f"J{iface}" + location = self.m_locations.get(iface, "interfaces") + includes.append(f'#include <{location}/json/{json_header}.h>') - return generateSimpleText(includes) + if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION and self.m_plugin_config: + includes.append(f"#include ") + + return generateSimpleText(self._dedupe_preserve_order(includes)) def _generateHeaderInheritedClasses(self) -> str: if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION: parts = [] + + if self.m_plugin_config: + parts.append(f"public Exchange::IConfiguration") + for iface in self.m_comrpc_interfaces: - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - parts.append(f"public {namespace}::{iface}") + ns = self._iface_namespace(iface) + parts.append(f"public {ns}::{iface}") else: parts = ["public PluginHost::IPlugin"] if self.m_jsonrpc: parts.append("public PluginHost::JSONRPC") if not self.m_out_of_process: for iface in self.m_comrpc_interfaces: - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - parts.append(f"public {namespace}::{iface}") + ns = self._iface_namespace(iface) + parts.append(f"public {ns}::{iface}") return ": " + generateSimpleText(parts, sep=", ", remove_empty=True) @@ -357,10 +621,9 @@ def _generateHeaderConstructor(self) -> str: members = [] if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION: - for i, iface in enumerate(self.m_comrpc_interfaces): - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - members.append(f"{namespace}::{iface}()") + for iface in self.m_comrpc_interfaces: + ns = self._iface_namespace(iface) + members.append(f"{ns}::{iface}()") if self.m_notification_interfaces: members.append("_adminLock()") members.extend(f"_{convertToBaseName(iface).lower()}Notification()" for iface in self.m_notification_interfaces) @@ -370,10 +633,9 @@ def _generateHeaderConstructor(self) -> str: if self.m_jsonrpc: members.append("PluginHost::JSONRPC()") if not self.m_out_of_process and self.m_comrpc_interfaces: - for i, iface in enumerate(self.m_comrpc_interfaces): - full_name = self.resolveFullName(iface) - namespace = self._extractStrippedNamespace(full_name) - members.append(f"{namespace}::{iface}()") + for iface in self.m_comrpc_interfaces: + ns = self._iface_namespace(iface) + members.append(f"{ns}::{iface}()") if self.m_out_of_process: members.extend(["_service(nullptr)", "_connectionId(0)"]) members.extend(f"_impl{convertToBaseName(iface)}(nullptr)" for iface in self.m_comrpc_interfaces) @@ -399,70 +661,39 @@ def _generateHeaderInheritedMethod(self) -> str: lines.append(f"// {short_name} methods") for m in cls_data.m_methods: - add_const = " const" if getattr(m, "m_is_const", False) else "" if is_header: - paramsNoNames, names = self.commentParamnames(m.m_params) + paramsNoNames, _ = self.commentParamnames(m.m_params) lines.append(f"{m.m_return_type} {m.m_name}({paramsNoNames}){add_const} override;") else: + paramsNoNames, _ = self.commentParamnames(m.m_params) + if short_name in self.m_notification_interfaces and m.m_name in ("Register", "Unregister"): + return_stmt = self._defaultReturnStatement(m.m_return_type) if m.m_name == "Register": lines.append(generateSimpleText([ f"{m.m_return_type} Register({namespace}::{short_name}::INotification* notification) override {{", self.notification_registers(short_name), + *return_stmt, "}" - ])) + ], remove_empty=False)) else: lines.append(generateSimpleText([ f"{m.m_return_type} Unregister(const {namespace}::{short_name}::INotification* notification) override {{", self.notification_unregisters(short_name), + *return_stmt, "}" - ])) + ], remove_empty=False)) else: - paramsNoNames, names = self.commentParamnames(m.m_params) - - lines.append(generateSimpleText([ + body = [ f"{m.m_return_type} {m.m_name}({paramsNoNames}){add_const} override {{", - " return Core::ERROR_NONE;", + *self._defaultReturnStatement(m.m_return_type), "}" - ])) + ] + lines.append(generateSimpleText(body, remove_empty=False)) return generateSimpleText(lines, sep="\n\n") - - def notification_registers(self, interface): - text = [] - text.append('\nASSERT(notification != nullptr);\n') - text.append('\n_adminLock.Lock();') - text.append(f'''\nauto item = std::find(_{interface[1:].lower()}Notification.begin(), _{interface[1:].lower()}Notification.end(), notification); - ASSERT(item == _{interface[1:].lower()}Notification.end()); - - if (item == _{interface[1:].lower()}Notification.end()) {{ - notification->AddRef(); - _{interface[1:].lower()}Notification.push_back(notification); - }} - - _adminLock.Unlock(); ''') - text.append('\n') - return ''.join(text) - - def notification_unregisters(self,interface): - text = [] - text.append(f'''\n - ASSERT(notification != nullptr); - - _adminLock.Lock(); - auto item = std::find(_{interface[1:].lower()}Notification.begin(), _{interface[1:].lower()}Notification.end(), notification); - ASSERT(item != _{interface[1:].lower()}Notification.end()); - - if (item != _{interface[1:].lower()}Notification.end()) {{ - _{interface[1:].lower()}Notification.erase(item); - notification->Release(); - }} - _adminLock.Unlock(); - ''') - return ''.join(text) - def _generateHeaderUsingContainer(self) -> str: cond = ( (self.m_out_of_process and self.m_type == self.HeaderType.HEADER_IMPLEMENTATION) @@ -483,8 +714,74 @@ def _generateHeaderUsingContainer(self) -> str: lines.append(container_typedef) return generateSimpleText(lines) + + def _generateHeaderUsingExternal(self) -> str: + if self.m_type != self.HeaderType.HEADER_IMPLEMENTATION: + return "" + + usings: Dict[str, str] = {} + + for full_name, (cls_data, _) in self.m_parsed.items(): + for name, value in getattr(cls_data, "m_usings", {}).items(): + if name in usings and usings[name] != value: + print( + f"[WARN] Conflicting type alias '{name}': '{usings[name]}' vs '{value}'. " + "Keeping the first definition." + ) + continue + usings.setdefault(name, value) + + if not usings: + return "" + + lines = ["// Type aliases copied from interface headers"] + for name in sorted(usings.keys()): + lines.append(f"using {name} = {usings[name]};") + + return generateSimpleText(lines, remove_empty=False) class SourceData(FileData): + _KNOWN_SUBSYSTEMS = { + "PLATFORM", + "SECURITY", + "NETWORK", + "IDENTIFIER", + "GRAPHICS", + "INTERNET", + "LOCATION", + "TIME", + "PROVISIONING", + "DECRYPTION", + "WEBSOURCE", + "STREAMING", + "BLUETOOTH", + "CRYPTOGRAPHY", + "INSTALLATION", + "NOT_PLATFORM", + "NOT_SECURITY", + "NOT_NETWORK", + "NOT_IDENTIFIER", + "NOT_GRAPHICS", + "NOT_INTERNET", + "NOT_LOCATION", + "NOT_TIME", + "NOT_PROVISIONING", + "NOT_DECRYPTION", + "NOT_WEBSOURCE", + "NOT_STREAMING", + "NOT_BLUETOOTH", + "NOT_CRYPTOGRAPHY", + "NOT_INSTALLATION", + } + + def _validateSubsystemEntries(self, entries: List[str], label: str) -> List[str]: + if not entries: + return [] + invalid = [e for e in entries if e.upper() not in self._KNOWN_SUBSYSTEMS] + for e in invalid: + print(f"[WARN] Unknown subsystem in {label}: '{e}'. It will still be emitted as subsystem::{e.upper()}.") + return [e.upper() for e in entries if e] + def static_keys(self) -> Dict[str, str]: return { "{{PLUGIN_NAME}}": self.m_plugin_name, @@ -502,6 +799,7 @@ def static_keys(self) -> Dict[str, str]: "{{DEINITIALIZE_OOP}}": self._generateDeinitializeOOP(), "{{INCLUDE_STATEMENTS}}": self._generateSourceIncludeStatements(), "{{SOURCE_METHOD_IMPL}}": self._generateSourcePluginMethods(), + "{{SOURCE_NOTIFY_IMPL}}": self._generateSourceNotify(), "{{PRECONDITIONS}}": self._generatePreconditions(), "{{TERMINATIONS}}": self._generateTerminations(), "{{CONTROLS}}": self._generateControls(), @@ -528,39 +826,46 @@ def _generateUnregisterNotification(self): def _generatePreconditions(self): if not self.m_preconditions: return '' - lines = [f" subsystem::{entry} " for entry in self.m_preconditions] + entries = self._validateSubsystemEntries(self.m_preconditions, "Preconditions") + lines = [f" subsystem::{entry} " for entry in entries] return generateSimpleText(lines, sep=",") def _generateTerminations(self): if not self.m_terminations: return '' - lines = [f" subsystem::{entry} " for entry in self.m_terminations] + entries = self._validateSubsystemEntries(self.m_terminations, "Terminations") + lines = [f" subsystem::{entry} " for entry in entries] return generateSimpleText(lines, sep=",") def _generateControls(self): if not self.m_controls: return '' - lines = [f" subsystem::{entry} " for entry in self.m_controls] + entries = self._validateSubsystemEntries(self.m_controls, "Controls") + lines = [f" subsystem::{entry} " for entry in entries] return generateSimpleText(lines, sep=",") def _generateSourceIncludeStatements(self): + """ + The source file is responsible for JSONRPC Register/Unregister calls for @json interfaces, + so it may need J* headers even when the header does not. + """ includes = [f'#include "{self.m_plugin_name}.h"'] if self.m_plugin_config and self.m_out_of_process: includes.append('#include ') - for json_iface in self.m_jsonrpc_interfaces: - base_name = convertToCOMRPC(json_iface) + for iface in self.m_comrpc_interfaces: + full_name = self.resolveFullName(iface) + if not full_name: + continue + cls_data, _ = self.m_parsed.get(full_name, (None, None)) + if not cls_data or "json" not in getattr(cls_data, "m_tags", []): + continue - header_filename = next( - (fname for fname, iface in self.m_file_interface_map.items() - if iface == base_name), - None - ) - if header_filename: - location = self.m_locations.get(base_name, "interfaces") - includes.append(f'#include <{location}/json/J{header_filename[1:]}>') + json_header = f"J{iface[1:]}" if iface.startswith('I') else f"J{iface}" + location = self.m_locations.get(iface, "interfaces") + includes.append(f'#include <{location}/json/{json_header}.h>') - return generateSimpleText(includes) + return generateSimpleText(self._dedupe_preserve_order(includes)) def _generateSourcePluginMethods(self) -> str: @@ -586,23 +891,32 @@ def _generateSourcePluginMethods(self) -> str: f"void {self.m_plugin_name}::Dangling(const Core::IUnknown* remote, const uint32_t interfaceId) {{\n" "ASSERT(remote != nullptr);\n" ) - - impl_var = self.m_comrpc_interfaces[0][1:] - impl_var_cpp = f"_impl{impl_var}" if impl_var else "_implementation" - + + # root interface -> implementation member (e.g. IBrowser -> _implBrowser) + impl_map = {iface: f"_impl{convertToBaseName(iface)}" for iface in self.m_comrpc_interfaces} + for fq_name, _ in entries: + # fq_name like Exchange::IBrowser::INotification + root_iface = fq_name.split("::")[-2] + unregister_impl = impl_map.get(root_iface) + if not unregister_impl: + # Fallback to first interface if we can't resolve (should not happen for selected roots) + unregister_impl = impl_map.get(self.m_comrpc_interfaces[0]) + dangling.append( f" if (interfaceId == {fq_name}::ID) {{\n" f" auto* revokedInterface = remote->QueryInterface<{fq_name}>();\n" f" if (revokedInterface) {{\n" - f" {impl_var_cpp}->Unregister(revokedInterface);\n" + f" {unregister_impl}->Unregister(revokedInterface);\n" f" revokedInterface->Release();\n" f" }}\n" f" }}\n" - f" }}\n" ) - + + dangling.append("}\n") + return "\n".join(method) + "\n".join(dangling) + lines = [] for full_name, (cls_data, _) in self.m_blueprint.parsed_data.items(): @@ -610,15 +924,35 @@ def _generateSourcePluginMethods(self) -> str: iface = parts[-1] base_name = convertToBaseName(iface) notify_var = f"_{base_name.lower()}Notification" + namespace = self._extractStrippedNamespace(full_name) for method in cls_data.m_methods: paramsNoNames, _ = self.commentParamnames(method.m_params) add_const = " const" if getattr(method, "m_is_const", False) else "" - lines.extend([ - f"{method.m_return_type} {self.m_plugin_name}::{method.m_name}({paramsNoNames}){add_const} {{", - f" return Core::ERROR_NONE;", - "}" - ]) + + # Handle Register and Unregister methods for notification interfaces + if iface in self.m_notification_interfaces and method.m_name in ("Register", "Unregister"): + return_stmt = self._defaultReturnStatement(method.m_return_type) + if method.m_name == "Register": + lines.extend([ + f"{method.m_return_type} {self.m_plugin_name}::Register({namespace}::{iface}::INotification* notification) {{", + self.notification_registers(iface).strip(), + *return_stmt, + "}" + ]) + else: + lines.extend([ + f"{method.m_return_type} {self.m_plugin_name}::Unregister(const {namespace}::{iface}::INotification* notification) {{", + self.notification_unregisters(iface).strip(), + *return_stmt, + "}" + ]) + else: + lines.extend([ + f"{method.m_return_type} {self.m_plugin_name}::{method.m_name}({paramsNoNames}){add_const} {{", + *self._defaultReturnStatement(method.m_return_type), + "}" + ]) if "event" in cls_data.m_tags: lines.extend([ @@ -633,6 +967,37 @@ def _generateSourcePluginMethods(self) -> str: return "\n".join(lines) + def _generateSourceNotify(self) -> str: + """Generate notification method implementations for in-process plugins""" + if self.m_out_of_process or not self.m_notification_interfaces: + return '' + + all_methods = [] + + for fq_name, cls_data in self.m_blueprint.notification_entries: + iface = fq_name.split("::")[-2] + no_i = iface[1:] if iface.startswith("I") else iface + container = f"_{no_i.lower()}Notification" + + for m in cls_data.m_methods: + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=True) + _, param_names = self.commentParamnames(qualified_params) + param_str = ", ".join(param_names) + + method_lines = [ + f"void {self.m_plugin_name}::Notify{m.m_name}({qualified_params}) const {{", + " _adminLock.Lock();", + f" for (auto* notification : {container}) {{", + f" notification->{m.m_name}({param_str if param_str else ''});", + " }", + " _adminLock.Unlock();", + "}" + ] + + all_methods.append('\n'.join(method_lines)) + + return '\n\n'.join(all_methods) + def _generateVariableUnusedDeinitial(self): return "" if self.m_out_of_process else "VARIABLE_IS_NOT_USED " @@ -643,16 +1008,13 @@ def _generateVariableUnusedInitial(self): return "VARIABLE_IS_NOT_USED " def _generateFirstCOMRPCInterface(self): + """Return the fully-qualified type name for the first selected COMRPC root interface.""" if not self.m_comrpc_interfaces: return "Exchange::IPlugin" short_name = self.m_comrpc_interfaces[0] - full_name = next((k for k in self.m_parsed if k.endswith(f"::{short_name}")), None) - if full_name: - namespace = self._extractStrippedNamespace(full_name) - return f"{namespace}::{short_name}" if namespace else short_name - else: - return short_name + ns = self._iface_namespace(short_name) + return f"{ns}::{short_name}" if ns else short_name def _generateFirstCOMRPCNoPrefix(self): name = self.m_comrpc_interfaces[0] if self.m_comrpc_interfaces else "" @@ -666,10 +1028,9 @@ def _generateJSONRegisterIP(self) -> str: for short_name in self.m_comrpc_interfaces: json_iface = convertToJSONRPC(short_name) if json_iface in self.m_jsonrpc_interfaces: - full_name = self.resolveFullName(short_name) - if full_name: - namespace = self._extractStrippedNamespace(full_name) - lines.append(f"{namespace}::{json_iface}::Register(*this, this);") + ns = self._iface_namespace(short_name) + if ns: + lines.append(f"{ns}::{json_iface}::Register(*this, this);") return generateSimpleText(lines) def _generateJSONUnregisterIP(self) -> str: @@ -677,10 +1038,9 @@ def _generateJSONUnregisterIP(self) -> str: for short_name in self.m_comrpc_interfaces: json_iface = convertToJSONRPC(short_name) if json_iface in self.m_jsonrpc_interfaces: - full_name = self.resolveFullName(short_name) - if full_name: - namespace = self._extractStrippedNamespace(full_name) - lines.append(f"{namespace}::{json_iface}::Unregister(*this);") + ns = self._iface_namespace(short_name) + if ns: + lines.append(f"{ns}::{json_iface}::Unregister(*this);") return generateSimpleText(lines) def _generateConfigurationIP(self): @@ -701,73 +1061,99 @@ def _generateDeinitializeImplementation(self): template = FileUtils.readFile(path) return (FileUtils.replaceKeywords(template, self.m_keywords)if template else '') - def _generateNestedQuery(self) -> str: + def _iface_has_notification_register(self, iface: str) -> bool: + full_name = self.resolveFullName(iface) + if not full_name: + return False + cls_data, _ = self.m_parsed.get(full_name, (None, None)) + if not cls_data: + return False + + method_names = {m.m_name for m in getattr(cls_data, "m_methods", [])} + if not ({"Register", "Unregister"} <= method_names): + return False + + + return any(fq.split("::")[-2] == iface for fq, _ in getattr(self.m_blueprint, "notification_entries", [])) + def _generateNestedQuery(self) -> str: if not self.m_comrpc_interfaces: return "" interfaces = self.m_comrpc_interfaces - N = len(interfaces) - - # qi = queryinterface - has_qi = N >= 2 - init_lines = [] - - for short_name in interfaces: - json_iface = convertToJSONRPC(short_name) - if json_iface in self.m_jsonrpc_interfaces: - name = convertToBaseName(short_name) - full_name = next((k for k in self.m_parsed if k.endswith(f"::{short_name}")), None) - namespace = self._extractStrippedNamespace(full_name) - qualified_json = f"{namespace}::{json_iface}" - - if short_name in self.m_notification_interfaces: - init_lines.append(f"_impl{name}->Register(&_notification);") - init_lines.append(f"{qualified_json}::Register(*this, _impl{name});") + has_qi = len(interfaces) >= 2 + init_lines = self._nestedquery_init_lines(interfaces) if self.m_plugin_config: - rootName = convertToBaseName(interfaces[0]) - if init_lines and init_lines[-1] != "": - init_lines.append("") - init_lines.extend([ - f"Exchange::IConfiguration* configuration = _impl{rootName}->QueryInterface();", - "ASSERT(configuration != nullptr);", - "if (configuration != nullptr) {", - " if (configuration->Configure(service) != Core::ERROR_NONE) {", - f' message = _T("{self.m_plugin_name} could not be configured.");', - " }", - " configuration->Release();", - "}" - ]) - - has_init = bool(init_lines) - has_work = has_qi or has_init + init_lines = self._nestedquery_add_configure(init_lines, interfaces[0]) - # for the if statment: close or "else" - if not has_work: + if not (has_qi or init_lines): return generateSimpleText(["}"]) - lines = ["} else {"] + + lines: List[str] = ["} else {"] if has_qi: - for idx, short_name in enumerate(interfaces[1:], start=1): - baseName = convertToBaseName(short_name) - full_name = next((k for k in self.m_parsed if k.endswith(f"::{short_name}")), None) - namespace = self._extractStrippedNamespace(full_name) - lines.extend([ - f"_impl{baseName} = _impl{convertToBaseName(interfaces[0])}->QueryInterface<{namespace}::{short_name}>();", - f"if (_impl{baseName} == nullptr) {{", - f'message = _T("Failed to acquire {short_name} interface.");', - "} else {" - ]) + lines.extend(self._nestedquery_qi_lines(interfaces)) - if has_init: + if init_lines: lines.extend(init_lines) - close_count = 1 + (N - 1 if has_qi else 0) - lines.extend("}" for _ in range(close_count)) - + lines.extend(self._nestedquery_closing_braces(len(interfaces), has_qi)) return generateSimpleText(lines) + def _nestedquery_init_lines(self, interfaces: List[str]) -> List[str]: + init: List[str] = [] + for iface in interfaces: + base = convertToBaseName(iface) + + if self._iface_has_notification_register(iface): + init.append(f"_impl{base}->Register(&_notification);") + + json_iface = convertToJSONRPC(iface) + if json_iface in self.m_jsonrpc_interfaces: + ns = self._iface_namespace(iface) + init.append(f"{ns}::{json_iface}::Register(*this, _impl{base});") + + return init + + def _nestedquery_add_configure(self, init_lines: List[str], root_iface: str) -> List[str]: + root_base = convertToBaseName(root_iface) + out = list(init_lines) + if out and out[-1] != "": + out.append("") + out.extend([ + f"Exchange::IConfiguration* configuration = _impl{root_base}->QueryInterface();", + "ASSERT(configuration != nullptr);", + "if (configuration != nullptr) {", + " if (configuration->Configure(service) != Core::ERROR_NONE) {", + f' message = _T("{self.m_plugin_name} could not be configured.");', + " }", + " configuration->Release();", + "}", + ]) + return out + + def _nestedquery_qi_lines(self, interfaces: List[str]) -> List[str]: + root_base = convertToBaseName(interfaces[0]) + lines: List[str] = [] + + for iface in interfaces[1:]: + base = convertToBaseName(iface) + ns = self._iface_namespace(iface) + lines.extend([ + f"_impl{base} = _impl{root_base}->QueryInterface<{ns}::{iface}>();", + f"if (_impl{base} == nullptr) {{", + f'message = _T("Failed to acquire {iface} interface.");', + "} else {", + ]) + + return lines + + @staticmethod + def _nestedquery_closing_braces(interface_count: int, has_qi: bool) -> List[str]: + close_count = 1 + (interface_count - 1 if has_qi else 0) + return ["}" for _ in range(close_count)] + def _generateDeinitializeOOP(self): if not self.m_comrpc_interfaces: return "" @@ -777,55 +1163,55 @@ def _generateDeinitializeOOP(self): firstBase = convertToBaseName(first) firstJson = convertToJSONRPC(first) - full_name_first = next((k for k in self.m_parsed if k.endswith(f"::{first}")), None) - namespace_first = self._extractStrippedNamespace(full_name_first) + namespace_first = self._iface_namespace(first) lines.append(f"if (_impl{firstBase} != nullptr) {{") - if firstJson in self.m_jsonrpc_interfaces: - lines.append(f"{namespace_first}::{firstJson}::Unregister(*this);") - - if first in self.m_notification_interfaces: - lines.append(f"_impl{firstBase}->Unregister(&_notification);") - + # Unregister JSONRPC bindings + COMRPC notifications for secondary interfaces first for comrpc in self.m_comrpc_interfaces[1:]: baseName = convertToBaseName(comrpc) jsonName = convertToJSONRPC(comrpc) - full_name = next((k for k in self.m_parsed if k.endswith(f"::{comrpc}")), None) - namespace = self._extractStrippedNamespace(full_name) + namespace = self._iface_namespace(comrpc) lines.append("\n") lines.append(f"if (_impl{baseName} != nullptr) {{") if jsonName in self.m_jsonrpc_interfaces: lines.append(f"{namespace}::{jsonName}::Unregister(*this);") - - if comrpc in self.m_notification_interfaces: - lines.append(f"_impl{baseName}->Unregister(&_notification);") + + if self._iface_has_notification_register(comrpc): + lines.append(f"_impl{baseName}->Unregister(&_notification);") lines.append(f"_impl{baseName}->Release();") lines.append(f"_impl{baseName} = nullptr;") lines.append("}") + # Now unregister for the root interface last + if firstJson in self.m_jsonrpc_interfaces: + lines.append(f"{namespace_first}::{firstJson}::Unregister(*this);") + + if self._iface_has_notification_register(first): + lines.append(f"_impl{firstBase}->Unregister(&_notification);") + lines.extend([ "\n", - f"\n", + "\n", f"RPC::IRemoteConnection* connection(service->RemoteConnection(_connectionId));", f"VARIABLE_IS_NOT_USED uint32_t result = _impl{firstBase}->Release();", f"_impl{firstBase} = nullptr;", - ]) + ]) lines.append("\n") lines.append(f"ASSERT((result == Core::ERROR_DESTRUCTION_SUCCEEDED) || (result == Core::ERROR_CONNECTION_CLOSED));") lines.append("\n") lines.extend([ - f"// The process can disappear in the meantime...", - f"if (connection != nullptr) {{", - f"// But if it did not disappear in the meantime, forcefully terminate it. Shoot to kill", - f"connection->Terminate();", - f"connection->Release();", - f"}}", - f"}}" + "// The process can disappear in the meantime...", + "if (connection != nullptr) {", + "// But if it did not disappear in the meantime, forcefully terminate it. Shoot to kill", + "connection->Terminate();", + "connection->Release();", + "}", + "}", ]) return generateSimpleText(lines, remove_empty=False) diff --git a/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py b/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py index b98cdb93..8fc975e8 100644 --- a/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py +++ b/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py @@ -9,7 +9,8 @@ class PluginRepositoryGenerator: def __init__(self, blueprint) -> None: self.m_plugin_name = blueprint.name - self.m_directory = blueprint.name + root = getattr(blueprint, "output_dir", None) or os.getcwd() + self.m_directory = os.path.join(root, blueprint.name) os.makedirs(self.m_directory, exist_ok=False) self.m_indenter = Indenter() diff --git a/PluginSkeletonGenerator/menu/Menu.py b/PluginSkeletonGenerator/menu/Menu.py index 371cb9c5..10e27566 100644 --- a/PluginSkeletonGenerator/menu/Menu.py +++ b/PluginSkeletonGenerator/menu/Menu.py @@ -2,12 +2,13 @@ import sys import yaml import argparse +import re from typing import Dict, Tuple, List from parser.Parser import Printer, ClassData from parser.MultiParser import MultiParser from core.GeneratorCoordinator import GeneratorCoordinator - - + + def loadYAML(file_path: str) -> Dict: if not os.path.isfile(file_path): print(f"[ERROR]: Config file '{file_path}' does not exist") @@ -18,6 +19,7 @@ def loadYAML(file_path: str) -> Dict: except yaml.YAMLError as e: print(f"[ERROR]: Failed to parse: {e}") + def validatePaths(paths: List[str]) -> None: files = [p for p in paths if not os.path.isfile(p)] if files: @@ -26,23 +28,59 @@ def validatePaths(paths: List[str]) -> None: print(f"-{missing}") sys.exit(1) + def collectList(label: str) -> List[str]: - print(f"\nEnter {label} (press Enter on empty line to finish):") - entries = [] - while True: - entry = input(f"{label[:-1]}: ").strip() - if not entry: - break - entries.append(entry) - return entries - -def prompts() -> Tuple[str, bool, bool, List[str], Dict[str, str], List[str], List[str], List[str]]: + print(f"\nEnter {label} (press Enter on empty line to finish):") + entries = [] + while True: + entry = input(f"{label[:-1]}: ").strip() + if not entry: + break + entries.append(entry) + return entries + + +def _prompt_yes_no(question: str, default: bool = False) -> bool: + suffix = "[Y/N, Enter defaults to N]" if default is False else "[Y/N, Enter defaults to Y]" + while True: + raw = input(f"{question} (Y/N) {suffix}: ").strip().lower() + if raw == "": + return default + if raw in ("y", "yes"): + return True + if raw in ("n", "no"): + return False + print("[WARN] Invalid input. Please enter Y or N.") + + +def _validate_plugin_name(name: str) -> str: + name = name.strip() + if not name: + print("[ERROR]: Plugin name can not be empty") + sys.exit(1) + # C++ identifier + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): + print("[ERROR]: Invalid plugin name. Use only letters, digits and '_' and do not start with a digit.") + sys.exit(1) + return name + + +def prompts() -> Tuple[str, bool, bool, List[str], Dict[str, str], List[str], List[str], List[str], str]: print("=== Plugin Generator ===") - - plugin_name = input("What will your plugin be called: ").strip() - out_of_process = input("Is your plugin out of process? (Y/N): ").strip().lower() == "y" - plugin_config = input("Custom plugin specific configuration needed? (Y/N): ").strip().lower() == "y" - + + plugin_name = _validate_plugin_name(input("What will your plugin be called: ")) + + output_dir = input("Where should the plugin be generated? [default: current directory]: ").strip() + if not output_dir: + output_dir = os.getcwd() + output_dir = os.path.abspath(output_dir) + if not os.path.isdir(output_dir): + print(f"[ERROR]: Output directory does not exist: {output_dir}") + sys.exit(1) + + out_of_process = _prompt_yes_no("Is your plugin out of process?", default=False) + plugin_config = _prompt_yes_no("Custom plugin specific configuration needed?", default=False) + header_files = [] print("\nAdd FULL path to your interface (C++ IDL header file)...") print("Example: /home/Thunder/ThunderInterfaces/interfaces/ITestInterface.h") @@ -51,30 +89,32 @@ def prompts() -> Tuple[str, bool, bool, List[str], Dict[str, str], List[str], Li if not path: break header_files.append(path) - + if not header_files and out_of_process: print("Error: At least one header path must be provided") sys.exit(1) validatePaths(header_files) - - subsystems_enabled = input("\nDoes your plugin rely on Thunder subsystems [Preconditions, Terminations, Controls]? (Y/N): ").strip().lower() == "y" - - + + subsystems_enabled = _prompt_yes_no( + "Does your plugin rely on Thunder subsystems [Preconditions, Terminations, Controls]?", + default=False, + ) + preconditions = collectList("Preconditions") if subsystems_enabled else [] terminations = collectList("Terminations") if subsystems_enabled else [] controls = collectList("Controls") if subsystems_enabled else [] - - return plugin_name, out_of_process, plugin_config, header_files, {}, preconditions, terminations, controls - - + + return plugin_name, out_of_process, plugin_config, header_files, {}, preconditions, terminations, controls, output_dir + + def parserInterfaces(header_files: List[str]) -> Tuple[Dict[str, Tuple[ClassData, str]], Dict[str, str]]: parser = MultiParser(header_files) parsed_data = parser.parseAll() header_lookup = parser.m_header_lookup return parsed_data, header_lookup - - + + def displayParsedData(parsed_data: Dict[str, Tuple[ClassData, str]]) -> None: print("=" * 30) print(f"[INFO] Parsed {len(parsed_data)} interfaces:") @@ -82,59 +122,60 @@ def displayParsedData(parsed_data: Dict[str, Tuple[ClassData, str]]) -> None: print(f"[INFO]: {name} (from {header})") Printer.printTree(class_data) print("=" * 30) - - + + def _root(full_name: str) -> str: return full_name.split("::")[-1] - - + + def menu() -> None: parser = argparse.ArgumentParser(description="Plugin Skeleton Generator") parser.add_argument("--config", help="Path to .yaml file") args = parser.parse_args() - + if args.config: config = loadYAML(args.config) - + plugin_name = config.get("PluginName") out_of_process = config.get("OutOfProcess", False) plugin_config = config.get("PluginConfig", False) header_files = config.get("Paths", []) raw_locations = config.get("Locations", {}) - + preconditions = config.get("Preconditions", []) terminations = config.get("Terminations", []) controls = config.get("Controls", []) - + select_map = config.get("SelectInterfaces", {}) - + if not plugin_name: print("[ERROR]: PluginName must be specified in the config file") sys.exit(1) if not header_files: print("[ERROR]: Paths must be specified in the config file") sys.exit(1) + output_dir = os.getcwd() else: - plugin_name, out_of_process, plugin_config, header_files, raw_locations, preconditions, terminations, controls = prompts() + plugin_name, out_of_process, plugin_config, header_files, raw_locations, preconditions, terminations, controls, output_dir = prompts() select_map = {} - - if os.path.exists(plugin_name): - print(f"[ERROR]: Directory '{plugin_name}' already exists") + + plugin_root = os.path.join(output_dir, plugin_name) + if os.path.exists(plugin_root): + print(f"[ERROR]: Directory '{plugin_root}' already exists") sys.exit(1) - + parsed_data, header_lookup = parserInterfaces(header_files) - - # Grouping root level by header for root interface selection + by_header: Dict[str, List[str]] = {} for full_name, (_, header_path) in parsed_data.items(): by_header.setdefault(header_path, []).append(full_name) - + selected_full_names: set = set() - + if args.config: for header_path, full_names in by_header.items(): header_file = os.path.basename(header_path) - wanted = select_map.get(header_file) # list or none + wanted = select_map.get(header_file) if not wanted: selected_full_names.update(full_names) else: @@ -143,11 +184,11 @@ def menu() -> None: else: for header_path, full_names in by_header.items(): roots = [_root(fn) for fn in full_names] - + if len(full_names) == 1: selected_full_names.add(full_names[0]) continue - + print(f"\n[SELECT] Please pick which interfaces you want to use from {header_path}") for i, r in enumerate(roots, 1): print(f" {i}) {r}") @@ -165,18 +206,18 @@ def menu() -> None: print("[WARN] Empty/invalid selection, keeping ALL for this header.") chosen = full_names selected_full_names.update(chosen) - + parsed_data = {k: v for k, v in parsed_data.items() if k in selected_full_names} chosen_roots = {_root(k) for k in parsed_data.keys()} header_lookup = {k: v for k, v in header_lookup.items() if k in chosen_roots} - + locations: Dict[str, str] = {} for full_name, (_, header_path) in parsed_data.items(): iface_name = _root(full_name) header_file = os.path.basename(header_path) if header_file in raw_locations: locations[iface_name] = raw_locations[header_file] - + displayParsedData(parsed_data) header_lookup_test = [] if not args.config: @@ -194,7 +235,7 @@ def menu() -> None: if loc: for r in roots_in_header: locations[r] = loc - + coordinator = GeneratorCoordinator( plugin_name, out_of_process, @@ -205,5 +246,6 @@ def menu() -> None: preconditions, terminations, controls, + output_dir=output_dir, ) - coordinator.generateAll() + coordinator.generateAll() \ No newline at end of file diff --git a/PluginSkeletonGenerator/parser/Parser.py b/PluginSkeletonGenerator/parser/Parser.py index d8c3635b..9f5dc21f 100755 --- a/PluginSkeletonGenerator/parser/Parser.py +++ b/PluginSkeletonGenerator/parser/Parser.py @@ -3,7 +3,6 @@ import os from typing import Dict - class ScopeType(Enum): CLASS = 1 NAMESPACE = 2 @@ -24,6 +23,8 @@ def __init__(self, name, namespace=""): self.m_tags = [] self.m_methods = [] self.m_children = {} + self.m_usings = {} + self.m_types = set() def addMethod(self, method): self.m_methods.append(method) @@ -31,7 +32,6 @@ def addMethod(self, method): def addChild(self, child): self.m_children[child.m_name] = child - class InterfaceTree: ''' Each parsed file is represented as a tree of ClassData objects. @@ -129,6 +129,8 @@ def parseFile(self) -> Dict[str, ClassData]: self.processTag(line) self.processNamespace(line) self.processClass(line) + self.processUsing(line) + self.processTypeDecl(line) self.processMethod(line) self.processScope(line) return self.m_interface_tree.parsedClasses() @@ -146,11 +148,42 @@ def processNamespace(self, line): self.m_interface_tree.pushNamespace(namespace) def processClass(self, line): - match = re.match(r'struct\s+EXTERNAL\s+(\w+)\s*:\s*virtual\s+public\s+Core::IUnknown', line) + # struct EXTERNAL IFoo : virtual public Core::IUnknown + # struct IFoo : virtual public Core::IUnknown + match = re.match(r'struct\s+(?:EXTERNAL\s+)?(\w+)\s*:\s*virtual\s+public\s+Core::IUnknown', line) if match: interface = match.group(1) self.m_pending_class = interface + def processUsing(self, line): + """Parse a `using Name = ...;` alias inside the current class scope.""" + m = re.match(r'^using\s+(\w+)\s*=\s*(.+);$', line) + if m and self.m_interface_tree.m_class_stack: + name = m.group(1).strip() + value = m.group(2).strip() + current = self.m_interface_tree.m_class_stack[-1] + if name not in current.m_usings: + current.m_usings[name] = value + + def processTypeDecl(self, line: str): + """Capture nested typedef/enum/struct names inside the current class scope.""" + if not self.m_interface_tree.m_class_stack: + return + + current = self.m_interface_tree.m_class_stack[-1] + + m = re.match(r'^typedef\s+.+\s+(\w+)\s*;\s*$', line) + if m: + current.m_types.add(m.group(1)) + else: + m = re.match(r'^enum\s+(?:class\s+)?(\w+)\b', line) + if m: + current.m_types.add(m.group(1)) + elif 'Core::IUnknown' not in line: + m = re.match(r'^struct\s+(\w+)\b', line) + if m: + current.m_types.add(m.group(1)) + def processMethod(self, line): if "virtual" in line and ";" in line: self.m_interface_tree.addMethod(line) diff --git a/PluginSkeletonGenerator/templates/.plugin_header.txt b/PluginSkeletonGenerator/templates/.plugin_header.txt index 3dc86d8e..181be5e8 100644 --- a/PluginSkeletonGenerator/templates/.plugin_header.txt +++ b/PluginSkeletonGenerator/templates/.plugin_header.txt @@ -55,7 +55,7 @@ namespace Plugin { {{HEADER_INTERFACE_AGGREGATE}} END_INTERFACE_MAP - private: +{{ADD_PRIVATE_MEMBERS_FIELD}} {{HEADER_USING_CONTAINER}} {{HEADER_STATIC_TIMEOUT}} diff --git a/PluginSkeletonGenerator/templates/.plugin_implementation.txt b/PluginSkeletonGenerator/templates/.plugin_implementation.txt index 19c9f22a..33980087 100644 --- a/PluginSkeletonGenerator/templates/.plugin_implementation.txt +++ b/PluginSkeletonGenerator/templates/.plugin_implementation.txt @@ -41,8 +41,12 @@ namespace Plugin { BEGIN_INTERFACE_MAP({{PLUGIN_NAME}}Implementation) {{HEADER_INTERFACE_ENTRY}} END_INTERFACE_MAP - + + {{HEADER_USING_EXTERNAL}} + {{HEADER_INHERITED_METHOD}} + + {{CONFIGURE_OOP}} private: {{HEADER_USING_CONTAINER}} diff --git a/PluginSkeletonGenerator/templates/.plugin_source.txt b/PluginSkeletonGenerator/templates/.plugin_source.txt index c997e39c..e6f0f50b 100644 --- a/PluginSkeletonGenerator/templates/.plugin_source.txt +++ b/PluginSkeletonGenerator/templates/.plugin_source.txt @@ -49,5 +49,7 @@ string {{PLUGIN_NAME}}::Information() const { } {{SOURCE_METHOD_IMPL}} + +{{SOURCE_NOTIFY_IMPL}} } // Plugin } // Thunder \ No newline at end of file diff --git a/PluginSkeletonGenerator/utils/FileUtils.py b/PluginSkeletonGenerator/utils/FileUtils.py index 4a1b1ed1..e246db39 100644 --- a/PluginSkeletonGenerator/utils/FileUtils.py +++ b/PluginSkeletonGenerator/utils/FileUtils.py @@ -1,6 +1,11 @@ import re class FileUtils: + @staticmethod + def _postProcess(code: str) -> str: + code = re.sub(r"\b(Metadata\s*<[^>]+>)\s*(?=[A-Za-z_])", r"\1 ", code) + return code + @staticmethod def replaceKeywords(template, keywords): lines = template.split('\n') @@ -21,7 +26,7 @@ def replaceKeywords(template, keywords): replaced_line = pattern.sub(lambda m: keywords[m.group(0)], line) result_lines.append(replaced_line) - return '\n'.join(result_lines) + return FileUtils._postProcess('\n'.join(result_lines)) @staticmethod def readFile(template_name): From 807fd39ec488f6c0dc53ba76296310ec236ac066 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Thu, 26 Feb 2026 05:37:25 -0800 Subject: [PATCH 02/19] apache and workflow --- .../core/GeneratorCoordinator.py | 19 +++++++++++++++ .../core/GlobalVariables.py | 19 +++++++++++++++ .../core/PluginBlueprint.py | 20 +++++++++++++++- PluginSkeletonGenerator/data/FileData.py | 24 +++++++++++++++++-- .../generators/PluginRepositoryGenerator.py | 21 ++++++++++++++-- PluginSkeletonGenerator/menu/Menu.py | 19 +++++++++++++++ PluginSkeletonGenerator/parser/MultiParser.py | 19 +++++++++++++++ PluginSkeletonGenerator/parser/Parser.py | 19 +++++++++++++++ 8 files changed, 155 insertions(+), 5 deletions(-) diff --git a/PluginSkeletonGenerator/core/GeneratorCoordinator.py b/PluginSkeletonGenerator/core/GeneratorCoordinator.py index a40c52e6..3f53b5d9 100644 --- a/PluginSkeletonGenerator/core/GeneratorCoordinator.py +++ b/PluginSkeletonGenerator/core/GeneratorCoordinator.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + from core.PluginBlueprint import PluginBlueprint from data.FileData import HeaderData, SourceData, CMakeData, JSONData, ConfData from generators.PluginRepositoryGenerator import PluginRepositoryGenerator diff --git a/PluginSkeletonGenerator/core/GlobalVariables.py b/PluginSkeletonGenerator/core/GlobalVariables.py index 02fd4ae0..ab9547e8 100644 --- a/PluginSkeletonGenerator/core/GlobalVariables.py +++ b/PluginSkeletonGenerator/core/GlobalVariables.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + import os def findRepoRoot(filename="PluginSkeletonGenerator.py"): diff --git a/PluginSkeletonGenerator/core/PluginBlueprint.py b/PluginSkeletonGenerator/core/PluginBlueprint.py index 33312f4a..d36e407d 100644 --- a/PluginSkeletonGenerator/core/PluginBlueprint.py +++ b/PluginSkeletonGenerator/core/PluginBlueprint.py @@ -1,5 +1,23 @@ -from typing import Dict, List, Tuple +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' +from typing import Dict, List, Tuple class PluginBlueprint: class ParsedPluginInfo: diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 4e0e3901..69d6a8c6 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + from abc import ABC from enum import Enum from core.PluginBlueprint import PluginBlueprint @@ -437,7 +456,8 @@ def _generateHeaderNotify(self) -> str: result = [] for fq_name, cls_data in self.m_blueprint.notification_entries: for m in cls_data.m_methods: - paramsNoNames, _ = self.commentParamnames(m.m_params) + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=True) + paramsNoNames, _ = self.commentParamnames(qualified_params) result.append(f"void Notify{m.m_name}({paramsNoNames}) const;") return generateSimpleText(result) @@ -502,7 +522,7 @@ def _generatePublicField(self): return "public:" def _generatePrivateMembersField(self): - if not self.m_out_of_process and not self.m_plugin_config: + if not self.m_out_of_process and not self.m_plugin_config and not self.m_notification_entries: return "" return "private:" diff --git a/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py b/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py index 8fc975e8..08ec5d6a 100644 --- a/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py +++ b/PluginSkeletonGenerator/generators/PluginRepositoryGenerator.py @@ -1,11 +1,28 @@ -import os +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' +import os from data.FileData import FileData from utils.FileUtils import FileUtils from utils.Indenter import Indenter import core.GlobalVariables as GlobalVariables - class PluginRepositoryGenerator: def __init__(self, blueprint) -> None: self.m_plugin_name = blueprint.name diff --git a/PluginSkeletonGenerator/menu/Menu.py b/PluginSkeletonGenerator/menu/Menu.py index 10e27566..937e90a4 100644 --- a/PluginSkeletonGenerator/menu/Menu.py +++ b/PluginSkeletonGenerator/menu/Menu.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + import os import sys import yaml diff --git a/PluginSkeletonGenerator/parser/MultiParser.py b/PluginSkeletonGenerator/parser/MultiParser.py index 1fb11c15..308702c9 100644 --- a/PluginSkeletonGenerator/parser/MultiParser.py +++ b/PluginSkeletonGenerator/parser/MultiParser.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + import os from typing import List, Dict, Tuple from parser.Parser import Parser, ClassData diff --git a/PluginSkeletonGenerator/parser/Parser.py b/PluginSkeletonGenerator/parser/Parser.py index 9f5dc21f..cffd53bd 100755 --- a/PluginSkeletonGenerator/parser/Parser.py +++ b/PluginSkeletonGenerator/parser/Parser.py @@ -1,3 +1,22 @@ +''' + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +''' + import re from enum import Enum import os From 3a2e837b145a32b2124ab696d18fd24573c3ebdc Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Thu, 26 Feb 2026 05:40:18 -0800 Subject: [PATCH 03/19] workflow --- .github/workflows/PluginSkeletonGenerator.yml | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index a2f43d58..b85b712f 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -39,17 +39,49 @@ jobs: id: set-matrix shell: python run: | - import json, os + import json + import os variants = [ - { "name": "InProcess", "header": "IMath.h", "answers": ["InProcess", "N", "N", "__INTERFACES_H__", "", "N", ""] }, - { "name": "OutOfProcess", "header": "IBrowser.h", "answers": ["OutOfProcess", "Y", "N", "__INTERFACES_H__", "", "", "N", ""] }, - { "name": "InProcessConfig", "header": "IPower.h", "answers": ["InProcessConfig", "N", "Y", "__INTERFACES_H__", "", "N", ""] }, - { "name": "OutOfProcessConfig", "header": "INetworkControl.h", "answers": ["OutOfProcessConfig", "Y", "Y", "__INTERFACES_H__", "", "N", ""] }, - { "name": "InProcessPreconditions", "header": "ITimeSync.h", "answers": ["InProcessPreconditions", "N", "N", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] }, - { "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", "answers": ["OutOfProcessPreconditions", "Y", "N", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] }, - { "name": "InProcessConfigPreconditions", "header": "IDictionary.h", "answers": ["InProcessConfigPreconditions", "N", "Y", "__INTERFACES_H__", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] }, - { "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", "answers": ["OutOfProcessConfigPreconditions", "Y", "Y", "__INTERFACES_H__", "", "", "Y", "GRAPHICS", "", "NOT_GRAPHICS" , "", "TIME", "", ""] } + # Answers order matches generator prompts: + # 1) plugin name + # 2) output location (empty = default current directory) + # 3) out-of-process (Y/N) + # 4) custom config (Y/N) + # 5) interface path(s) (first may be __INTERFACES_H__) + # 6) blank line to finish interface list + # 7) (optional) if multiple root interfaces: pick indices or Enter for ALL + # 8) include location prompt: Enter for default 'interfaces' + # 9) preconditions (Y/N) + # 10+) (if preconditions=Y) additional preconditions answers... + + { "name": "InProcess", "header": "IMath.h", + "answers": ["InProcess", "", "N", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + + { "name": "OutOfProcess", "header": "IBrowser.h", + "answers": ["OutOfProcess", "", "Y", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + + { "name": "InProcessConfig", "header": "IPower.h", + "answers": ["InProcessConfig", "", "N", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + + { "name": "OutOfProcessConfig", "header": "INetworkControl.h", + "answers": ["OutOfProcessConfig", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + + { "name": "InProcessPreconditions", "header": "ITimeSync.h", + "answers": ["InProcessPreconditions", "", "N", "N", "__INTERFACES_H__", "", "", "", "Y", + "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + + { "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", + "answers": ["OutOfProcessPreconditions", "", "Y", "N", "__INTERFACES_H__", "", "", "", "Y", + "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + + { "name": "InProcessConfigPreconditions", "header": "IDictionary.h", + "answers": ["InProcessConfigPreconditions", "", "N", "Y", "__INTERFACES_H__", "", "", "", "Y", + "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + + { "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", + "answers": ["OutOfProcessConfigPreconditions", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "Y", + "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] } ] include = [] From 77997fc13d21d78dbfcf23f92e1cced91646e038 Mon Sep 17 00:00:00 2001 From: nxtum <94901881+nxtum@users.noreply.github.com> Date: Thu, 26 Feb 2026 06:18:47 -0800 Subject: [PATCH 04/19] Apply suggestion from @Copilot - workflow Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/PluginSkeletonGenerator.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index b85b712f..93bc304f 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -50,37 +50,37 @@ jobs: # 4) custom config (Y/N) # 5) interface path(s) (first may be __INTERFACES_H__) # 6) blank line to finish interface list - # 7) (optional) if multiple root interfaces: pick indices or Enter for ALL - # 8) include location prompt: Enter for default 'interfaces' - # 9) preconditions (Y/N) - # 10+) (if preconditions=Y) additional preconditions answers... + # 7) subsystems (Y/N) + # 8) (optional) if multiple root interfaces: pick indices or Enter for ALL + # 9) include location prompt: Enter for default 'interfaces' + # 10+) (if subsystems=Y) subsystem entries: name/condition pairs and final blank line { "name": "InProcess", "header": "IMath.h", - "answers": ["InProcess", "", "N", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": ["InProcess", "", "N", "N", "__INTERFACES_H__", "", "N", "", ""] }, { "name": "OutOfProcess", "header": "IBrowser.h", - "answers": ["OutOfProcess", "", "Y", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": ["OutOfProcess", "", "Y", "N", "__INTERFACES_H__", "", "N", "", ""] }, { "name": "InProcessConfig", "header": "IPower.h", - "answers": ["InProcessConfig", "", "N", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": ["InProcessConfig", "", "N", "Y", "__INTERFACES_H__", "", "N", "", ""] }, { "name": "OutOfProcessConfig", "header": "INetworkControl.h", - "answers": ["OutOfProcessConfig", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": ["OutOfProcessConfig", "", "Y", "Y", "__INTERFACES_H__", "", "N", "", ""] }, { "name": "InProcessPreconditions", "header": "ITimeSync.h", - "answers": ["InProcessPreconditions", "", "N", "N", "__INTERFACES_H__", "", "", "", "Y", + "answers": ["InProcessPreconditions", "", "N", "N", "__INTERFACES_H__", "", "Y", "", "", "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, { "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", - "answers": ["OutOfProcessPreconditions", "", "Y", "N", "__INTERFACES_H__", "", "", "", "Y", + "answers": ["OutOfProcessPreconditions", "", "Y", "N", "__INTERFACES_H__", "", "Y", "", "", "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, { "name": "InProcessConfigPreconditions", "header": "IDictionary.h", - "answers": ["InProcessConfigPreconditions", "", "N", "Y", "__INTERFACES_H__", "", "", "", "Y", + "answers": ["InProcessConfigPreconditions", "", "N", "Y", "__INTERFACES_H__", "", "Y", "", "", "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, { "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", - "answers": ["OutOfProcessConfigPreconditions", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "Y", + "answers": ["OutOfProcessConfigPreconditions", "", "Y", "Y", "__INTERFACES_H__", "", "Y", "", "", "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] } ] From 7b593121886972c6e09a3268e6507eb7592ea43d Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Fri, 27 Feb 2026 03:02:01 -0800 Subject: [PATCH 05/19] workflow metadata --- .github/workflows/PluginSkeletonGenerator.yml | 88 ++++++++++++++----- PluginSkeletonGenerator/CombinedPlugins.sh | 45 +++++++--- PluginSkeletonGenerator/menu/Menu.py | 2 +- .../templates/.plugin_source.txt | 2 +- 4 files changed, 101 insertions(+), 36 deletions(-) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index b85b712f..d5bcbf9e 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -42,46 +42,86 @@ jobs: import json import os - variants = [ - # Answers order matches generator prompts: - # 1) plugin name - # 2) output location (empty = default current directory) - # 3) out-of-process (Y/N) - # 4) custom config (Y/N) - # 5) interface path(s) (first may be __INTERFACES_H__) - # 6) blank line to finish interface list - # 7) (optional) if multiple root interfaces: pick indices or Enter for ALL - # 8) include location prompt: Enter for default 'interfaces' - # 9) preconditions (Y/N) - # 10+) (if preconditions=Y) additional preconditions answers... + def mk_answers(plugin_name, oop, custom_cfg, pre=None): + """ + Prompt order (interactive mode): + 1) plugin name + 2) output dir (Enter = current directory) + 3) out-of-process (Y/N) + 4) custom config (Y/N) + 5+) interface path(s) ... then blank line + next) subsystems (Y/N) + if subsystems=Y: + Preconditions list entries ... blank + Terminations list entries ... blank + Controls list entries ... blank + next) (optional, per header with >1 root) pick indices or Enter for ALL + next) include location per header (Enter for default) + """ + if pre is None: + pre = {"pre": [], "term": [], "ctrl": []} + + answers = [ + plugin_name, + "", # output dir: Enter => current directory + oop, + custom_cfg, + "__INTERFACES_H__", # interface path injected later + "", # finish interface list + ("Y" if (pre["pre"] or pre["term"] or pre["ctrl"]) else "N"), + ] + + if answers[-1] == "Y": + # Preconditions list + terminator + answers += list(pre["pre"]) + [""] + # Terminations list + terminator + answers += list(pre["term"]) + [""] + # Controls list + terminator + answers += list(pre["ctrl"]) + [""] + + # After subsystems collection, generator may ask: + # - (optional) interface selection (Enter=ALL) for multi-root headers + # - include location (Enter=default 'interfaces') + answers += ["", ""] + + return answers + variants = [ { "name": "InProcess", "header": "IMath.h", - "answers": ["InProcess", "", "N", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": mk_answers("InProcess", "N", "N") }, { "name": "OutOfProcess", "header": "IBrowser.h", - "answers": ["OutOfProcess", "", "Y", "N", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": mk_answers("OutOfProcess", "Y", "N") }, { "name": "InProcessConfig", "header": "IPower.h", - "answers": ["InProcessConfig", "", "N", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": mk_answers("InProcessConfig", "N", "Y") }, { "name": "OutOfProcessConfig", "header": "INetworkControl.h", - "answers": ["OutOfProcessConfig", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "N", ""] }, + "answers": mk_answers("OutOfProcessConfig", "Y", "Y") }, { "name": "InProcessPreconditions", "header": "ITimeSync.h", - "answers": ["InProcessPreconditions", "", "N", "N", "__INTERFACES_H__", "", "", "", "Y", - "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + "answers": mk_answers( + "InProcessPreconditions", "N", "N", + pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + ) }, { "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", - "answers": ["OutOfProcessPreconditions", "", "Y", "N", "__INTERFACES_H__", "", "", "", "Y", - "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + "answers": mk_answers( + "OutOfProcessPreconditions", "Y", "N", + pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + ) }, { "name": "InProcessConfigPreconditions", "header": "IDictionary.h", - "answers": ["InProcessConfigPreconditions", "", "N", "Y", "__INTERFACES_H__", "", "", "", "Y", - "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] }, + "answers": mk_answers( + "InProcessConfigPreconditions", "N", "Y", + pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + ) }, { "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", - "answers": ["OutOfProcessConfigPreconditions", "", "Y", "Y", "__INTERFACES_H__", "", "", "", "Y", - "GRAPHICS", "", "NOT_GRAPHICS", "", "TIME", "", ""] } + "answers": mk_answers( + "OutOfProcessConfigPreconditions", "Y", "Y", + pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + ) }, ] include = [] diff --git a/PluginSkeletonGenerator/CombinedPlugins.sh b/PluginSkeletonGenerator/CombinedPlugins.sh index e313238b..e2ce8a63 100755 --- a/PluginSkeletonGenerator/CombinedPlugins.sh +++ b/PluginSkeletonGenerator/CombinedPlugins.sh @@ -48,6 +48,22 @@ fi TARGET_DIR="$TARGET_ROOT_ABS/CombinedPlugins" mkdir -p "$TARGET_DIR" +# Optional subsystem lists can be provided via environment variables: +# PSG_PRECONDITIONS (newline-separated) +# PSG_TERMINATIONS (newline-separated) +# PSG_CONTROLS (newline-separated) +# If any of these are set (non-empty), the script answers 'Y' to the subsystems prompt and +# feeds the lists (each terminated by a blank line). Otherwise it answers 'N'. +_psg_emit_list() { + local content="$1" + if [ -n "$content" ]; then + # Print as-is, ensure it ends with a newline + printf '%s\n' "$content" + fi + # terminating blank line + printf '\n' +} + # Work inside CombinedPlugins in a subshell so the parent shell CWD is unchanged ( cd "$TARGET_DIR" @@ -56,16 +72,25 @@ mkdir -p "$TARGET_DIR" for i in $(seq 1 "$COUNT"); do plugin="Plugin$i" - # Answers (one per prompt): - # 1) plugin name - # 2) output directory (empty => current directory) - # 3) out of process (N) - # 4) custom config (N) - # 5) interface path - # 6) done adding interfaces (empty) - # 7) relies on subsystems (N) - # 8) include location for the interface (empty => default) - printf '%s\n\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py" + if [ -n "${PSG_PRECONDITIONS:-}" ] || [ -n "${PSG_TERMINATIONS:-}" ] || [ -n "${PSG_CONTROLS:-}" ]; then + # Subsystems enabled + { + printf '%s\n' "$plugin" # name + printf '\n' # output dir (default) + printf 'N\n' # out-of-process + printf 'N\n' # custom config + printf '%s\n' "$IFACE_ABS"# interface path + printf '\n' # finish interface list + printf 'Y\n' # subsystems enabled + _psg_emit_list "${PSG_PRECONDITIONS:-}" # Preconditions + _psg_emit_list "${PSG_TERMINATIONS:-}" # Terminations + _psg_emit_list "${PSG_CONTROLS:-}" # Controls + printf '\n' # include location (default) + } | python3 "$START_DIR/PluginSkeletonGenerator.py" + else + # No subsystems + printf '%s\n\nN\nN\n%s\n\nN\n\n' "$plugin" "$IFACE_ABS" | python3 "$START_DIR/PluginSkeletonGenerator.py" + fi done # Create CMakeLists.txt with one add_subdirectory line per plugin diff --git a/PluginSkeletonGenerator/menu/Menu.py b/PluginSkeletonGenerator/menu/Menu.py index 937e90a4..88a6f08f 100644 --- a/PluginSkeletonGenerator/menu/Menu.py +++ b/PluginSkeletonGenerator/menu/Menu.py @@ -44,7 +44,7 @@ def validatePaths(paths: List[str]) -> None: if files: print(f"[ERROR]: Following header paths do not exist: ") for missing in files: - print(f"-{missing}") + print(f"- {missing}") sys.exit(1) diff --git a/PluginSkeletonGenerator/templates/.plugin_source.txt b/PluginSkeletonGenerator/templates/.plugin_source.txt index e6f0f50b..d05aa6ad 100644 --- a/PluginSkeletonGenerator/templates/.plugin_source.txt +++ b/PluginSkeletonGenerator/templates/.plugin_source.txt @@ -24,7 +24,7 @@ namespace Plugin { namespace { - static Metadata<{{PLUGIN_NAME}}>metadata( + static Metadata<{{PLUGIN_NAME}}> metadata( // Version 1, 0, 0, // Preconditions From 731768e8d63ba7ad5eb7de8806c1fb98799f70ae Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Mon, 2 Mar 2026 06:23:11 -0800 Subject: [PATCH 06/19] const_cast --- PluginSkeletonGenerator/data/FileData.py | 74 +++++++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 69d6a8c6..dbf8b8ab 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -30,6 +30,9 @@ from utils.CodeGenUtils import convertToBaseName, convertToCOMRPC, convertToJSONRPC class FileData(ABC): + _BLOCK_COMMENT_RE = r"/\*.*?\*/" + _CONST_PREFIX = "const " + def __init__(self, blueprint: PluginBlueprint) -> None: self.m_blueprint: PluginBlueprint = blueprint self.m_plugin_name: str = blueprint.name @@ -49,6 +52,7 @@ def __init__(self, blueprint: PluginBlueprint) -> None: self.m_terminations = blueprint._TERMINATIONS self.m_controls = blueprint._CONTROLS self.m_event_notification_entries = getattr(blueprint, "event_notification_entries", []) + self.m_warnings: List[str] = [] @staticmethod def _dedupe_preserve_order(items: List[str]) -> List[str]: @@ -86,7 +90,7 @@ def commentParamnames(params: str): names: List[str] = [] for chunk in parts: - no_comments = re.sub(r"/\*.*?\*/", "", chunk) + no_comments = re.sub(FileData._BLOCK_COMMENT_RE, "", chunk) no_default = no_comments.split("=")[0].strip() # Examples of named params: # `const string& msg` @@ -212,7 +216,7 @@ def _qualify_first_type_token_in_param( if "::" in param: return param - analysis = re.sub(r"/\*.*?\*/", "", param) + analysis = re.sub(FileData._BLOCK_COMMENT_RE, "", param) analysis = analysis.split("=")[0].strip() tokens = analysis.split() qualifiers = {"const", "volatile", "signed", "unsigned", "struct", "class", "enum"} @@ -327,6 +331,34 @@ def _defaultReturnStatement(return_type: str) -> List[str]: return [f" return {rt}();"] + @staticmethod + def _first_param_is_const(params: str) -> bool: + if not params: + return False + p = re.sub(FileData._BLOCK_COMMENT_RE, "", params).strip() + first = p.split(",", 1)[0].strip() + return first.startswith(FileData._CONST_PREFIX) + + def _notif_unregister_param_is_const(self, iface_short: str) -> bool: + full_name = self.resolveFullName(iface_short) + if not full_name: + return True + cls_data, _ = self.m_parsed.get(full_name, (None, None)) + if not cls_data: + return True + + for m in getattr(cls_data, "m_methods", []): + if getattr(m, "m_name", None) == "Unregister": + return self._first_param_is_const(getattr(m, "m_params", "")) + return True + + def _maybe_warn_nonconst_unregister(self, iface_short: str) -> None: + if not self._notif_unregister_param_is_const(iface_short): + self.m_warnings.append( + f"[WARNING] {iface_short}::Unregister() does not take a const notification pointer. " + "Const should be enforced however the generator will const_cast where required to compile." + ) + class HeaderData(FileData): class HeaderType(Enum): HEADER = 1 @@ -698,8 +730,12 @@ def _generateHeaderInheritedMethod(self) -> str: "}" ], remove_empty=False)) else: + is_const = self._notif_unregister_param_is_const(short_name) + if not is_const: + self._maybe_warn_nonconst_unregister(short_name) + const_kw = FileData._CONST_PREFIX if is_const else "" lines.append(generateSimpleText([ - f"{m.m_return_type} Unregister(const {namespace}::{short_name}::INotification* notification) override {{", + f"{m.m_return_type} Unregister({const_kw}{namespace}::{short_name}::INotification* notification) override {{", self.notification_unregisters(short_name), *return_stmt, "}" @@ -916,18 +952,22 @@ def _generateSourcePluginMethods(self) -> str: impl_map = {iface: f"_impl{convertToBaseName(iface)}" for iface in self.m_comrpc_interfaces} for fq_name, _ in entries: - # fq_name like Exchange::IBrowser::INotification root_iface = fq_name.split("::")[-2] unregister_impl = impl_map.get(root_iface) if not unregister_impl: - # Fallback to first interface if we can't resolve (should not happen for selected roots) unregister_impl = impl_map.get(self.m_comrpc_interfaces[0]) + unreg_const = self._notif_unregister_param_is_const(root_iface) + if not unreg_const: + self._maybe_warn_nonconst_unregister(root_iface) + + cast_expr = "revokedInterface" if unreg_const else f"const_cast<{fq_name}*>(revokedInterface)" + dangling.append( f" if (interfaceId == {fq_name}::ID) {{\n" f" auto* revokedInterface = remote->QueryInterface<{fq_name}>();\n" f" if (revokedInterface) {{\n" - f" {unregister_impl}->Unregister(revokedInterface);\n" + f" {unregister_impl}->Unregister({cast_expr});\n" f" revokedInterface->Release();\n" f" }}\n" f" }}\n" @@ -935,7 +975,9 @@ def _generateSourcePluginMethods(self) -> str: dangling.append("}\n") - return "\n".join(method) + "\n".join(dangling) + out = "\n".join(method) + "\n".join(dangling) + self._emit_warning_report() + return out lines = [] @@ -961,8 +1003,12 @@ def _generateSourcePluginMethods(self) -> str: "}" ]) else: + is_const = self._notif_unregister_param_is_const(iface) + if not is_const: + self._maybe_warn_nonconst_unregister(iface) + const_kw = FileData._CONST_PREFIX if is_const else "" lines.extend([ - f"{method.m_return_type} {self.m_plugin_name}::Unregister(const {namespace}::{iface}::INotification* notification) {{", + f"{method.m_return_type} {self.m_plugin_name}::Unregister({const_kw}{namespace}::{iface}::INotification* notification) {{", self.notification_unregisters(iface).strip(), *return_stmt, "}" @@ -985,7 +1031,9 @@ def _generateSourcePluginMethods(self) -> str: f"}}" ]) - return "\n".join(lines) + out = "\n".join(lines) + self._emit_warning_report() + return out def _generateSourceNotify(self) -> str: """Generate notification method implementations for in-process plugins""" @@ -1236,6 +1284,14 @@ def _generateDeinitializeOOP(self): return generateSimpleText(lines, remove_empty=False) + def _emit_warning_report(self) -> None: + if not getattr(self, "m_warnings", None): + return + unique = FileData._dedupe_preserve_order(self.m_warnings) + if not unique: + return + print("\n".join(unique)) + class CMakeData(FileData): def static_keys(self) -> dict: return { From 785d6a1b7e0ad4e147b8403381836e8a238e7676 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Mon, 2 Mar 2026 06:53:33 -0800 Subject: [PATCH 07/19] event --- .../core/PluginBlueprint.py | 12 ++++++-- PluginSkeletonGenerator/data/FileData.py | 29 +++++++------------ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/PluginSkeletonGenerator/core/PluginBlueprint.py b/PluginSkeletonGenerator/core/PluginBlueprint.py index d36e407d..2d7c2c71 100644 --- a/PluginSkeletonGenerator/core/PluginBlueprint.py +++ b/PluginSkeletonGenerator/core/PluginBlueprint.py @@ -44,11 +44,17 @@ def processInterfaces(self, parsed_data: Dict[str, Tuple]): json_name = f"J{root_name[1:]}" if root_name.startswith("I") else f"J{root_name}" self._jsonrpc_interfaces.append(json_name) - if self.hasNotification(cls_data): - self._notification_interfaces.append(root_name) - self.gatherNotificationEntries(full_name, cls_data) + event_roots = [] + seen = set() + for fq, _ in self._event_notification_entries: + root = fq.split("::")[-2] + if root not in seen: + seen.add(root) + event_roots.append(root) + self._notification_interfaces = event_roots + @staticmethod def extractRootName(full_name: str) -> str: return full_name.split("::")[-1] diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index dbf8b8ab..a66b70ef 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -409,14 +409,14 @@ def _generateHeaderDanglingMethod(self): return f'void Dangling(const Core::IUnknown* remote, const uint32_t interfaceId);' if self.m_out_of_process and self.m_notification_entries else '' def _generateNotifClass(self): - entries = self.m_blueprint.notification_entries + entries = (self.m_event_notification_entries if self.m_jsonrpc else []) if not entries: return ": public RPC::IRemoteConnection::INotification" derived = [f"public {fq_name}" for fq_name, _ in entries] return generateSimpleText([": public RPC::IRemoteConnection::INotification, public PluginHost::IShell::ICOMLink::INotification"] + derived, sep=", ") def _generateNotifConstructor(self): - entries = self.m_blueprint.notification_entries + entries = (self.m_event_notification_entries if self.m_jsonrpc else []) lines = [] if entries: lines.append(", PluginHost::IShell::ICOMLink::INotification()") @@ -424,7 +424,7 @@ def _generateNotifConstructor(self): return generateSimpleText(lines) def _generateNotifEntry(self): - entries = self.m_blueprint.notification_entries + entries = (self.m_event_notification_entries if self.m_jsonrpc else []) lines = [] if entries: lines.append('INTERFACE_ENTRY(PluginHost::IShell::ICOMLink::INotification)') @@ -432,8 +432,7 @@ def _generateNotifEntry(self): return generateSimpleText(lines) def _generateNotifFunction(self): - all_entries = list(getattr(self.m_blueprint, "notification_entries", [])) - event_entries = {fq: cls for fq, cls in (self.m_event_notification_entries if self.m_jsonrpc else [])} + all_entries = list(self.m_event_notification_entries if self.m_jsonrpc else []) result = [] @@ -454,20 +453,12 @@ def _generateNotifFunction(self): param_str = ", ".join(param_names) add_const = " const" if getattr(m, "m_is_const", False) else "" - if fq_name in event_entries: - result.extend([ - f"void {m.m_name}({qualified_params}) override {{", - f" Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};", - "}", - "", - ]) - else: - result.extend([ - f"void {m.m_name}({qualified_params}) override {{", - " // Intentionally left blank (non-@event notification).", - "}", - "", - ]) + result.extend([ + f"void {m.m_name}({qualified_params}) override {{", + f" Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};", + "}", + "", + ]) return generateSimpleText(result, remove_empty=True, sep="\n") From b73a8f7ff8b607495af657da4a7191c4d1b92b5f Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 01:46:13 -0800 Subject: [PATCH 08/19] notif class fix --- .../core/PluginBlueprint.py | 8 ++++---- PluginSkeletonGenerator/data/FileData.py | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/PluginSkeletonGenerator/core/PluginBlueprint.py b/PluginSkeletonGenerator/core/PluginBlueprint.py index 2d7c2c71..a20673b9 100644 --- a/PluginSkeletonGenerator/core/PluginBlueprint.py +++ b/PluginSkeletonGenerator/core/PluginBlueprint.py @@ -46,14 +46,14 @@ def processInterfaces(self, parsed_data: Dict[str, Tuple]): self.gatherNotificationEntries(full_name, cls_data) - event_roots = [] + all_roots: List[str] = [] seen = set() - for fq, _ in self._event_notification_entries: + for fq, _ in self._notification_entries: root = fq.split("::")[-2] if root not in seen: seen.add(root) - event_roots.append(root) - self._notification_interfaces = event_roots + all_roots.append(root) + self._notification_interfaces = all_roots @staticmethod def extractRootName(full_name: str) -> str: diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index a66b70ef..9d8b6cec 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -474,23 +474,27 @@ def _generateConfigureOOPMethod(self): def _generateHeaderNotify(self) -> str: if not self.m_notification_interfaces: return '' - + + event_entries = list(self.m_event_notification_entries if self.m_jsonrpc else []) + if not event_entries: + return '' + if self.m_type == self.HeaderType.HEADER and not self.m_out_of_process: result = [] - for fq_name, cls_data in self.m_blueprint.notification_entries: + for fq_name, cls_data in event_entries: for m in cls_data.m_methods: qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=True) paramsNoNames, _ = self.commentParamnames(qualified_params) result.append(f"void Notify{m.m_name}({paramsNoNames}) const;") return generateSimpleText(result) - + if self.m_type == self.HeaderType.HEADER_IMPLEMENTATION: result = [] - for fq_name, cls_data in self.m_blueprint.notification_entries: + for fq_name, cls_data in event_entries: iface = fq_name.split("::")[-2] no_i = iface[1:] if iface.startswith("I") else iface container = f"_{no_i.lower()}Notification" - + for m in cls_data.m_methods: qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=True) _, param_names = self.commentParamnames(qualified_params) @@ -1031,9 +1035,13 @@ def _generateSourceNotify(self) -> str: if self.m_out_of_process or not self.m_notification_interfaces: return '' + event_entries = list(self.m_event_notification_entries if self.m_jsonrpc else []) + if not event_entries: + return '' + all_methods = [] - for fq_name, cls_data in self.m_blueprint.notification_entries: + for fq_name, cls_data in event_entries: iface = fq_name.split("::")[-2] no_i = iface[1:] if iface.startswith("I") else iface container = f"_{no_i.lower()}Notification" From 9329550a3b8d41439dd6180e02ce57328de407ee Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 02:18:39 -0800 Subject: [PATCH 09/19] actually now notif class --- PluginSkeletonGenerator/data/FileData.py | 92 ++++++++++++++++-------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 9d8b6cec..fc9bbc34 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -409,56 +409,88 @@ def _generateHeaderDanglingMethod(self): return f'void Dangling(const Core::IUnknown* remote, const uint32_t interfaceId);' if self.m_out_of_process and self.m_notification_entries else '' def _generateNotifClass(self): - entries = (self.m_event_notification_entries if self.m_jsonrpc else []) - if not entries: + if not self.m_out_of_process: return ": public RPC::IRemoteConnection::INotification" - derived = [f"public {fq_name}" for fq_name, _ in entries] - return generateSimpleText([": public RPC::IRemoteConnection::INotification, public PluginHost::IShell::ICOMLink::INotification"] + derived, sep=", ") + + notif_entries = list(getattr(self.m_blueprint, "notification_entries", [])) + if not notif_entries: + return ": public RPC::IRemoteConnection::INotification" + + derived = [f"public {fq_name}" for fq_name, _ in notif_entries] + return generateSimpleText( + [": public RPC::IRemoteConnection::INotification, public PluginHost::IShell::ICOMLink::INotification"] + + derived, + sep=", ") def _generateNotifConstructor(self): - entries = (self.m_event_notification_entries if self.m_jsonrpc else []) - lines = [] - if entries: + if not self.m_out_of_process: + return "" + + notif_entries = list(getattr(self.m_blueprint, "notification_entries", [])) + lines: List[str] = [] + if notif_entries: lines.append(", PluginHost::IShell::ICOMLink::INotification()") - lines.extend([f", {fq_name}()" for fq_name, _ in entries]) + lines.extend([f", {fq_name}()" for fq_name, _ in notif_entries]) return generateSimpleText(lines) def _generateNotifEntry(self): - entries = (self.m_event_notification_entries if self.m_jsonrpc else []) - lines = [] - if entries: + if not self.m_out_of_process: + return "" + + notif_entries = list(getattr(self.m_blueprint, "notification_entries", [])) + lines: List[str] = [] + if notif_entries: lines.append('INTERFACE_ENTRY(PluginHost::IShell::ICOMLink::INotification)') - lines.extend([f"INTERFACE_ENTRY({fq_name})" for fq_name, _ in entries]) + lines.extend([f"INTERFACE_ENTRY({fq_name})" for fq_name, _ in notif_entries]) return generateSimpleText(lines) def _generateNotifFunction(self): - all_entries = list(self.m_event_notification_entries if self.m_jsonrpc else []) + if not self.m_out_of_process: + return "" + + all_notif_entries = list(getattr(self.m_blueprint, "notification_entries", [])) + event_map = {fq: cls for fq, cls in (self.m_event_notification_entries if self.m_jsonrpc else [])} - result = [] + result: List[str] = [] - if all_entries: + if all_notif_entries: result.append( "void Dangling(const Core::IUnknown* remote, const uint32_t interfaceId) override {\n" " _parent.Dangling(remote, interfaceId);\n" "}" ) + result.append("") - for fq_name, cls_data in all_entries: - root_iface = fq_name.split("::")[-2] - jprefix = f"J{root_iface[1:]}" if root_iface.startswith("I") else f"J{root_iface}" + for fq_name, cls_data in all_notif_entries: + is_event = fq_name in event_map - for m in cls_data.m_methods: - qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=False) - _, param_names = self.commentParamnames(qualified_params) - param_str = ", ".join(param_names) - add_const = " const" if getattr(m, "m_is_const", False) else "" + if is_event and self.m_jsonrpc: + root_iface = fq_name.split("::")[-2] + jprefix = f"J{root_iface[1:]}" if root_iface.startswith("I") else f"J{root_iface}" - result.extend([ - f"void {m.m_name}({qualified_params}) override {{", - f" Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};", - "}", - "", - ]) + for m in cls_data.m_methods: + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=False) + _, param_names = self.commentParamnames(qualified_params) + param_str = ", ".join(param_names) + add_const = " const" if getattr(m, "m_is_const", False) else "" + + result.extend([ + f"void {m.m_name}({qualified_params}) override {{", + f" Exchange::{jprefix}::Event::{m.m_name}(_parent{', ' + param_str if param_str else ''}){add_const};", + "}", + "", + ]) + else: + for m in cls_data.m_methods: + qualified_params = self._qualifyParamTypes(m.m_params, fq_name, cls_data, prefer_unqualified_owner=False) + params_no_names, _ = self.commentParamnames(qualified_params) + add_const = " const" if getattr(m, "m_is_const", False) else "" + + result.extend([ + f"void {m.m_name}({params_no_names}){add_const} override {{", + "}", + "", + ]) return generateSimpleText(result, remove_empty=True, sep="\n") @@ -622,8 +654,6 @@ def _generateHeaderIncludes(self): location = self.m_locations.get(iface, "interfaces") includes.append(f'#include <{location}/{filename}>') - # JSON includes in the *header* are only needed for JSONRPC event dispatch methods - # that are emitted into Notification (OOP). So only include J-headers for @event notifications. if self.m_type == self.HeaderType.HEADER and self.m_out_of_process and self.m_jsonrpc: needed_roots: List[str] = [] seen = set() From f9a922d2437f7cc7214ad68050b5d40ec1372f2a Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 03:12:29 -0800 Subject: [PATCH 10/19] inprocess notif --- PluginSkeletonGenerator/data/FileData.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index fc9bbc34..2809e14f 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -739,9 +739,21 @@ def _generateHeaderInheritedMethod(self) -> str: for m in cls_data.m_methods: add_const = " const" if getattr(m, "m_is_const", False) else "" + if is_header: - paramsNoNames, _ = self.commentParamnames(m.m_params) - lines.append(f"{m.m_return_type} {m.m_name}({paramsNoNames}){add_const} override;") + if short_name in self.m_notification_interfaces and m.m_name in ("Register", "Unregister"): + is_unreg = (m.m_name == "Unregister") + is_const = self._notif_unregister_param_is_const(short_name) if is_unreg else False + if is_unreg and not is_const: + self._maybe_warn_nonconst_unregister(short_name) + const_kw = FileData._CONST_PREFIX if (is_unreg and is_const) else "" + lines.append( + f"{m.m_return_type} {m.m_name}({const_kw}{namespace}::{short_name}::INotification* const /* sink */){add_const} override;" + ) + else: + paramsNoNames, _ = self.commentParamnames(m.m_params) + lines.append(f"{m.m_return_type} {m.m_name}({paramsNoNames}){add_const} override;") + else: paramsNoNames, _ = self.commentParamnames(m.m_params) From 25f9e9db4f7ef2fb99ace7b93bdab6170da49d40 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 04:26:24 -0800 Subject: [PATCH 11/19] value() and using fix --- PluginSkeletonGenerator/data/FileData.py | 13 +++++++++++-- .../templates/nested_methods/.configure.txt | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 2809e14f..badc00f1 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -808,12 +808,20 @@ def _generateHeaderUsingContainer(self) -> str: return generateSimpleText(lines) + def _patch_unqualified_ids(self, owner_full_name: str, alias_value: str) -> str: + patched = alias_value + if "ID_" not in patched or "::ID_" in patched: + return patched + owner_ns = self._extractStrippedNamespace(owner_full_name) + if not owner_ns: + return patched + return re.sub(r"\b(ID_[A-Z0-9_]+)\b", rf"{owner_ns}::\1", patched) + def _generateHeaderUsingExternal(self) -> str: if self.m_type != self.HeaderType.HEADER_IMPLEMENTATION: return "" usings: Dict[str, str] = {} - for full_name, (cls_data, _) in self.m_parsed.items(): for name, value in getattr(cls_data, "m_usings", {}).items(): if name in usings and usings[name] != value: @@ -822,7 +830,8 @@ def _generateHeaderUsingExternal(self) -> str: "Keeping the first definition." ) continue - usings.setdefault(name, value) + + usings.setdefault(name, self._patch_unqualified_ids(full_name, value)) if not usings: return "" diff --git a/PluginSkeletonGenerator/templates/nested_methods/.configure.txt b/PluginSkeletonGenerator/templates/nested_methods/.configure.txt index 9dd1f1cc..e2ae7514 100644 --- a/PluginSkeletonGenerator/templates/nested_methods/.configure.txt +++ b/PluginSkeletonGenerator/templates/nested_methods/.configure.txt @@ -2,6 +2,6 @@ uint32_t Configure(PluginHost::IShell* service) override { ASSERT(service != nullptr); Config config; config.FromString(service->ConfigLine()); - TRACE(Trace::Information, (_T("This is just an example: [%s]"), config.Example.Value.c_str())); + TRACE(Trace::Information, (_T("This is just an example: [%s]"), config.Example.Value().c_str())); return Core::ERROR_NONE; } \ No newline at end of file From 0ab29988c17c54d237fa94892d35bd6540c2e1a2 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 04:56:25 -0800 Subject: [PATCH 12/19] change interfaces in workflow --- .github/workflows/PluginSkeletonGenerator.yml | 4 ++-- PluginSkeletonGenerator/menu/Menu.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index d5bcbf9e..2ccbfe9d 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -111,13 +111,13 @@ jobs: pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} ) }, - { "name": "InProcessConfigPreconditions", "header": "IDictionary.h", + { "name": "InProcessConfigPreconditions", "header": "IVolumeControl.h", "answers": mk_answers( "InProcessConfigPreconditions", "N", "Y", pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} ) }, - { "name": "OutOfProcessConfigPreconditions", "header": "IBluetooth.h", + { "name": "OutOfProcessConfigPreconditions", "header": "IWifiControl.h", "answers": mk_answers( "OutOfProcessConfigPreconditions", "Y", "Y", pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} diff --git a/PluginSkeletonGenerator/menu/Menu.py b/PluginSkeletonGenerator/menu/Menu.py index 88a6f08f..0d2637ae 100644 --- a/PluginSkeletonGenerator/menu/Menu.py +++ b/PluginSkeletonGenerator/menu/Menu.py @@ -75,7 +75,7 @@ def _prompt_yes_no(question: str, default: bool = False) -> bool: def _validate_plugin_name(name: str) -> str: name = name.strip() if not name: - print("[ERROR]: Plugin name can not be empty") + print("[ERROR]: Plugin name cannot be empty") sys.exit(1) # C++ identifier if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name): From 68b0050143ae17f85c6e67bc504bee1bbb69f56c Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 05:25:27 -0800 Subject: [PATCH 13/19] template for config... --- .../templates/nested_class/.config_class.txt | 14 +++++++------- .../templates/nested_methods/.configure.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/PluginSkeletonGenerator/templates/nested_class/.config_class.txt b/PluginSkeletonGenerator/templates/nested_class/.config_class.txt index a5aeac02..9a980ab1 100644 --- a/PluginSkeletonGenerator/templates/nested_class/.config_class.txt +++ b/PluginSkeletonGenerator/templates/nested_class/.config_class.txt @@ -1,17 +1,17 @@ -class Config : public Core::JSON::Container { +class PluginConfig : public Core::JSON::Container { public: - Config(const Config&) = delete; - Config& operator=(const Config&) = delete; - Config(Config&&) = delete; - Config& operator=(Config&&) = delete; + PluginConfig(const PluginConfig&) = delete; + PluginConfig& operator=(const PluginConfig&) = delete; + PluginConfig(PluginConfig&&) = delete; + PluginConfig& operator=(PluginConfig&&) = delete; - Config() + PluginConfig() : Core::JSON::Container() , Example() { Add(_T("example"), &Example); } - ~Config() override = default; + ~PluginConfig() override = default; public: Core::JSON::String Example; }; \ No newline at end of file diff --git a/PluginSkeletonGenerator/templates/nested_methods/.configure.txt b/PluginSkeletonGenerator/templates/nested_methods/.configure.txt index e2ae7514..07e65d7a 100644 --- a/PluginSkeletonGenerator/templates/nested_methods/.configure.txt +++ b/PluginSkeletonGenerator/templates/nested_methods/.configure.txt @@ -1,6 +1,6 @@ uint32_t Configure(PluginHost::IShell* service) override { ASSERT(service != nullptr); - Config config; + PluginConfig config; config.FromString(service->ConfigLine()); TRACE(Trace::Information, (_T("This is just an example: [%s]"), config.Example.Value().c_str())); return Core::ERROR_NONE; From ffe10ed538a7d497915f340bd29ac2eb8976ffdb Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Wed, 4 Mar 2026 06:06:29 -0800 Subject: [PATCH 14/19] ip config --- PluginSkeletonGenerator/data/FileData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index badc00f1..b1349f42 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -1165,7 +1165,7 @@ def _generateConfigurationIP(self): if not self.m_plugin_config: return '' return ( - "Config config;\n" + "PluginConfig config;\n" "config.FromString(service->ConfigLine());\n" "TRACE(Trace::Information, (_T(\"This is just an example: [%s]\"), config.Example.Value().c_str()));") From b441a6bf840bbf8976dd44164b051ee7ecf209a2 Mon Sep 17 00:00:00 2001 From: nxtum <94901881+nxtum@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:20:11 +0100 Subject: [PATCH 15/19] Update PluginSkeletonGenerator/data/FileData.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- PluginSkeletonGenerator/data/FileData.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index b1349f42..5abc78b3 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -809,13 +809,13 @@ def _generateHeaderUsingContainer(self) -> str: return generateSimpleText(lines) def _patch_unqualified_ids(self, owner_full_name: str, alias_value: str) -> str: - patched = alias_value - if "ID_" not in patched or "::ID_" in patched: - return patched - owner_ns = self._extractStrippedNamespace(owner_full_name) - if not owner_ns: - return patched - return re.sub(r"\b(ID_[A-Z0-9_]+)\b", rf"{owner_ns}::\1", patched) + patched = alias_value + if "ID_" not in patched or "::ID_" in patched: + return patched + owner_ns = self._extractStrippedNamespace(owner_full_name) + if not owner_ns: + return patched + return re.sub(r"\b(ID_[A-Z0-9_]+)\b", rf"{owner_ns}::\1", patched) def _generateHeaderUsingExternal(self) -> str: if self.m_type != self.HeaderType.HEADER_IMPLEMENTATION: From e1131fc5a26eb9d37a6db6c938c7dcf4438003c4 Mon Sep 17 00:00:00 2001 From: Mateusz Daniluk <121170681+VeithMetro@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:03:22 +0100 Subject: [PATCH 16/19] Add step to upload combined diff artifact --- .github/workflows/PluginSkeletonGenerator.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index 2ccbfe9d..fc214cc3 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -502,6 +502,14 @@ jobs: Path("diffs/combined_ai.diff").write_text(text, encoding="utf-8") PY + - name: Upload combined diff artifact + if: github.event_name == 'pull_request' && steps.build_site.outputs.has_diff == 'true' + uses: actions/upload-artifact@v4 + with: + name: generated-plugin-diff + path: diffs/combined.diff + retention-days: 14 + - name: AI summary (GitHub Models) if: github.event_name == 'pull_request' && steps.build_site.outputs.has_diff == 'true' id: ai From a72cfa49ce284f114cadc401ac59daca2a7ada5e Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Mon, 30 Mar 2026 04:23:12 -0700 Subject: [PATCH 17/19] workflow fix, and spacing --- .github/workflows/PluginSkeletonGenerator.yml | 8 ++++---- PluginSkeletonGenerator/data/FileData.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/PluginSkeletonGenerator.yml b/.github/workflows/PluginSkeletonGenerator.yml index fc214cc3..1b902c3b 100644 --- a/.github/workflows/PluginSkeletonGenerator.yml +++ b/.github/workflows/PluginSkeletonGenerator.yml @@ -102,25 +102,25 @@ jobs: { "name": "InProcessPreconditions", "header": "ITimeSync.h", "answers": mk_answers( "InProcessPreconditions", "N", "N", - pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]} ) }, { "name": "OutOfProcessPreconditions", "header": "IMessageControl.h", "answers": mk_answers( "OutOfProcessPreconditions", "Y", "N", - pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]} ) }, { "name": "InProcessConfigPreconditions", "header": "IVolumeControl.h", "answers": mk_answers( "InProcessConfigPreconditions", "N", "Y", - pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]} ) }, { "name": "OutOfProcessConfigPreconditions", "header": "IWifiControl.h", "answers": mk_answers( "OutOfProcessConfigPreconditions", "Y", "Y", - pre={"pre": ["GRAPHICS", "NOT_GRAPHICS", "TIME"], "term": [], "ctrl": []} + pre={"pre": ["GRAPHICS"], "term": ["NOT_GRAPHICS"], "ctrl": ["TIME"]} ) }, ] diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index 5abc78b3..b5ed728b 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -929,21 +929,21 @@ def _generatePreconditions(self): if not self.m_preconditions: return '' entries = self._validateSubsystemEntries(self.m_preconditions, "Preconditions") - lines = [f" subsystem::{entry} " for entry in entries] + lines = [f" subsystem::{entry}" for entry in entries] return generateSimpleText(lines, sep=",") def _generateTerminations(self): if not self.m_terminations: return '' entries = self._validateSubsystemEntries(self.m_terminations, "Terminations") - lines = [f" subsystem::{entry} " for entry in entries] + lines = [f" subsystem::{entry}" for entry in entries] return generateSimpleText(lines, sep=",") def _generateControls(self): if not self.m_controls: return '' entries = self._validateSubsystemEntries(self.m_controls, "Controls") - lines = [f" subsystem::{entry} " for entry in entries] + lines = [f" subsystem::{entry}" for entry in entries] return generateSimpleText(lines, sep=",") def _generateSourceIncludeStatements(self): From 1ca424790d89fe00fabee8fda0db5f9ed31bbaef Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Mon, 30 Mar 2026 04:29:45 -0700 Subject: [PATCH 18/19] whitespace.......... --- PluginSkeletonGenerator/data/FileData.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PluginSkeletonGenerator/data/FileData.py b/PluginSkeletonGenerator/data/FileData.py index b5ed728b..77d4be9b 100644 --- a/PluginSkeletonGenerator/data/FileData.py +++ b/PluginSkeletonGenerator/data/FileData.py @@ -929,22 +929,22 @@ def _generatePreconditions(self): if not self.m_preconditions: return '' entries = self._validateSubsystemEntries(self.m_preconditions, "Preconditions") - lines = [f" subsystem::{entry}" for entry in entries] - return generateSimpleText(lines, sep=",") + lines = [f"subsystem::{entry}" for entry in entries] + return generateSimpleText(lines, sep=", ") def _generateTerminations(self): if not self.m_terminations: return '' entries = self._validateSubsystemEntries(self.m_terminations, "Terminations") - lines = [f" subsystem::{entry}" for entry in entries] - return generateSimpleText(lines, sep=",") + lines = [f"subsystem::{entry}" for entry in entries] + return generateSimpleText(lines, sep=", ") def _generateControls(self): if not self.m_controls: return '' entries = self._validateSubsystemEntries(self.m_controls, "Controls") - lines = [f" subsystem::{entry}" for entry in entries] - return generateSimpleText(lines, sep=",") + lines = [f"subsystem::{entry}" for entry in entries] + return generateSimpleText(lines, sep=", ") def _generateSourceIncludeStatements(self): """ From ec30a5c71683ce7eece112270d28127b8f8238b1 Mon Sep 17 00:00:00 2001 From: nxtumUbun Date: Tue, 7 Apr 2026 05:09:46 -0700 Subject: [PATCH 19/19] use new c++ versions --- PluginSkeletonGenerator/templates/.cmake.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginSkeletonGenerator/templates/.cmake.txt b/PluginSkeletonGenerator/templates/.cmake.txt index 9ffe8712..be051820 100644 --- a/PluginSkeletonGenerator/templates/.cmake.txt +++ b/PluginSkeletonGenerator/templates/.cmake.txt @@ -51,7 +51,7 @@ add_library(${MODULE_NAME} SHARED set_target_properties(${MODULE_NAME} PROPERTIES ~INDENT_INCREASE~ - CXX_STANDARD 11 + CXX_STANDARD ${CXX_STD} CXX_STANDARD_REQUIRED YES) ~INDENT_DECREASE~