Skip to content

feat: add C++ header-only exporter for embedded/MCU use - #526

Open
SoundMatt wants to merge 5 commits into
COVESA:masterfrom
SoundMatt:feat/cpph-exporter
Open

feat: add C++ header-only exporter for embedded/MCU use#526
SoundMatt wants to merge 5 commits into
COVESA:masterfrom
SoundMatt:feat/cpph-exporter

Conversation

@SoundMatt

Copy link
Copy Markdown
Contributor

Summary

  • Adds vspec export cpp-header (new file src/vss_tools/exporters/cpp_header.py)
  • Emits a single .hpp with a VssSignal struct and one constexpr aggregate per leaf signal
  • Allowed-value lists become null-terminated constexpr const char*[] arrays declared immediately above their signal
  • VssSignal.cpp_type maps VSS datatypes to C++ equivalents (uint8_t, float, const char*, bool, …) using the same type table as datatypes.py
  • --namespace (default vss) and --include-branches/--no-include-branches CLI flags
  • No new runtime dependencies; header requires only <cstddef> and <cstdint>

Resolves #513.

Test plan

  • uv run pytest tests/vspec/test_cpp_header/ passes
  • vspec export cpp-header -s <spec> -o out.hpp produces valid C++ (compile-checked manually with g++ -std=c++17 -c out.hpp)
  • Signals with no allowed values emit nullptr for allowed_values
  • Signals with allowed values emit a named null-terminated array above the aggregate

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
@@ -0,0 +1,215 @@
# Copyright (c) 2024 Contributors to COVESA

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New file, change to 2026 unless copied in full from somewhere else

@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright (c) 2024 Contributors to COVESA

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New file, use 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way - does our license checker complains if this file does not have a license text? If this tool is intended to be used by customers the output does not necessarily needs to be with a MPL license, and copyright is not necessarily only COVESA as it is based on signal definitions that might be proprietary.

I am thinking of removing the copyright/license part here, and if needed adjust the license reviewer so it does not care about this file.

@erikbosch

Copy link
Copy Markdown
Collaborator

@JMorceaux - is this what you need?

@erikbosch

Copy link
Copy Markdown
Collaborator

Awaiting feedback from Jeremy. before merging the file https://github.com/COVESA/vss-tools/blob/master/docs/vspec.md shall be expanded and reference a file cpp-header.mdthat gives a short overview of this tool. See how other tools are documented

@JMorceaux

Copy link
Copy Markdown

Thanks @erikbosch for the ping ! Just came back from vacation, I'll definitely look at at this PR in details because it might indeed be exactly what I need :)

Comment on lines +176 to +181
@click.option(
"--include-branches/--no-include-branches",
default=False,
show_default=True,
help="Include branch nodes in addition to leaf signals.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SoundMatt Thank you for this PR. In the current state, your exporter would export the entire vss specs and put all signals in the .hpp. The VSS specs is starting to be a bit heavy and I fear the .text/.bss is gonna be huge if we export all signals. I think it would be nice if the user could give the tool a list of signals (as an option) to export and only these signals would be exported. This would reduce .text/.bss and better suited for MCU use.

Comment on lines +29 to +32
"sensor",
"float",
"float",
"km",

@JMorceaux JMorceaux Jun 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of using const char*/string is that it's pretty robust to change but if we want to make an exporter for embedded system, it is best to think about memory footprint.

I think it would be best if an enum was created for unit/type/datatype/cpp_type

Same thing for the min/max value, I don't think a string is appropriate here

Adds `vspec export cpp-header` which emits a single `.hpp` containing
a `VssSignal` struct and one `constexpr` instance per leaf signal.
Allowed-value lists become null-terminated `constexpr const char*[]`
arrays declared immediately above the signal aggregate.

The struct includes a `cpp_type` field that maps VSS datatypes to their
C++ equivalents (uint8_t, float, const char*, bool, etc.) derived from
the existing Datatypes enum.

Resolves COVESA#513.

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
Adds docs/cpp-header.md covering the exporter's purpose, CLI flags
(--namespace, --include-branches), VSS→C++ type mapping table, and a
concrete model.vspec → vss.hpp example. Adds a reference link in
docs/vspec.md alongside the other exporter docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
…or min/max

Replace all const char* metadata fields in VssSignal with typed enums:
- VssNodeType (sensor/actuator/attribute/branch)
- VssDataType (all VSS primitive and array types)
- VssCppType (all C++ type mappings)
- VssUnit (per-export enum of units actually present; kNone for unset)

min_value/max_value change from const char* to double (kNoValue sentinel
= std::numeric_limits<double>::quiet_NaN() for unset). Requires <limits>.

Also updates copyright year to 2026 in the exporter source and generated
header output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt force-pushed the feat/cpph-exporter branch from 75c7b8e to 274e05f Compare June 15, 2026 13:46
@SoundMatt

Copy link
Copy Markdown
Contributor Author

Updated in the latest commit:

  • Enums for type/datatype/cpp_type/unitVssNodeType, VssDataType, VssCppType are fixed enums covering all VSS types. VssUnit is a per-export enum containing only the units present in the output (so the emitted enum stays small on a filtered spec). All four replace the previous const char* fields.
  • double for min/maxmin_value/max_value are now double with a kNoValue = std::numeric_limits<double>::quiet_NaN() sentinel for unset values (requires <limits>).
  • Copyright year — updated to 2026 in both the exporter source and the generated header.

@JMorceaux — on the --signals filter: before I build it, can you describe the expected UX? My current thinking is a repeatable --signal option that accepts dotted paths, e.g.:

vspec export cpp-header --signal Vehicle.Speed --signal Vehicle.Body.Lights.IsBackupOn -s spec/... -o out.hpp

Only the named paths (and their parent branches, if --include-branches is set) would be emitted. Glob patterns welcome too (Vehicle.ADAS.*).

Does that match what you had in mind, or did you want something different (e.g., a file listing paths, prefix-based filtering, etc.)? Happy to implement once we're aligned on scope.

Comment thread docs/cpp-header.md Outdated

namespace vss {

struct VssSignal {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just Signal. Its already in the vss namespace

from vss_tools.tree import VSSNode

_CPP_TYPE_MAP: dict[str, str] = {
"uint8": "uint8_t",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use datatypes.py definitions

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
}

_NODE_TYPE_ENUM: dict[str, str] = {
"sensor": "VssNodeType::kSensor",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just NodeType?

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
}

_DATATYPE_ENUM: dict[str, str] = {
"uint8": "VssDataType::kUint8",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataType

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
}

_CPP_TYPE_ENUM: dict[str, str] = {
"uint8_t": "VssCppType::kUint8T",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"uint8_t": "VssCppType::kUint8T",
"uint8_t": "kUint8T",

"""Convert a VSS unit string to a C++ enum member name prefixed with k."""
s = unit
s = s.replace("/", "_Per_")
s = s.replace("^", "_Pow_")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what vss unit contains those symbols?

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
else:
allowed_ref = "nullptr"

lines.append(f"constexpr VssSignal {ident} = {{")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be a real template engine like jinja2

Comment thread src/vss_tools/exporters/cpp_header.py Outdated
unit_map = {u: f"VssUnit::{m}" for u, m in seen_units.items()}

out: list[str] = []
out.append("// SPDX-FileCopyrightText: Copyright (c) 2026 Contributors to COVESA")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jinja2

Comment thread src/vss_tools/cli.py
"yaml": "vss_tools.exporters.yaml:cli",
"tree": "vss_tools.exporters.tree:cli",
"samm": "vss_tools.exporters.samm:cli",
"cpp-header": "vss_tools.exporters.cpp_header:cli",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about hpp?

Addresses sschleemilch's review (Jul 21):

- Renamed VssSignal -> Signal, VssNodeType -> NodeType, VssDataType ->
  DataType, VssCppType -> CppType, VssUnit -> Unit. All are already
  scoped inside 'namespace vss', so the prefix was redundant.
- _CPP_TYPE_MAP is now derived from vss_tools.datatypes.Datatypes
  instead of a hand-duplicated dict, so it can't drift out of sync
  with the datatype list vss-tools already treats as canonical.
- _unit_enum_member() dropped its handling of degree/percent/middle-dot/
  asterisk symbols - checked the real spec/units.yaml and no VSS unit
  uses them (only '/', '^', '-' actually occur, e.g. cm/s^2, mpg-uk).
- The hand-rolled out.append(...) boilerplate is now a Jinja2
  template (src/vss_tools/exporters/templates/cpp_header.hpp.j2).
  jinja2 is a new dependency (pyproject.toml, uv.lock).
- The enum-value dicts (_NODE_TYPE_ENUM etc.) now hold bare member
  names ('kUint8T') rather than prefixed ones ('VssCppType::kUint8T'),
  per Sebastian's suggested diff on line 92 - the prefix is added by
  the template instead.

Also implements JMorceaux's still-open request for a signal filter:

- New repeatable --signal option (dotted path, glob '*' supported)
  to restrict output to specific signals - the original motivation
  for this exporter was reducing .text/.bss on MCU targets, which
  exporting the entire spec works against.
- With --include-branches, only the ancestor branches of matched
  signals are pulled in, not the whole tree, to keep that goal intact.

docs/cpp-header.md rewritten to match: renamed types, --signal
documented, and the worked example now runs from the actual exporter
output rather than hand-written text (it had drifted from reality -
the doc's example wasn't buildable, since the Vehicle.Powertrain
branch it uses was never declared).

expected.hpp regenerated from tool output; added 4 new tests for the
--signal filter (exact match, glob, no-match warning, ancestor-branch
inclusion). Full suite (431 tests) passes; ruff, ruff format and mypy
clean. Manually verified the generated header compiles with both g++
and clang++ (-std=c++17).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt

Copy link
Copy Markdown
Contributor Author

@sschleemilch — pushed a round addressing your review:

  • Renamed VssSignalSignal, VssNodeTypeNodeType, VssDataTypeDataType (and VssCppTypeCppType, VssUnitUnit for consistency, since the same "already in namespace vss" reasoning applies to those too — let me know if you'd rather keep those two prefixed).
  • _CPP_TYPE_MAP is now built from vss_tools.datatypes.Datatypes instead of a hand-written dict, so it can't drift from the datatypes vss-tools already treats as canonical.
  • Dropped the °/%/·/* handling in _unit_enum_member — checked spec/units.yaml and none of those actually occur; only /, ^, - do (cm/s^2, mpg-uk).
  • Applied your suggested diff pattern across the board: the enum dicts now hold bare member names (kUint8T) rather than prefixed ones, with the prefix added in the template.
  • Converted the out.append(...) boilerplate to a Jinja2 template (templates/cpp_header.hpp.j2). jinja2 is now a dependency.
  • Left cpp-header as the CLI export name for now rather than renaming to hpp — that changes the public CLI surface, so didn't want to guess. Happy to make the change if you'd still like it.

@JMorceaux — also implemented the --signal filter we discussed back in June: repeatable, dotted-path + glob (Vehicle.ADAS.*). With --include-branches, only the ancestor branches of matched signals come along, not the whole tree, so it still serves the original memory-footprint goal.

Also updated docs/cpp-header.md — the worked example wasn't actually buildable (it referenced an undeclared Vehicle.Powertrain branch), so I regenerated it from the real tool output and it's now covered by the test suite.

Full suite (431 tests, 4 new for --signal) passes, ruff/ruff-format/mypy clean, and I compiled the generated header with both g++ and clang++ to confirm it's valid C++17.

@JMorceaux

Copy link
Copy Markdown

Hello @SoundMatt !

First of all, I wanted to apologize for the lack of response on my end this last few weeks ! I was away and coulnd't reach out.

Secondly, thank you again for all of your work. The --signal that you implemented feels like it's exactly what I needed. I'm gonna try the tool on my PC and will give you feedback ASAP !

Thank you again !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C++ exporter (and why not Rust later)

4 participants