Skip to content

Support repeated ROS intrinsics, cross-package enums, aliased enums, and self-named fields - #7

Merged
dallison merged 1 commit into
mainfrom
ros-repeated-time
Aug 27, 2026
Merged

Support repeated ROS intrinsics, cross-package enums, aliased enums, and self-named fields#7
dallison merged 1 commit into
mainfrom
ros-repeated-time

Conversation

@dallison

@dallison dallison commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Four cases the generator could not express. Each is independent; they are together because they share test scaffolding.

Repeated time and duration

The ROS frontend rejected any repeated ROS intrinsic:

ROS intrinsic field %s.%s must be singular and cannot be in a oneof

Because the frontend compiles the whole transitive proto set, a single time[] field blocked every schema that transitively depended on it, not just its own.

RosRepeatedMessageField (in phaser/runtime/ros.h) wraps MessageVectorField<Backend> or MessageArrayField<Backend, N> and converts per element, so a time[] field reads and writes as ::ros::Time rather than the google.protobuf.Timestamp backing it. Unlike the singular RosMessageField there is no cache: every access converts against the payload, so a mutation is visible to the backing message immediately and SyncToPayload has nothing to reconcile. A Proxy return from the mutable operator[] lets field[i] = value work without handing out a reference into a payload the element does not own.

Two rejections remain, and the error messages now say which applies:

  • Repeated Header. RosHeaderField hands out a view whose lifetime is tied to the field rather than a value, so it has no element type to repeat.
  • Any ROS intrinsic in a oneof.

Cross-package enums

Enums, along with their stringizer and parser, are emitted at namespace scope, so the short name only resolved inside the package that declared them. A field referencing an enum from another proto package generated an unqualified name that did not compile. EnumName now spells out the declaring package's namespace and the added namespace, mirroring what MessageName already did.

Aliased enums

option allow_alias lets several names share one number. The stringizer emitted one case per name, so an aliased enum produced a switch with duplicate case labels:

duplicate case value

Only the first name declared for a number gets a case now, which is also what protobuf's own _Name reports for that number.

Fields named after their own message

A .msg file may name a field after the message holding it, which C++ reads as a constructor declaration rather than a member:

member 'Polygon' has the same name as its class

Such a field now gets trailing underscores, as reserved words already did. protoc never meets this because it lowercases accessors, and roscpp never meets it because it names the struct Foo_<Allocator> and typedefs Foo to that. The two frontends differ: the ROS frontend exposes the member directly so only the member moves, while the protobuf frontend keeps the accessor at the field name and pushes the member one underscore past it. Prefixed accessors such as set_x and add_x are already legal whatever the field is called, so only the bare accessor is renamed.

CloneFrom's template parameter is also renamed T -> _phaser_Source, since T is a plausible field name and would win name lookup inside the template.

Tests

Six new test files plus extensions to existing ones, all under bazel test //... (22 targets, all passing):

  • ros_intrinsics_test.cc — repeated intrinsics: API, assignability, protobuf and ROS wire roundtrips, 8-bytes-per-element ROS layout, CloneFrom.
  • ros_wire_conversion_test.cc — repeated intrinsics added to the golden ROS byte stream and to the either-frontend parse test.
  • cross_package_enum_test.cc / cross_package_enum_ros_test.cc — singular, repeated, sized-array, nested, and union enum fields across packages, on both frontends.
  • aliased_enum_test.cc — aliases compare equal, stringizer reports the first name, parser accepts every alias.
  • self_named_field_test.cc — self-named fields on both frontends, the flattened nested class name, wire roundtrip, and CloneFrom.
  • invalid_ros_intrinsic_test.sh — now checks both remaining rejections separately (repeated Header, and an intrinsic in a oneof via the new InvalidRosIntrinsicOneof.proto).

RosMetadata.proto gains a repeated Timestamp field so the ROS definition and MD5 expectations cover time[].

…and self-named fields

Generating Phaser schemas for a large corpus of real ROS messages turned up
four cases the generator could not express.

Repeated time and duration. The ROS frontend rejected any repeated ROS
intrinsic, so a `time[]` or `duration[]` field blocked the whole transitive
proto set. RosRepeatedMessageField wraps the message vector or array and
converts per element, so such a field reads and writes as ::ros::Time rather
than the google.protobuf.Timestamp backing it. A repeated Header stays
rejected: RosHeaderField hands out a view whose lifetime is tied to the field
rather than a value, so it has no element type to repeat. An intrinsic in a
oneof stays rejected too.

Cross-package enums. Enums are emitted at namespace scope, so the short name
only resolved inside the declaring package. EnumName now qualifies with the
declaring package's namespace and the added namespace, as MessageName does.

Aliased enums. `option allow_alias` lets several names share one number, which
gave the stringizer a switch with duplicate case labels. Only the first name
for a number gets a case now, matching protobuf's own _Name.

Self-named fields. A .msg file may name a field after the message holding it,
which C++ reads as a constructor rather than a member. Such a field now gets
trailing underscores, as reserved words already did.
@dallison
dallison merged commit 72a3458 into main Aug 27, 2026
5 checks passed
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.

1 participant