Repository navigation
Events can travel over MQTT, and they can be written and read in the Protobuf event format.
ce::mqtt is a new binding (MQTT 5.0 structured and binary mode, MQTT 3.1.1 structured mode), and ce::protobuf_format is a new, hand-written event format that also works as structured mode in the HTTP, Kafka and NATS bindings.
Around the SDK, the coverage gate now counts branches, and the Go and Java SDKs check the Protobuf bytes and the MQTT messages the way they already check HTTP, Kafka and NATS.
This is a feature release (D-BUILD-5: before 1.0 a feature release bumps the minor version, as a break does).
The reason, as the owner ruled on 2026-10-27: it adds a new binding and a new format, each a capability of its own with its own requirements, entry points and interop suite, and D-BUILD-4 as it stood would have numbered it v0.6.2.
There are no breaking changes.
Everything below is an addition to ce::v4 or a change outside the library, and ce::v1 to ce::v4 keep every declaration and every documented behaviour they had at v0.6.1: python3 scripts/frozen_api_diff.py --base v0.6.1 finds no frozen declaration removed or changed (1,843 declarations compared).
The published to_message, from_message and from_payload return what they returned for every input, a message of the Protobuf content type included, and no namespace is added, so ce::v4 stays the current generation.
Added
-
An MQTT binding, structured mode (
binding/mqtt.hpp,ce::mqtt, CR-0008 slice 1).
An event travels as an MQTT 5.0 PUBLISH held in ace::message: the JSON event as the payload, the Content Type propertyapplication/cloudevents+jsonas the headercontent-type, and no User Property written or read.
ce::mqtt::to_message<Codec>(event, ce::content_mode::structured)andce::mqtt::from_message<Codec>(message, options)are the 5.0 pair, withdetect_content_mode;ce::mqtt::to_payload<Codec>(event)andfrom_payload<Codec>(payload_or_message, options)are the MQTT 3.1.1 pair, which has no properties, so the caller names the version by the function.
A message with no CloudEvents Content Type isnot_a_cloudevent, a malformed payload is the codec's error, a batch is refused withinvalid_argumentand so is a second header namedcontent-type, and names match byte for byte.
Binary mode is the next bullet.
The binding is header-only, names no MQTT client library and adds the targetce::binding_mqtt, the module exportce::v4::mqttand the fuzz targetfuzz_mqtt_message; the Go SDK reads and writes the same structured messages (sixteen recorded goldens, no broker, sdk-java has no MQTT binding).
It is an addition toce::v4:python3 scripts/frozen_api_diff.py --base v0.6.1finds no changed declaration.
Use:auto publish = ce::mqtt::to_message<codec>(event, ce::content_mode::structured); auto received = ce::mqtt::from_message<codec>(*publish);
-
MQTT binary mode, with sdk-go's
ce-prefix as an opt-in (ce::mqtt, CR-0008 slice 2).
ce::mqtt::to_message<Codec>(event, ce::content_mode::binary_mode)writes an MQTT 5.0 PUBLISH whose payload is the data, whose Content Type property isdatacontenttypeand whose User Properties are every other attribute and extension;from_message<Codec>reads it.
The names follow the binding text, unchanged (specversion,id), by default.
Name the policy for the form the Go SDK writes and requires:to_message<Codec, ce::mqtt::prefixed_names>(...)andfrom_message<Codec, ce::mqtt::prefixed_names>(...)usece-specversion,ce-id;unprefixed_namesis the default and the concept isname_policy.
A message with a CloudEvents Content Type is still structured; any other is binary when it carries thespecversionUser Property under the policy, andnot_a_cloudeventotherwise, where an MQTT binding limited to structured mode would answernot_a_cloudeventfor every such message.
A User Property that is not an attribute name under the policy is ignored; adatacontenttypeUser Property must equal the Content Type property; values are well-formed UTF-8 without NUL and at most 65,535 bytes.
The eight Go binary goldens recorded with slice 1 now decode underprefixed_names, arenot_a_cloudeventunder the default, and Go reads the binary messages this SDK writes under the policy.
fuzz_mqtt_messagecovers binary mode under both policies, and the module also exportsname_policy,unprefixed_namesandprefixed_names.
Use:auto for_go = ce::mqtt::to_message<codec, ce::mqtt::prefixed_names>(event, ce::content_mode::binary_mode); auto back = ce::mqtt::from_message<codec, ce::mqtt::prefixed_names>(*for_go);
-
The Protobuf event format:
ce::protobuf_format, targetce::format_protobuf.
Bytes to event and event to bytes for the CloudEvents v1.0.2 Protobuf format, one event or a batch, hand-written over the standard library andce::core: nolibprotobuf, noprotoc, no generated code and no codec.
encode,decode,encode_batchanddecode_batch;decode_as<Codec>, which parses a JSON payload on request and holds no document; anddecode_detailedandencode_proto_data, which carry the type URL of aproto_datapayload beside the event.
The field numbers are those ofcloudevents.proto, not the prose of the specification.
The writer follows the specification (text_datafor text, the attributes in ascending order of name) and the reader accepts all three data fields, so the bytes of the Go and Java SDKs decode here and the bytes written here decode there.
Decoding is strict, uses the existing error codes, and is bounded byprotobuf::decode_options(16 MiB per event, 1024 attributes per event, 100,000 events per batch, all adjustable and checked before an allocation); a libFuzzer target and the goldens of both SDKs and ofprotocback it.
A decodedtimeis in UTC with the fewest of 0, 3, 6 or 9 fraction digits, and a Timestamp outside the range ofce::timestampisinvalid_timestamp.
It is an addition toce::v4and changes no declaration of any generation (CR-0009, SWR-PROTO-0001 to SWR-PROTO-0018, SWR-PROTO-0024 to SWR-PROTO-0026); structured mode in the bindings is the next bullet.
Use:auto bytes = ce::protobuf_format::encode(event); auto back = ce::protobuf_format::decode(*bytes);
-
Structured Protobuf in the HTTP, Kafka and NATS bindings (CR-0009 slice 2).
ce::http::,ce::kafka::andce::nats::each getto_message_protobuf(event), which writes theapplication/cloudevents+protobufcontent type under the binding's own header spelling and the bytes ofprotobuf_format::encode, andfrom_message_protobuf(message, options), which returnsnot_a_cloudeventfor a message whose media type, compared ignoring case and parameters, is another one.
NATS also gets the payload form,to_payload_protobuf(event)andfrom_payload_protobuf(bytes, options), which always reads Protobuf.
The functions are new names and not templates, there is no content mode and no moving variant, Kafka sets no key, and the publishedto_message,from_messageandfrom_payloadreturn what they returned for every input, a message of the Protobuf content type included.
The Go and Java SDKs' HTTP and Kafka bindings write and read the same messages (thirteen events each, no server and no broker), and the fuzz target for the decoder drives the new functions.
It is an addition toce::v4(SWR-PROTO-0019 to SWR-PROTO-0023).
Use:auto record = ce::kafka::to_message_protobuf(event); auto back = ce::kafka::from_message_protobuf(*record);
Tooling
None of this changes what the library does.
- Branch coverage is gated (CR-0007,
SWR-SEC-0007, D-SEC-5): thecoverage floorjob fails a pull request whose branch coverage, with the compiler's exception edges excluded, falls belowCE_COVERAGE_BRANCH_FLOOR(86), next to the unchanged 90% line floor.
The floor is valid for one toolchain, so the job asserts gcovr 7.0 and gcov-13 and fails before measuring, naming both versions, if the image ships others.
scripts/check_coverage_gate.pyand its cases (coverage_gate_toolsunder ctest) fail on a dropped flag, a literal floor or a floating toolchain.
The floor is raised by a person, in the pull request that raises the measurement by a whole point, and never lowered to let a release go out.
Tests were added for the untested error paths of JSON decode, header values, typed payloads and the payload scanner (#110). - The versioning rule (D-BUILD-5,
docs/RELEASE.md): before 1.0 a break and a feature release each bump the minor version, any other release the patch; the owner decides that a release is a feature release and the release pull request states the reason. - The
delivered_incheck is part of the spec gate (SWR-BUILD-0016, D-SPEC-5): every pull request runsderive_delivered_in.py --checkwith the full history, so a requirement that a tag delivered cannot stayapproved.
The gate is red between a tag and the merge of the post-tag pull request, by design, so that pull request is merged first. - The vcpkg port is proved against a release (
vcpkg-releasedworkflow,scripts/check_vcpkg_release_guard.py,SWR-ADOPT-0009): vcpkg downloads the archive of the tag withoutCLOUDEVENTS_CPP_SOURCE_DIR, refuses a hash that does not match, and the job also checks that a wrong hash is refused.
It skips, with a notice, while the tag is missing or the hash is still the previous release's. - Interop covers MQTT and Protobuf (
interop/mqtt,interop/protobuf,SWR-MQTT-0014,SWR-PROTO-0016,SWR-PROTO-0023, D-INTEROP-6): theinteropjob verifies the Go SDK's MQTT messages and the Go and Java SDKs' Protobuf bytes and HTTP and Kafka messages in both directions, without a broker or a server. - Two fuzz targets,
fuzz_mqtt_messageandfuzz_protobuf_format(which also drives the binding entry points differentially against the decoder). - Spike reports for the MQTT binding and the Protobuf format (
docs/spikes, CR-0006), written before the two change requests were approved.
Measured
- Coverage, from the
coverage floorjob onmainat2118176, the merge of the last change before this release (run 40577415617, job 122448222373; GCC 13, gcovr 7.0, C++20, Debug): lines 98.0% (5300 of 5406), functions 92.7% (2325 of 2509), branches 89.1% (5839 of 6554) with exception edges excluded, against floors of 90% lines and 86% branches.
The v0.6.1 figures (lines 96.2%, functions 91.9%, branches 60.9%) counted exception edges as branches, so its branch figure is not comparable with this one; the measurement that set the floor was 88.1% (5196 of 5895) on the same metric (D-SEC-5). - CI toolchains are those of v0.6.1: GCC 13 at C++20 and C++23, Clang 16 with libstdc++ 13, Clang 17 with libc++ 17 at C++23, AppleClang at C++20 and C++23, MSVC 19 (Windows 2022), a C++26 static-reflection job on GCC 15 (GCC 14 if 15 is unavailable; allowed to fail), plus the forced polyfill, exceptions disabled, no default codec, RapidJSON alone, Boost.JSON alone, every codec on Linux and macOS at C++20 and C++23, ASan with UBSan and TSan on Clang 17, the coverage floor on GCC 13, the clang-tidy gate on Homebrew LLVM, install-and-consume, the examples, and interop with the Go and Java SDKs, now including MQTT messages and Protobuf bytes.
Thefrozen-api,packagingandmutationworkflows of v0.6.1 are unchanged;vcpkg-releasedis new. - Fuzzing: 30 targets (seven for each of the four generations and
fuzz_mqtt_messageandfuzz_protobuf_formatince::v4), run on every pull request at one minute each against the checked-in seed corpus, and nightly at fifteen minutes.
On the release commiteaffb88all 30 ran ten minutes each (seconds=600) with no crash and no finding (fuzzworkflow run 40795680906, started on demand): 30 of 30 jobs succeeded, eachFuzz for 600 secondsstep took 601 to 602 seconds, and the run uploaded no findings artifact.
fuzz_protobuf_formatran 629,057 inputs in 60 seconds with no finding on the pull request that added the binding entry points (#127).
Known limitations
- libc++ 17 and 18 implement
std::formatwithout defining__cpp_lib_format; the SDK carves them out by version (D-CI-1). - Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode, so the C++23 Clang job uses libc++ (D-CI-1).
- The module interface is usable, with constraints on what an importing translation unit may also include (D-MODULE-1), and it exports
ce::v4only: notce::v3, and notfrom_v3orto_v3.
It exports the MQTT and Protobuf names. - Given the same non-JSON payload, the Java SDK writes
data_base64where Go and this SDK write a JSON string. Both are legal (D-INTEROP-1). - HTTP binary mode percent-encodes header values as the binding requires; the Go and Java SDKs do not, so talking to them needs
ce::http::literal_values(D-HTTP-1). - The binary-mode parse is the default, and a forwarder turns it off. A binary-mode body under a JSON media type is parsed once on receive unless
json::decode_options{.parse_binary_body = false}is passed, and.retain_document_up_to = 0alone still parses to validate (SWR-BIND-0007, SWR-BIND-0008).
With the option set nothing validates the body: it is accepted as it came.
The option applies to binary mode only; structured and batched decode ignore it, and so doesce::v3. - Moving saves nothing for a JSON body or a small one. A JSON body takes the copying path because the parse reads the bytes, and a
std::stringcannot adopt astd::vector<std::byte>; a 256 B body saves memory and an allocation, not instructions.
http::from_batch_message_movingandnats::from_payload_movingexist for symmetry and save nothing, because a batch and a NATS payload are JSON. - Kafka header names are case-sensitive here, as in sdk-java and not as in sdk-go. A record whose headers read
CE_idorContent-Typeis read by the Go SDK and refused by this one and by the Java SDK (D-KAFKA-2, D-INTEROP-5). - Kafka records of this SDK carry an empty value, not a null one, for an event with no payload, so it cannot write a tombstone; the Go and Java SDKs can (D-INTEROP-5).
- MQTT: sdk-go prefixes User Properties with
ce-and the specification text does not. The default follows the text;ce::mqtt::prefixed_namesis the opt-in for a Go peer, and under either policy the other form isnot_a_cloudevent(D-MQTT-2). - MQTT has one interop peer and no broker. sdk-java has no MQTT binding, so binary mode has no second opinion, and the Go SDK's messages are checked as in-memory PUBLISH packets; no broker was involved.
A lonedatacontenttypeUser Property without a Content Type property is refused, andfrom_message_movingdoes not exist for MQTT (D-MQTT-2). - Protobuf: sdk-go never writes
text_dataand quotes JSON. sdk-go writes text asbinary_dataand reads atext_dataofapplication/jsonback as a quoted JSON string, so a JSON event written by this SDK arrives quoted in a Go consumer; this SDK writestext_dataas the specification says and reads all three data fields (D-INTEROP-6). - Protobuf timestamps lose the offset and the fraction digits, and have a range. A decoded
timeis the same instant in UTC with the fewest of 0, 3, 6 or 9 digits; aTimestampoutside about 1678 to 2262 isinvalid_timestamp(D-PROTO-1). - An empty
binary_datapayload without adatacontenttypereads back as no payload, because sdk-go writes that for an event with no data (D-PROTO-1). - The type URL of a
proto_datapayload that differs fromdataschemais returned bydecode_detailedonly;decodekeepsdataschema(D-PROTO-1).