From e7e15f70b40c4b45f7851f7e4045eb941ed0b9a1 Mon Sep 17 00:00:00 2001 From: Dave Allison Date: Fri, 9 Oct 2026 10:22:30 -0700 Subject: [PATCH 1/2] Handle full fixed buffers and build without exceptions (2.1.3) Messages in caller-provided buffers now stay inside the buffer when it runs out of space: - TryCreateMutable returns InvalidArgument for a buffer smaller than its header and ResourceExhausted when the message does not fit. CreateMutable aborts in those cases. - Setters and repeated fields keep their previous contents. mutable_, add_ and array element accessors return a detached handle whose writes go to per-thread scratch storage. - AllocationFailed() reports the failure. Serialize then fails, Deserialize returns ResourceExhausted, and CreateReadonly treats the buffer as empty. - Placeholder string handles ignore assignment instead of dereferencing null. The runtime and generated code build with -fno-exceptions -fno-rtti, where misuse that would throw aborts instead. fixed_buffer_test builds and parses messages in every buffer size up to 4 KB under those flags and checks that nothing is written past the end of the buffer. Requires cpp_toolbelt 2.1.6, pinned with a git_override until it is in the Bazel Central Registry. --- MODULE.bazel | 12 +- README.md | 32 +++ phaser/BUILD.bazel | 16 ++ phaser/compiler/message_gen.cc | 50 +++- phaser/fixed_buffer_test.cc | 388 ++++++++++++++++++++++++++++++ phaser/runtime/any.h | 20 +- phaser/runtime/arrays.h | 296 +++++++++++++++-------- phaser/runtime/fields.h | 30 ++- phaser/runtime/message.h | 85 ++++++- phaser/runtime/phaser_bank.h | 10 +- phaser/runtime/union.h | 29 ++- phaser/runtime/vectors.h | 179 +++++++++++--- phaser/testdata/BUILD | 37 +++ phaser/testdata/FixedBuffer.proto | 29 +++ 14 files changed, 1040 insertions(+), 173 deletions(-) create mode 100644 phaser/fixed_buffer_test.cc create mode 100644 phaser/testdata/FixedBuffer.proto diff --git a/MODULE.bazel b/MODULE.bazel index 49a7a93..5766b01 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( name = "phaser", - version = "2.1.2" + version = "2.1.3" ) bazel_dep(name = "bazel_skylib", version = "1.9.0") @@ -17,7 +17,15 @@ bazel_dep(name = "rules_shell", version = "0.8.0") bazel_dep(name = "rules_pkg", version = "1.0.1") bazel_dep(name = "zlib", version = "1.3.1.bcr.5") -bazel_dep(name = "cpp_toolbelt", version = "2.1.5") +bazel_dep(name = "cpp_toolbelt", version = "2.1.6") + +# Remove once cpp_toolbelt 2.1.6 is in the Bazel Central Registry. +git_override( + module_name = "cpp_toolbelt", + commit = "86b45cdd3da46a97a4476be2f2a61909677284a2", + remote = "https://github.com/dallison/cpp_toolbelt.git", +) + bazel_dep(name = "coroutines", version = "3.3.2") # protobuf pulls an older rules_go via gazelle; Bazel 9 needs a current rules_go. diff --git a/README.md b/README.md index 6727a96..0132f51 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,38 @@ Dynamic messages, reflection/debug output, unknown `Any` error handling, and explicit owning conversions such as `RosHeaderView::ToOwned()` are outside this guarantee. +#### When a fixed buffer runs out of space + +A caller-provided buffer cannot grow, so every write checks for space and stays +inside the buffer. `TryCreateMutable` reports a buffer that is too small for the +message: + +```c++ +absl::StatusOr msg = + foo::bar::phaser::TestMessage::TryCreateMutable(buffer, size); +if (!msg.ok()) { + // InvalidArgument for a buffer smaller than its header, ResourceExhausted + // when the message itself does not fit. +} +``` + +`CreateMutable` aborts in the same cases. Once the message exists, a write that +needs more space than is left does nothing and marks the buffer: + +- Setters keep the previous value. +- Repeated fields keep their previous elements. +- `mutable_` accessors, `add_` accessors and array elements return a detached + handle whose writes go to per-thread scratch storage. +- `msg.AllocationFailed()` is true from then on. +- `Serialize` and `SerializeToArray` fail, and `Deserialize` returns + `ResourceExhausted`. +- `CreateReadonly` treats the marked buffer as empty, so a receiver never sees a + partly written message. + +Phaser and its generated code build with `-fno-exceptions -fno-rtti`. Without +exceptions, misuse that would otherwise throw, such as `at()` out of range or +mutating a readonly message, aborts. + ### 3. Zero-copy field access Beyond the standard protobuf accessors, Phaser adds helpers that hand you the final storage diff --git a/phaser/BUILD.bazel b/phaser/BUILD.bazel index b9e0771..d3de385 100644 --- a/phaser/BUILD.bazel +++ b/phaser/BUILD.bazel @@ -194,6 +194,22 @@ cc_test( ], ) +cc_test( + name = "fixed_buffer_test", + srcs = ["fixed_buffer_test.cc"], + copts = PHASER_COPTS + [ + "-fno-exceptions", + "-fno-rtti", + ], + data = ["valgrind.supp"], + deps = [ + "//phaser/runtime:phaser_runtime", + "//phaser/testdata:fixed_buffer_phaser", + "//phaser/testdata:fixed_buffer_ros_phaser", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "ros_wire_conversion_test", srcs = ["ros_wire_conversion_test.cc"], diff --git a/phaser/compiler/message_gen.cc b/phaser/compiler/message_gen.cc index 3885a7c..949c332 100644 --- a/phaser/compiler/message_gen.cc +++ b/phaser/compiler/message_gen.cc @@ -1726,6 +1726,9 @@ void MessageGenerator::GenerateFieldInitializers(std::ostream& os, void MessageGenerator::GenerateCreators(std::ostream& os, bool decl) { if (decl) { + os << " static absl::StatusOr<" << MessageName(message_) + << "> TryCreateMutable(void *addr, size_t size, ::phaser::Tuning " + "tuning = ::phaser::Tuning::kPerformance);\n"; os << " static " << MessageName(message_) << " CreateMutable(void *addr, size_t size, ::phaser::Tuning tuning = " "::phaser::Tuning::kPerformance);\n"; @@ -1752,28 +1755,52 @@ void MessageGenerator::GenerateCreators(std::ostream& os, bool decl) { "::phaser::Tuning::kPerformance);\n"; return; } - os << "// Create a mutable message in the given memory.\n"; - os << MessageName(message_) << " " << MessageName(message_) - << "::CreateMutable(void *addr, size_t size, ::phaser::Tuning tuning) {\n" + os << "// Create a mutable message in the given memory. Fails if the memory " + "is too\n// small for the message's fixed parts.\n"; + os << "absl::StatusOr<" << MessageName(message_) << "> " + << MessageName(message_) + << "::TryCreateMutable(void *addr, size_t size, ::phaser::Tuning tuning) " + "{\n" + " if (addr == nullptr || size < sizeof(::toolbelt::PayloadBuffer) + " + "sizeof(::toolbelt::FreeBlockHeader)) {\n" + " return absl::InvalidArgumentError(\"phaser: buffer is too " + "small\");\n" + " }\n" " ::toolbelt::PayloadBuffer *pb = new (addr) " "::toolbelt::PayloadBuffer(static_cast(size), tuning == " "::phaser::Tuning::kPerformance);\n" - " ::toolbelt::PayloadBuffer::AllocateMainMessage(&pb, " + " if (::toolbelt::PayloadBuffer::AllocateMainMessage(&pb, " << MessageName(message_) - << "::BinarySize());\n" - " ::phaser::InitializeRuntimeControl(&pb, " + << "::BinarySize()) == nullptr ||\n" + " ::phaser::InitializeRuntimeControl(&pb, " << MessageName(message_) - << "::MetadataTypeCount());\n" + << "::MetadataTypeCount()) == nullptr) {\n" + " return ::phaser::internal::AllocationFailedError();\n" + " }\n" " ::phaser::MessageRuntime runtime(pb, true);\n" " auto msg = " << MessageName(message_) << "(BorrowRuntime(runtime), pb->message);\n" - " msg.InstallMetadata<" + " if (!msg.InstallMetadata<" << MessageName(message_) - << ">();\n" + << ">()) {\n" + " return ::phaser::internal::AllocationFailedError();\n" + " }\n" " return msg;\n" "}\n" "\n"; + os << "// Create a mutable message in the given memory. Aborts if the " + "memory is too\n// small for the message's fixed parts.\n"; + os << MessageName(message_) << " " << MessageName(message_) + << "::CreateMutable(void *addr, size_t size, ::phaser::Tuning tuning) {\n" + " auto message = TryCreateMutable(addr, size, tuning);\n" + " if (!message.ok()) {\n" + " ::phaser::internal::Fatal(\"buffer is too small for " + "CreateMutable\");\n" + " }\n" + " return std::move(*message);\n" + "}\n" + "\n"; os << "// Create a readonly message that already exists at the given " "address with a size.\n"; @@ -4011,6 +4038,8 @@ void MessageGenerator::GenerateSerializer(std::ostream& os, bool decl) { } os << "absl::Status " << MessageName(message_) << "::Serialize(::phaser::ProtoBuffer &_phaser_buffer) const {\n"; + os << " if (AllocationFailed()) return " + "::phaser::internal::AllocationFailedError();\n"; for (auto& field : fields_) { if (field->field->is_repeated()) { os << " if (absl::Status _phaser_status = " << field->member_name @@ -4113,6 +4142,8 @@ void MessageGenerator::GenerateDeserializer(std::ostream& os, bool decl) { } } } + os << " if (AllocationFailed()) return " + "::phaser::internal::AllocationFailedError();\n"; os << " return absl::OkStatus();\n"; os << "}\n\n"; } @@ -4535,6 +4566,7 @@ void MessageGenerator::GeneratePhaserBank(std::ostream& os) { << "Allocate(std::shared_ptr<::phaser::MessageRuntime> runtime) {\n"; os << " void *addr = toolbelt::PayloadBuffer::Allocate(&runtime->pb, " << MessageName(message_) << "::BinarySize());\n"; + os << " if (addr == nullptr) return {nullptr, 0};\n"; os << " toolbelt::BufferOffset offset = runtime->pb->ToOffset(addr);\n"; os << " auto msg = new " << MessageName(message_) << "(runtime, offset);\n"; os << " msg->InstallMetadata<" << MessageName(message_) << ">();\n"; diff --git a/phaser/fixed_buffer_test.cc b/phaser/fixed_buffer_test.cc new file mode 100644 index 0000000..47a04c1 --- /dev/null +++ b/phaser/fixed_buffer_test.cc @@ -0,0 +1,388 @@ +// Fills fixed size buffers until they run out of space. Every operation must +// stay inside the buffer, report the failure and leave earlier contents intact. +// This test and the generated code it uses are built without exceptions or +// RTTI. + +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "gtest/gtest.h" +#include "phaser/testdata/FixedBuffer.phaser.h" +#include "phaser/testdata/fixed_buffer_ros_phaser/phaser/testdata/FixedBuffer.phaser.h" + +namespace { + +using Message = foo::bar::phaser::FixedBufferMessage; +using RosMessage = foo::bar::phaser_ros::FixedBufferMessage; + +constexpr std::array kTunings = { + phaser::Tuning::kPerformance, phaser::Tuning::kSize}; + +constexpr size_t kLargeBuffer = 65536; + +// Bytes after the end of the buffer under test that must never change. +constexpr size_t kCanarySize = 64; +constexpr std::byte kCanary{0xa5}; + +void SetCanary(std::vector& storage, size_t size) { + std::fill(storage.begin() + static_cast(size), + storage.begin() + static_cast(size + kCanarySize), + kCanary); +} + +bool CanaryIntact(const std::vector& storage, size_t size) { + for (size_t i = size; i < size + kCanarySize; i++) { + if (storage[i] != kCanary) { + return false; + } + } + return true; +} + +template +std::string Serialized(const T& message) { + std::string out; + out.resize(message.SerializedSize()); + if (!out.empty() && !message.SerializeToArray(out.data(), out.size())) { + return ""; + } + return out; +} + +void Populate(Message& msg) { + msg.set_x(42); + msg.set_name("a name that needs some space in the buffer"); + msg.mutable_inner()->set_id(7); + msg.mutable_inner()->set_label("inner label"); + for (int i = 0; i < 20; i++) { + msg.add_values(i * 3); + } + for (int i = 0; i < 6; i++) { + msg.add_names("name number " + std::to_string(i)); + } + for (int i = 0; i < 4; i++) { + auto inner = msg.add_inners(); + inner.set_id(100 + i); + inner.set_label("repeated inner " + std::to_string(i)); + } + msg.mutable_choice_inner()->set_id(9); + msg.mutable_choice_inner()->set_label("choice"); + msg.set_payload(std::string(64, '\x5a')); +} + +void Populate(RosMessage& msg) { + msg.x = 42; + msg.name = "a name that needs some space in the buffer"; + msg.inner->id = 7; + msg.inner->label = "inner label"; + for (int i = 0; i < 20; i++) { + msg.values.push_back(i * 3); + } + for (int i = 0; i < 6; i++) { + msg.names.push_back("name number " + std::to_string(i)); + } + for (int i = 0; i < 4; i++) { + auto inner = msg.inners.Add(); + inner->id = 100 + i; + inner->label = "repeated inner " + std::to_string(i); + } + for (size_t i = 0; i < 4; i++) { + msg.fixed_values[i] = static_cast(i + 1); + } + int32_t extra = 10; + for (int32_t& v : msg.fixed_values) { + v += extra++; + } + msg.fixed_names[0] = "first fixed name"; + msg.fixed_names[1] = "second fixed name"; + msg.fixed_inners[0]->id = 21; + msg.fixed_inners[1]->label = "fixed inner"; + msg.choice.emplace().id = 9; + msg.payload = std::string(64, '\x5a'); +} + +template +std::string ReferenceWire() { + static std::vector storage(kLargeBuffer); + T msg = T::CreateMutable(storage.data(), storage.size()); + Populate(msg); + EXPECT_FALSE(msg.AllocationFailed()); + return Serialized(msg); +} + +TEST(FixedBufferTest, TryCreateMutableChecksTheBufferSize) { + alignas(8) std::array tiny{}; + absl::StatusOr too_small = + Message::TryCreateMutable(tiny.data(), tiny.size()); + EXPECT_EQ(too_small.status().code(), absl::StatusCode::kInvalidArgument); + + alignas(8) std::array + no_room_for_message{}; + absl::StatusOr no_room = Message::TryCreateMutable( + no_room_for_message.data(), no_room_for_message.size()); + EXPECT_EQ(no_room.status().code(), absl::StatusCode::kResourceExhausted); + + alignas(8) std::array enough{}; + absl::StatusOr ok = Message::TryCreateMutable(enough.data(), + enough.size()); + ASSERT_TRUE(ok.ok()) << ok.status(); + EXPECT_FALSE(ok->AllocationFailed()); + ok->set_x(1); + EXPECT_EQ(ok->x(), 1); +} + +TEST(FixedBufferTest, FailedStringSetKeepsTheOldValue) { + for (phaser::Tuning tuning : kTunings) { + alignas(8) std::array storage{}; + Message msg = Message::CreateMutable(storage.data(), storage.size(), tuning); + msg.set_x(5); + msg.set_name("hello"); + ASSERT_FALSE(msg.AllocationFailed()); + + msg.set_name(std::string(4096, 'x')); + EXPECT_TRUE(msg.AllocationFailed()); + EXPECT_EQ(msg.name(), "hello"); + EXPECT_EQ(msg.x(), 5); + + std::array wire{}; + phaser::ProtoBuffer out(wire.data(), wire.size()); + EXPECT_EQ(msg.Serialize(out).code(), absl::StatusCode::kResourceExhausted); + EXPECT_FALSE(msg.SerializeToArray(wire.data(), wire.size())); + + // A receiver sees the incomplete buffer as invalid. + Message received = Message::CreateReadonly(msg.Data(), msg.Size()); + EXPECT_EQ(received.x(), 0); + EXPECT_EQ(received.name(), ""); + } +} + +TEST(FixedBufferTest, RepeatedFieldsStopGrowingAndKeepTheirValues) { + for (phaser::Tuning tuning : kTunings) { + alignas(8) std::array storage{}; + Message msg = Message::CreateMutable(storage.data(), storage.size(), tuning); + + size_t values = 0; + while (!msg.AllocationFailed()) { + msg.add_values(static_cast(values)); + if (msg.values_size() == values) { + break; + } + values++; + ASSERT_LT(values, 10000u); + } + ASSERT_TRUE(msg.AllocationFailed()); + ASSERT_EQ(msg.values_size(), values); + for (size_t i = 0; i < values; i++) { + EXPECT_EQ(msg.values(i), static_cast(i)); + } + + size_t names = 0; + for (;;) { + msg.add_names("name " + std::to_string(names)); + if (msg.names_size() == names) { + break; + } + names++; + ASSERT_LT(names, 10000u); + } + for (size_t i = 0; i < names; i++) { + EXPECT_EQ(msg.names(i), "name " + std::to_string(i)); + } + + size_t inners = 0; + for (;;) { + auto inner = msg.add_inners(); + inner.set_id(static_cast(inners)); + if (msg.inners_size() == inners) { + break; + } + inners++; + ASSERT_LT(inners, 10000u); + } + for (size_t i = 0; i < inners; i++) { + EXPECT_EQ(msg.inners(i).id(), static_cast(i)); + } + + const std::array before = storage; + auto detached = msg.add_inners(); + detached.set_id(3); + detached.set_label("goes nowhere"); + EXPECT_EQ(storage, before); + EXPECT_EQ(msg.inners_size(), inners); + EXPECT_EQ(msg.names_size(), names); + EXPECT_EQ(msg.values_size(), values); + } +} + +TEST(FixedBufferTest, SubMessageWritesAfterFailureStayInBounds) { + for (phaser::Tuning tuning : kTunings) { + alignas(8) std::array storage{}; + Message msg = Message::CreateMutable(storage.data(), storage.size(), tuning); + msg.set_x(11); + while (!msg.AllocationFailed()) { + msg.add_names("filler string"); + } + const std::array before = storage; + + msg.mutable_inner()->set_id(7); + msg.mutable_inner()->set_label("label"); + msg.mutable_choice_inner()->set_id(8); + auto element = msg.add_inners(); + element.set_id(9); + + EXPECT_FALSE(msg.has_inner()); + EXPECT_EQ(msg.inner().id(), 0); + EXPECT_EQ(msg.x(), 11); + // Only the choice discriminator may change. + size_t changed = 0; + for (size_t i = 0; i < storage.size(); i++) { + changed += storage[i] != before[i] ? 1 : 0; + } + EXPECT_LE(changed, sizeof(int32_t)); + } +} + +TEST(FixedBufferTest, ParseIntoEveryBufferSizeSucceedsOrReportsExhaustion) { + const std::string wire = ReferenceWire(); + ASSERT_NE(wire, ""); + for (phaser::Tuning tuning : kTunings) { + std::vector storage(4096 + kCanarySize); + size_t successes = 0; + for (size_t size = sizeof(toolbelt::PayloadBuffer) + 16; + size + kCanarySize <= storage.size(); size += 4) { + SetCanary(storage, size); + absl::StatusOr msg = + Message::TryCreateMutable(storage.data(), size, tuning); + if (!msg.ok()) { + continue; + } + phaser::ProtoBuffer in(wire.data(), wire.size()); + absl::Status status = msg->Deserialize(in); + if (status.ok()) { + EXPECT_FALSE(msg->AllocationFailed()); + EXPECT_EQ(Serialized(*msg), wire) << "size " << size; + successes++; + } else { + EXPECT_EQ(status.code(), absl::StatusCode::kResourceExhausted) + << "size " << size << ": " << status; + EXPECT_TRUE(msg->AllocationFailed()) << "size " << size; + } + EXPECT_TRUE(CanaryIntact(storage, size)) << "size " << size; + } + EXPECT_GT(successes, 0u); + } +} + +TEST(FixedBufferTest, BuildInEveryBufferSizeSucceedsOrReportsExhaustion) { + const std::string wire = ReferenceWire(); + for (phaser::Tuning tuning : kTunings) { + std::vector storage(4096 + kCanarySize); + size_t successes = 0; + for (size_t size = sizeof(toolbelt::PayloadBuffer) + 16; + size + kCanarySize <= storage.size(); size += 4) { + SetCanary(storage, size); + absl::StatusOr msg = + Message::TryCreateMutable(storage.data(), size, tuning); + if (!msg.ok()) { + continue; + } + Populate(*msg); + if (msg->AllocationFailed()) { + std::array out{}; + EXPECT_FALSE(msg->SerializeToArray(out.data(), out.size())); + } else { + EXPECT_EQ(Serialized(*msg), wire) << "size " << size; + successes++; + } + EXPECT_TRUE(CanaryIntact(storage, size)) << "size " << size; + } + EXPECT_GT(successes, 0u); + } +} + +TEST(FixedBufferTest, RosArraysInEveryBufferSize) { + const std::string wire = ReferenceWire(); + ASSERT_NE(wire, ""); + for (phaser::Tuning tuning : kTunings) { + std::vector storage(4096 + kCanarySize); + size_t built = 0; + size_t parsed = 0; + for (size_t size = sizeof(toolbelt::PayloadBuffer) + 16; + size + kCanarySize <= storage.size(); size += 4) { + SetCanary(storage, size); + absl::StatusOr msg = + RosMessage::TryCreateMutable(storage.data(), size, tuning); + if (!msg.ok()) { + continue; + } + Populate(*msg); + if (!msg->AllocationFailed()) { + EXPECT_EQ(Serialized(*msg), wire) << "size " << size; + built++; + } + + absl::StatusOr target = + RosMessage::TryCreateMutable(storage.data(), size, tuning); + ASSERT_TRUE(target.ok()); + phaser::ProtoBuffer in(wire.data(), wire.size()); + absl::Status status = target->Deserialize(in); + if (status.ok()) { + EXPECT_EQ(Serialized(*target), wire) << "size " << size; + parsed++; + } else { + EXPECT_EQ(status.code(), absl::StatusCode::kResourceExhausted) + << "size " << size << ": " << status; + } + EXPECT_TRUE(CanaryIntact(storage, size)) << "size " << size; + } + EXPECT_GT(built, 0u); + EXPECT_GT(parsed, 0u); + } +} + +TEST(FixedBufferTest, RosArrayAccessInEveryBufferSize) { + for (phaser::Tuning tuning : kTunings) { + std::vector storage(4096 + kCanarySize); + size_t complete = 0; + for (size_t size = sizeof(toolbelt::PayloadBuffer) + 16; + size + kCanarySize <= storage.size(); size += 4) { + SetCanary(storage, size); + absl::StatusOr msg = + RosMessage::TryCreateMutable(storage.data(), size, tuning); + if (!msg.ok()) { + continue; + } + msg->x = 3; + msg->fixed_values[3] = 99; + int32_t next = 1; + for (int32_t& v : msg->fixed_values) { + v = next++; + } + msg->fixed_names[1] = "a fixed name"; + msg->fixed_inners[1]->id = 5; + msg->fixed_inners[0]->label = "a fixed inner"; + msg->inners.Add()->id = 6; + EXPECT_TRUE(CanaryIntact(storage, size)) << "size " << size; + EXPECT_EQ(msg->x.Get(), 3); + if (msg->AllocationFailed()) { + continue; + } + complete++; + ASSERT_NE(msg->fixed_values.data(), nullptr); + EXPECT_EQ(msg->fixed_values[0], 1); + EXPECT_EQ(msg->fixed_values[3], 4); + EXPECT_EQ(std::string_view(msg->fixed_names[1]), "a fixed name"); + EXPECT_EQ(msg->fixed_inners[1]->id.Get(), 5); + EXPECT_EQ(std::string_view(msg->fixed_inners[0]->label), "a fixed inner"); + EXPECT_EQ(msg->inners[0]->id.Get(), 6); + } + EXPECT_GT(complete, 0u); + } +} + +} // namespace diff --git a/phaser/runtime/any.h b/phaser/runtime/any.h index 0e57c11..cedf474 100644 --- a/phaser/runtime/any.h +++ b/phaser/runtime/any.h @@ -258,10 +258,18 @@ class AnyMessage : public Message { constexpr std::string_view full_name = T::FullName(); absl::Span url = type_url_.Allocate(prefix.size() + full_name.size(), false); - memcpy(url.data(), prefix.data(), prefix.size()); - memcpy(url.data() + prefix.size(), full_name.data(), full_name.size()); size_t size = T::BinarySize(); - absl::Span memory = value_.Allocate(size, true); + absl::Span memory; + if (url.data() != nullptr) { + memcpy(url.data(), prefix.data(), prefix.size()); + memcpy(url.data() + prefix.size(), full_name.data(), full_name.size()); + memory = value_.Allocate(size, true); + } + if (memory.data() == nullptr) { + T detached(InternalDefault{}); + internal::BindToAllocationFailureSink(detached); + return detached; + } auto msg = T(runtime, runtime->ToOffset(memory.data())); msg.template InstallMetadata(); return msg; @@ -356,6 +364,9 @@ class AnyMessage : public Message { return binary_size.status(); } absl::Span memory = value_.Allocate(*binary_size, true); + if (memory.data() == nullptr) { + return internal::AllocationFailedError(); + } return PhaserBankAllocateAtOffset(type, runtime, runtime->ToOffset(memory.data())); } @@ -370,6 +381,9 @@ class AnyMessage : public Message { return binary_size.status(); } absl::Span memory = value_.Allocate(*binary_size, true); + if (memory.data() == nullptr) { + return internal::AllocationFailedError(); + } ProtoBuffer sub(wire); return PhaserBankDeserializeAtOffset( type, runtime, runtime->ToOffset(memory.data()), sub); diff --git a/phaser/runtime/arrays.h b/phaser/runtime/arrays.h index 10483b1..277aa72 100644 --- a/phaser/runtime/arrays.h +++ b/phaser/runtime/arrays.h @@ -104,11 +104,15 @@ struct FixedArrayConstIterator { using const_reverse_iterator = ctype; \ \ iterator begin() { \ - EnsureExtent(); \ + if (!EnsureExtent()) { \ + return iterator(this, 0); \ + } \ return iterator(this, BaseOffset()); \ } \ iterator end() { \ - EnsureExtent(); \ + if (!EnsureExtent()) { \ + return iterator(this, 0); \ + } \ return iterator( \ this, BaseOffset() + static_cast<::toolbelt::BufferOffset>(extent * \ sizeof( \ @@ -123,9 +127,15 @@ struct FixedArrayConstIterator { const_iterator cbegin() const { return begin(); } \ const_iterator cend() const { return end(); } \ reverse_iterator rbegin() { \ + if (!EnsureExtent()) { \ + return reverse_iterator(this, 0, true); \ + } \ return reverse_iterator(this, BaseOffset(), true); \ } \ reverse_iterator rend() { \ + if (!EnsureExtent()) { \ + return reverse_iterator(this, 0, true); \ + } \ return reverse_iterator( \ this, BaseOffset() + static_cast<::toolbelt::BufferOffset>(extent * \ sizeof( \ @@ -158,9 +168,15 @@ class PrimitiveArrayField : public Field { PrimitiveArrayField(const PrimitiveArrayField&) = default; PrimitiveArrayField(PrimitiveArrayField&&) = default; + // Returns a detached value if the buffer has no space for the array. T& operator[](size_t index) { - EnsureExtent(); - return data()[index]; + T* base = data(); + if (base == nullptr) { + static thread_local T detached; + detached = T{}; + return detached; + } + return base[index]; } T operator[](size_t index) const { return GetAt(index); } @@ -216,7 +232,9 @@ class PrimitiveArrayField : public Field { if (parsed_count_ > N) { return absl::InvalidArgumentError("array_size overflow"); } - EnsureExtent(); + if (!EnsureExtent()) { + return internal::AllocationFailedError(); + } parsed_count_ = 0; return absl::OkStatus(); } @@ -225,13 +243,18 @@ class PrimitiveArrayField : public Field { ::toolbelt::PayloadBuffer::VectorClear(GetBufferAddr(), Header(relative_binary_offset_)); parsed_count_ = 0; - EnsureStorage(N); + this->ResetFieldCache(); + if (!EnsureStorage(N)) { + return; + } T* base = GetRuntime()->template ToAddress(BaseOffset()); + if (base == nullptr) { + return; + } for (size_t i = 0; i < N; i++) { base[i] = T{}; } SetActiveCount(N); - this->ResetFieldCache(); } void clear() { Clear(); } @@ -255,8 +278,11 @@ class PrimitiveArrayField : public Field { size_t max_size() const { return N; } bool empty() const { return N == 0; } + // Returns nullptr if the buffer has no space for the array. T* data() { - EnsureExtent(); + if (!EnsureExtent()) { + return nullptr; + } return GetRuntime()->template ToAddress(BaseOffset()); } const T* data() const { return StoragePointer(); } @@ -379,8 +405,10 @@ class PrimitiveArrayField : public Field { return absl::InvalidArgumentError("array_size overflow"); } if constexpr (FixedSize) { - EnsureStorage(N); - T* base = GetRuntime()->template ToAddress(BaseOffset()); + T* base = StorageFor(N); + if (base == nullptr) { + return internal::AllocationFailedError(); + } memcpy(base + parsed_count_, payload->data(), payload->size()); parsed_count_ += count; SetActiveCount(parsed_count_); @@ -394,38 +422,26 @@ class PrimitiveArrayField : public Field { if (!v.ok()) { return v.status(); } - EnsureStorage(parsed_count_ + 1); - GetRuntime()->template ToAddress(BaseOffset())[parsed_count_] = - *v; - parsed_count_++; - SetActiveCount(parsed_count_); + if (absl::Status status = Append(*v); !status.ok()) { + return status; + } } } } else { + if (parsed_count_ >= N) { + return absl::InvalidArgumentError("array_size overflow"); + } + absl::StatusOr v; if constexpr (FixedSize) { - if (parsed_count_ >= N) { - return absl::InvalidArgumentError("array_size overflow"); - } - absl::StatusOr v = buffer.DeserializeFixed(); - if (!v.ok()) { - return v.status(); - } - EnsureStorage(parsed_count_ + 1); - GetRuntime()->template ToAddress(BaseOffset())[parsed_count_] = *v; - parsed_count_++; - SetActiveCount(parsed_count_); + v = buffer.DeserializeFixed(); } else { - if (parsed_count_ >= N) { - return absl::InvalidArgumentError("array_size overflow"); - } - absl::StatusOr v = buffer.DeserializeVarint(); - if (!v.ok()) { - return v.status(); - } - EnsureStorage(parsed_count_ + 1); - GetRuntime()->template ToAddress(BaseOffset())[parsed_count_] = *v; - parsed_count_++; - SetActiveCount(parsed_count_); + v = buffer.DeserializeVarint(); + } + if (!v.ok()) { + return v.status(); + } + if (absl::Status status = Append(*v); !status.ok()) { + return status; } } return absl::OkStatus(); @@ -462,32 +478,53 @@ class PrimitiveArrayField : public Field { hdr->num_elements = static_cast(count); } - void EnsureStorage(size_t count) { - ::toolbelt::PayloadBuffer::VectorReserve(GetBufferAddr(), - Header(relative_binary_offset_), - count); - ::toolbelt::PayloadBuffer::VectorResize(GetBufferAddr(), - Header(relative_binary_offset_), - count); + // Returns false if the buffer has no space for 'count' elements. + bool EnsureStorage(size_t count) { + return ::toolbelt::PayloadBuffer::VectorReserve( + GetBufferAddr(), Header(relative_binary_offset_), count) && + ::toolbelt::PayloadBuffer::VectorResize( + GetBufferAddr(), Header(relative_binary_offset_), count); } - void EnsureExtent() { + // Returns the storage for at least 'count' elements, or nullptr if the + // buffer has no space for them. + T* StorageFor(size_t count) { + if (!EnsureStorage(count)) { + return nullptr; + } + return GetRuntime()->template ToAddress(BaseOffset()); + } + + absl::Status Append(T v) { + T* base = StorageFor(parsed_count_ + 1); + if (base == nullptr) { + return internal::AllocationFailedError(); + } + base[parsed_count_] = v; + parsed_count_++; + SetActiveCount(parsed_count_); + return absl::OkStatus(); + } + + // Returns false if the array is mutable and the buffer has no space for + // all N elements. + bool EnsureExtent() { if (!IsMutable()) { - return; + return true; } const size_t current = NumElements(); if (current >= N) { - return; + return true; } - EnsureStorage(N); - T* base = GetRuntime()->template ToAddress(BaseOffset()); + T* base = StorageFor(N); if (base == nullptr) { - return; + return false; } for (size_t i = current; i < N; i++) { base[i] = T{}; } SetActiveCount(N); + return true; } T GetAt(size_t index) const { @@ -557,9 +594,15 @@ class EnumArrayField : public Field { using T = typename std::underlying_type::type; + // Returns a detached value if the buffer has no space for the array. Enum& operator[](size_t index) { - EnsureExtent(); - return *reinterpret_cast(&data()[index]); + Enum* base = data(); + if (base == nullptr) { + static thread_local Enum detached; + detached = static_cast(T{}); + return detached; + } + return base[index]; } const Enum operator[](size_t index) const { return GetAt(index); } @@ -590,8 +633,14 @@ class EnumArrayField : public Field { } void Set(size_t index, Enum v) { - EnsureExtent(); - GetRuntime()->template ToAddress(BaseOffset())[index] = static_cast(v); + if (index >= N) { + return; + } + Enum* base = data(); + if (base == nullptr) { + return; + } + base[index] = v; } std::array Get() const { @@ -618,7 +667,9 @@ class EnumArrayField : public Field { if (parsed_count_ > N) { return absl::InvalidArgumentError("array_size overflow"); } - EnsureExtent(); + if (!EnsureExtent()) { + return internal::AllocationFailedError(); + } parsed_count_ = 0; return absl::OkStatus(); } @@ -627,13 +678,15 @@ class EnumArrayField : public Field { ::toolbelt::PayloadBuffer::VectorClear(GetBufferAddr(), Header(relative_binary_offset_)); parsed_count_ = 0; - EnsureStorage(N); - T* base = GetRuntime()->template ToAddress(BaseOffset()); + this->ResetFieldCache(); + T* base = StorageFor(N); + if (base == nullptr) { + return; + } for (size_t i = 0; i < N; i++) { base[i] = T{}; } SetActiveCount(N); - this->ResetFieldCache(); } void clear() { Clear(); } @@ -657,8 +710,11 @@ class EnumArrayField : public Field { size_t max_size() const { return N; } bool empty() const { return N == 0; } + // Returns nullptr if the buffer has no space for the array. Enum* data() { - EnsureExtent(); + if (!EnsureExtent()) { + return nullptr; + } return reinterpret_cast(GetRuntime()->template ToAddress( BaseOffset())); } @@ -757,10 +813,9 @@ class EnumArrayField : public Field { if (!v.ok()) { return v.status(); } - EnsureStorage(parsed_count_ + 1); - GetRuntime()->template ToAddress(BaseOffset())[parsed_count_] = *v; - parsed_count_++; - SetActiveCount(parsed_count_); + if (absl::Status status = Append(*v); !status.ok()) { + return status; + } } } else { if (parsed_count_ >= N) { @@ -770,10 +825,9 @@ class EnumArrayField : public Field { if (!v.ok()) { return v.status(); } - EnsureStorage(parsed_count_ + 1); - GetRuntime()->template ToAddress(BaseOffset())[parsed_count_] = *v; - parsed_count_++; - SetActiveCount(parsed_count_); + if (absl::Status status = Append(*v); !status.ok()) { + return status; + } } return absl::OkStatus(); } @@ -811,32 +865,48 @@ class EnumArrayField : public Field { hdr->num_elements = static_cast(count); } - void EnsureStorage(size_t count) { - ::toolbelt::PayloadBuffer::VectorReserve(GetBufferAddr(), - Header(relative_binary_offset_), - count); - ::toolbelt::PayloadBuffer::VectorResize(GetBufferAddr(), - Header(relative_binary_offset_), - count); + // Returns the storage for at least 'count' elements, or nullptr if the + // buffer has no space for them. + T* StorageFor(size_t count) { + if (!::toolbelt::PayloadBuffer::VectorReserve( + GetBufferAddr(), Header(relative_binary_offset_), count) || + !::toolbelt::PayloadBuffer::VectorResize( + GetBufferAddr(), Header(relative_binary_offset_), count)) { + return nullptr; + } + return GetRuntime()->template ToAddress(BaseOffset()); } - void EnsureExtent() { + absl::Status Append(T v) { + T* base = StorageFor(parsed_count_ + 1); + if (base == nullptr) { + return internal::AllocationFailedError(); + } + base[parsed_count_] = v; + parsed_count_++; + SetActiveCount(parsed_count_); + return absl::OkStatus(); + } + + // Returns false if the array is mutable and the buffer has no space for + // all N elements. + bool EnsureExtent() { if (!IsMutable()) { - return; + return true; } const size_t current = NumElements(); if (current >= N) { - return; + return true; } - EnsureStorage(N); - T* base = GetRuntime()->template ToAddress(BaseOffset()); + T* base = StorageFor(N); if (base == nullptr) { - return; + return false; } for (size_t i = current; i < N; i++) { base[i] = T{}; } SetActiveCount(N); + return true; } Enum GetAt(size_t index) const { @@ -960,8 +1030,8 @@ class MessageArrayField : public MessageVectorField { if (parsed_count_ > N) { return absl::InvalidArgumentError("array_size overflow"); } - while (MessageVectorField::size() < N) { - MessageVectorField::Add(); + if (!FillTo(N)) { + return internal::AllocationFailedError(); } parsed_count_ = 0; return absl::OkStatus(); @@ -973,9 +1043,7 @@ class MessageArrayField : public MessageVectorField { } MessageVectorField::Clear(); parsed_count_ = 0; - for (size_t i = 0; i < N; i++) { - MessageVectorField::Add(); - } + (void)FillTo(N); this->ResetFieldCache(); } void clear() { Clear(); } @@ -1058,9 +1126,10 @@ class MessageArrayField : public MessageVectorField { if (!v.ok()) { return v.status(); } - T msg = parsed_count_ < MessageVectorField::size() - ? MessageVectorField::Mutable(parsed_count_) - : MessageVectorField::Add(); + if (!FillTo(parsed_count_ + 1)) { + return internal::AllocationFailedError(); + } + T msg = MessageVectorField::Mutable(parsed_count_); ProtoBuffer msg_buffer(*v); if (absl::Status status = msg.Deserialize(msg_buffer); !status.ok()) { return status; @@ -1074,12 +1143,7 @@ class MessageArrayField : public MessageVectorField { if (!HasMutablePayload(MessageVectorField::GetRuntime())) { return MessageVectorField::operator[](static_cast(index)); } - while (MessageVectorField::size() <= index) { - MessageVectorField::Add(); - } - while (MessageVectorField::size() < N) { - MessageVectorField::Add(); - } + (void)FillTo(std::max(index + 1, N)); return MessageVectorField::Mutable(index); } @@ -1087,9 +1151,20 @@ class MessageArrayField : public MessageVectorField { if (!HasMutablePayload(MessageVectorField::GetRuntime())) { return; } - while (MessageVectorField::size() < N) { + (void)FillTo(N); + } + + // Adds messages until there are 'count'. Returns false if the buffer has + // no space for them. + bool FillTo(size_t count) { + while (MessageVectorField::size() < count) { + const size_t old_size = MessageVectorField::size(); MessageVectorField::Add(); + if (MessageVectorField::size() == old_size) { + return false; + } } + return true; } size_t parsed_count_for_serialize() const { @@ -1338,15 +1413,23 @@ class StringArrayField : public Field { if (parsed_count_ >= N) { return absl::InvalidArgumentError("array_size overflow"); } - EnsureStorage(parsed_count_ + 1); + if (!EnsureStorage(parsed_count_ + 1)) { + return internal::AllocationFailedError(); + } absl::StatusOr v = buffer.DeserializeString(); if (!v.ok()) { return v.status(); } void* str_hdr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), sizeof(toolbelt::StringHeader)); + if (str_hdr == nullptr) { + return internal::AllocationFailedError(); + } ::toolbelt::BufferOffset hdr_offset = GetRuntime()->ToOffset(str_hdr); - ::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), *v, hdr_offset); + if (::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), *v, hdr_offset) == + nullptr) { + return internal::AllocationFailedError(); + } toolbelt::VectorHeader* hdr = Header(); ::toolbelt::BufferOffset* data = GetRuntime()->template ToAddress<::toolbelt::BufferOffset>(hdr->data); @@ -1380,11 +1463,12 @@ class StringArrayField : public Field { Header()->num_elements = static_cast(count); } - void EnsureStorage(size_t count) { - ::toolbelt::PayloadBuffer::VectorReserve<::toolbelt::BufferOffset>( - GetBufferAddr(), Header(), count); - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), Header(), count); + // Returns false if the buffer has no space for 'count' elements. + bool EnsureStorage(size_t count) { + return ::toolbelt::PayloadBuffer::VectorReserve<::toolbelt::BufferOffset>( + GetBufferAddr(), Header(), count) && + ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), Header(), count); } size_t parsed_count_for_serialize() const { @@ -1421,7 +1505,10 @@ class StringArrayField : public Field { } const size_t current = NumElements(); if (current < N) { - EnsureStorage(N); + if (!EnsureStorage(N)) { + BindExistingSlots(0, N); + return; + } for (size_t i = current; i < N; i++) { AllocateStringSlot(i); } @@ -1467,6 +1554,9 @@ class StringArrayField : public Field { if (data[index] == 0) { void* str_hdr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), sizeof(toolbelt::StringHeader)); + if (str_hdr == nullptr) { + return; + } hdr = Header(); data = GetRuntime()->template ToAddress<::toolbelt::BufferOffset>( hdr->data); diff --git a/phaser/runtime/fields.h b/phaser/runtime/fields.h index 47fdc83..24c3095 100644 --- a/phaser/runtime/fields.h +++ b/phaser/runtime/fields.h @@ -89,7 +89,7 @@ class Field { protected: void RequireMutable(uint32_t source_offset) const { if (!Message::GetRuntime(this, source_offset)->IsMutable()) { - throw std::logic_error("cannot mutate a readonly Phaser message"); + PHASER_THROW(std::logic_error("cannot mutate a readonly Phaser message")); } } @@ -741,8 +741,10 @@ class StringField : public Field, public StringFieldFacade { if (!s.ok()) { return s.status(); } - ::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), *s, - StringHeaderOffset()); + if (::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), *s, + StringHeaderOffset()) == nullptr) { + return internal::AllocationFailedError(); + } return absl::OkStatus(); } @@ -810,7 +812,7 @@ class NonEmbeddedStringField if (this == &other) { return *this; } - if (other.IsPlaceholder()) { + if (IsPlaceholder() || other.IsPlaceholder()) { return *this; } std::string_view value = other.Get(); @@ -828,6 +830,9 @@ class NonEmbeddedStringField GetBuffer()->ToOffset(const_cast(value.data())); absl::Span destination = ::toolbelt::PayloadBuffer::AllocateString( GetBufferAddr(), value.size(), absolute_binary_offset_, false); + if (destination.data() == nullptr) { + return *this; + } memmove(destination.data(), GetBuffer()->ToAddress(source), value.size()); return *this; @@ -844,9 +849,7 @@ class NonEmbeddedStringField return *this; } NonEmbeddedStringField& operator=(const char* s) { - ::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), - std::string_view(s, std::strlen(s)), - absolute_binary_offset_); + Set(std::string_view(s, std::strlen(s))); return *this; } @@ -900,7 +903,7 @@ class NonEmbeddedStringField ::toolbelt::PayloadBuffer** GetBufferAddr() const { if (!msg_->runtime->IsMutable()) { - throw std::logic_error("cannot mutate a readonly Phaser string"); + PHASER_THROW(std::logic_error("cannot mutate a readonly Phaser string")); } return &msg_->runtime->pb; } @@ -1003,6 +1006,10 @@ class IndirectMessageField : public Field { // Allocate a new message. void* msg_addr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), MessageType::BinarySize()); + if (msg_addr == nullptr) { + internal::BindToAllocationFailureSink(msg_); + return &msg_; + } ::toolbelt::BufferOffset msg_offset = GetRuntime()->ToOffset(msg_addr); // Assign to the message. msg_.runtime = GetRuntime(); @@ -1105,6 +1112,9 @@ class IndirectMessageField : public Field { // Allocate a new message. void* msg_addr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), MessageType::BinarySize()); + if (msg_addr == nullptr) { + return internal::AllocationFailedError(); + } ::toolbelt::BufferOffset msg_offset = GetRuntime()->ToOffset(msg_addr); // Assign to the message. msg_.runtime = GetRuntime(); @@ -1116,7 +1126,9 @@ class IndirectMessageField : public Field { *addr = msg_offset; // Put message field offset into message. // Install the metadata into the binary message. - msg_.template InstallMetadata(); + if (!msg_.template InstallMetadata()) { + return internal::AllocationFailedError(); + } ProtoBuffer sub_buffer(s.value()); return msg_.Deserialize(sub_buffer); diff --git a/phaser/runtime/message.h b/phaser/runtime/message.h index 4b47f3a..5a840ee 100644 --- a/phaser/runtime/message.h +++ b/phaser/runtime/message.h @@ -8,9 +8,12 @@ #include #include +#include +#include #include #include #include +#include #include #include #include @@ -22,6 +25,27 @@ #include "toolbelt/hexdump.h" #include "toolbelt/payload_buffer.h" +namespace phaser { +namespace internal { +[[noreturn]] inline void Fatal(const char* what) { + (void)std::fprintf(stderr, "phaser: %s\n", what); + std::abort(); +} + +inline absl::Status AllocationFailedError() { + return absl::ResourceExhaustedError("phaser: payload buffer is full"); +} +} // namespace internal +} // namespace phaser + +// Reports a programming error such as an out-of-range index or a write to a +// readonly message. Builds without exceptions print the error and abort. +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) +#define PHASER_THROW(exception) throw exception +#else +#define PHASER_THROW(exception) ::phaser::internal::Fatal(#exception) +#endif + namespace phaser { // Message header. @@ -289,6 +313,9 @@ inline RuntimeControlBlock* InitializeRuntimeControl( capacity = std::max(capacity, 1); void* memory = ::toolbelt::PayloadBuffer::Allocate( pb, static_cast(RuntimeControlSize(capacity)), true); + if (memory == nullptr) { + return nullptr; + } auto* control = new (memory) RuntimeControlBlock; control->capacity = static_cast(capacity); (*pb)->metadata = (*pb)->ToOffset(control); @@ -343,6 +370,9 @@ struct MessageRuntime { void* memory = ::toolbelt::PayloadBuffer::Realloc( &pb, pb->ToAddress(old_offset), static_cast(RuntimeControlSize(new_capacity)), true); + if (memory == nullptr) { + return; + } control = static_cast(memory); control->capacity = new_capacity; pb->metadata = pb->ToOffset(control); @@ -350,6 +380,9 @@ struct MessageRuntime { void* name_memory = ::toolbelt::PayloadBuffer::Allocate( &pb, static_cast(name.size()), false); if (!name.empty()) { + if (name_memory == nullptr) { + return; + } memcpy(name_memory, name.data(), name.size()); } control = GetRuntimeControl(); @@ -629,7 +662,7 @@ struct Message { const Message* msg = reinterpret_cast( reinterpret_cast(field) - offset); if (!msg->runtime->IsMutable()) { - throw std::logic_error("cannot mutate a readonly Phaser message"); + PHASER_THROW(std::logic_error("cannot mutate a readonly Phaser message")); } return &msg->runtime->pb; } @@ -711,26 +744,46 @@ struct Message { return runtime->pb->ToAddress(offset); } + // Returns false, leaving the message without field metadata, if the buffer + // has no space for it. template - void InstallMetadata() { + bool InstallMetadata() { auto metadata = runtime->GetMetadata(MessageType::FullName()); if (metadata != 0) { ::toolbelt::BufferOffset* header = runtime->pb->ToAddress<::toolbelt::BufferOffset>( absolute_binary_offset); *header = metadata; - return; + return true; } // Allocate space for field data in the payload buffer and copy it in. void* fields = ::toolbelt::PayloadBuffer::Allocate( &runtime->pb, sizeof(MessageType::field_data), false); + if (fields == nullptr) { + return false; + } memcpy(fields, &MessageType::field_data, sizeof(MessageType::field_data)); ::toolbelt::BufferOffset* header = runtime->pb->ToAddress<::toolbelt::BufferOffset>( absolute_binary_offset); *header = runtime->pb->ToOffset(fields); runtime->AddMetadata(MessageType::FullName(), *header); + return true; + } + + // True if an allocation in this message's buffer failed because a fixed + // size buffer was full. The message is then incomplete: setters that + // needed space did nothing, and serializing the message fails. + bool AllocationFailed() const { + return runtime != nullptr && runtime->pb != nullptr && + runtime->pb->AllocationFailed(); + } + + // Binds this handle to its own copy of 'rt'. + void BindDetached(MessageRuntime& rt, ::toolbelt::BufferOffset offset) { + absolute_binary_offset = offset; + BindRuntime(BorrowRuntime(rt)); } // Looks for the field number in the field data. Returns the offset of the @@ -819,6 +872,32 @@ struct Message { } }; +namespace internal { + +// Binds 'msg' to detached per-thread storage after the allocation for it +// failed. Callers that must hand back a writable message use this so writes +// through the handle stay in bounds. The owning buffer has already recorded +// the failure, and nothing reads the storage back. Allocations made through +// the detached handle always fail because its buffer has no free space. +template +void BindToAllocationFailureSink(MessageType& msg) { + constexpr size_t kSize = + (sizeof(::toolbelt::PayloadBuffer) + MessageType::BinarySize() + 64 + 7) & + ~size_t{7}; + alignas(8) static thread_local char storage[kSize]; + auto* pb = new (storage) + ::toolbelt::PayloadBuffer(static_cast(kSize), false); + if (::toolbelt::PayloadBuffer::AllocateMainMessage( + &pb, MessageType::BinarySize()) == nullptr) { + Fatal("allocation failure sink is too small"); + } + pb->free_list = 0; + MessageRuntime runtime(pb, true); + msg.BindDetached(runtime, pb->message); +} + +} // namespace internal + ::toolbelt::PayloadBuffer* NewDynamicBuffer( size_t initial_size, Tuning tuning = Tuning::kPerformance); diff --git a/phaser/runtime/phaser_bank.h b/phaser/runtime/phaser_bank.h index 1d90930..88779f0 100644 --- a/phaser/runtime/phaser_bank.h +++ b/phaser/runtime/phaser_bank.h @@ -92,6 +92,9 @@ inline absl::StatusOr> PhaserBankAllocate( return bank_info.status(); } auto [msg, offset] = (*bank_info)->allocate(runtime); + if (msg == nullptr) { + return internal::AllocationFailedError(); + } return std::make_pair(static_cast(msg), offset); } @@ -103,7 +106,12 @@ PhaserBankAllocate(const std::string& message_type, if (!bank_info.ok()) { return bank_info.status(); } - return (*bank_info)->allocate(runtime); + std::pair result = + (*bank_info)->allocate(runtime); + if (result.first == nullptr) { + return internal::AllocationFailedError(); + } + return result; } absl::Status PhaserBankClear(const std::string& message_type, Message& msg); diff --git a/phaser/runtime/union.h b/phaser/runtime/union.h index 9e614ac..062f266 100644 --- a/phaser/runtime/union.h +++ b/phaser/runtime/union.h @@ -302,8 +302,10 @@ class UnionStringField : public UnionMemberField { if (!v.ok()) { return v.status(); } - ::toolbelt::PayloadBuffer::SetString(GetBufferAddr(runtime), *v, - abs_offset); + if (::toolbelt::PayloadBuffer::SetString(GetBufferAddr(runtime), *v, + abs_offset) == nullptr) { + return internal::AllocationFailedError(); + } return absl::OkStatus(); } @@ -359,6 +361,10 @@ class UnionMessageField : public UnionMemberField { // Allocate a new message. void* msg_addr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(runtime), MessageType::BinarySize()); + if (msg_addr == nullptr) { + internal::BindToAllocationFailureSink(msg_); + return &msg_; + } ::toolbelt::BufferOffset msg_offset = GetBuffer(runtime)->ToOffset(msg_addr); // Assign to the message. @@ -405,10 +411,18 @@ class UnionMessageField : public UnionMemberField { Bind(runtime, old_offset); msg_.Clear(); GetBuffer(runtime)->Free(runtime->ToAddress(old_offset)); + addr = GetIndirectAddress(runtime, abs_offset); + if (addr == nullptr) { + return; + } + *addr = 0; } // Allocate a new message. void* msg_addr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(runtime), MessageType::BinarySize()); + if (msg_addr == nullptr) { + return; + } ::toolbelt::BufferOffset msg_offset = GetBuffer(runtime)->ToOffset(msg_addr); // Assign to the message. @@ -475,12 +489,17 @@ class UnionMessageField : public UnionMemberField { } void* msg_addr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(runtime), MessageType::BinarySize()); + if (msg_addr == nullptr) { + return internal::AllocationFailedError(); + } ::toolbelt::BufferOffset msg_offset = GetBuffer(runtime)->ToOffset(msg_addr); // Assign to the message. msg_.runtime = runtime; msg_.absolute_binary_offset = msg_offset; - msg_.template InstallMetadata(); + if (!msg_.template InstallMetadata()) { + return internal::AllocationFailedError(); + } // Buffer might have moved, get address of indirect again. ::toolbelt::BufferOffset* addr = GetIndirectAddress(runtime, abs_offset); @@ -562,7 +581,7 @@ class UnionField : public Field { template decltype(auto) get() const { if (!holds_alternative()) { - throw std::bad_variant_access(); + PHASER_THROW(std::bad_variant_access()); } if constexpr (Alternative::kIsMessage) { return GetReference decltype(auto) get() { if (!holds_alternative()) { - throw std::bad_variant_access(); + PHASER_THROW(std::bad_variant_access()); } if constexpr (Alternative::kIsMessage) { return *Mutable(); diff --git a/phaser/runtime/vectors.h b/phaser/runtime/vectors.h index 42106b6..0f53247 100644 --- a/phaser/runtime/vectors.h +++ b/phaser/runtime/vectors.h @@ -150,13 +150,13 @@ class PrimitiveVectorField : public Field { T& at(size_t index) { if (index >= size()) { - throw std::out_of_range("PrimitiveVectorField::at"); + PHASER_THROW(std::out_of_range("PrimitiveVectorField::at")); } return (*this)[static_cast(index)]; } T at(size_t index) const { if (index >= size()) { - throw std::out_of_range("PrimitiveVectorField::at"); + PHASER_THROW(std::out_of_range("PrimitiveVectorField::at")); } return (*this)[static_cast(index)]; } @@ -169,6 +169,9 @@ class PrimitiveVectorField : public Field { T Get(size_t index) const { return (*this)[static_cast(index)]; } void Set(size_t index, T v) { + if (index >= size()) { + return; + } T* base = GetRuntime()->template ToAddress(BaseOffset()); if (base == nullptr) { return; @@ -193,14 +196,22 @@ class PrimitiveVectorField : public Field { #undef ITYPE #undef CTYPE - void push_back(const T& v) { - ::toolbelt::PayloadBuffer::VectorPush( + void push_back(const T& v) { (void)Push(v); } + + // Returns false, leaving the vector unchanged, if the buffer has no space. + bool Push(const T& v) { + return ::toolbelt::PayloadBuffer::VectorPush( GetBufferAddr(), Header(relative_binary_offset_), v); } + // Returns a detached value if the buffer has no space for the new element. template T& emplace_back(Args&&... args) { + const size_t old_size = size(); push_back(T(std::forward(args)...)); + if (size() == old_size) { + return (*this)[-1]; + } return (*this)[static_cast(size() - 1)]; } @@ -223,7 +234,7 @@ class PrimitiveVectorField : public Field { const T value_copy = value; const size_t old_size = size(); resize(n); - for (size_t i = old_size; i < n; ++i) { + for (size_t i = old_size; i < size(); ++i) { Set(i, value_copy); } } @@ -521,6 +532,9 @@ class PrimitiveVectorField : public Field { return absl::OkStatus(); } T* base = GetRuntime()->template ToAddress(BaseOffset()); + if (base == nullptr || size() != data->size() / sizeof(T)) { + return internal::AllocationFailedError(); + } memcpy(base, data->data(), data->size()); return absl::OkStatus(); } else { @@ -530,7 +544,9 @@ class PrimitiveVectorField : public Field { if (!v.ok()) { return v.status(); } - push_back(*v); + if (!Push(*v)) { + return internal::AllocationFailedError(); + } } } } else { @@ -539,13 +555,17 @@ class PrimitiveVectorField : public Field { if (!v.ok()) { return v.status(); } - push_back(*v); + if (!Push(*v)) { + return internal::AllocationFailedError(); + } } else { absl::StatusOr v = buffer.DeserializeVarint(); if (!v.ok()) { return v.status(); } - push_back(*v); + if (!Push(*v)) { + return internal::AllocationFailedError(); + } } } return absl::OkStatus(); @@ -644,13 +664,13 @@ class EnumVectorField : public Field { Enum& at(size_t index) { if (index >= size()) { - throw std::out_of_range("EnumVectorField::at"); + PHASER_THROW(std::out_of_range("EnumVectorField::at")); } return (*this)[static_cast(index)]; } Enum at(size_t index) const { if (index >= size()) { - throw std::out_of_range("EnumVectorField::at"); + PHASER_THROW(std::out_of_range("EnumVectorField::at")); } return (*this)[static_cast(index)]; } @@ -673,6 +693,9 @@ class EnumVectorField : public Field { Enum Get(size_t index) const { return (*this)[static_cast(index)]; } void Set(size_t index, Enum v) { + if (index >= size()) { + return; + } Enum* base = GetRuntime()->template ToAddress(BaseOffset()); if (base == nullptr) { return; @@ -717,14 +740,22 @@ class EnumVectorField : public Field { return absl::Span(base, n); } - void push_back(const Enum& v) { - ::toolbelt::PayloadBuffer::VectorPush( + void push_back(const Enum& v) { (void)Push(v); } + + // Returns false, leaving the vector unchanged, if the buffer has no space. + bool Push(const Enum& v) { + return ::toolbelt::PayloadBuffer::VectorPush( GetBufferAddr(), Header(relative_binary_offset_), static_cast(v)); } + // Returns a detached value if the buffer has no space for the new element. template Enum& emplace_back(Args&&... args) { + const size_t old_size = size(); push_back(Enum(std::forward(args)...)); + if (size() == old_size) { + return (*this)[-1]; + } return (*this)[static_cast(size() - 1)]; } @@ -746,7 +777,7 @@ class EnumVectorField : public Field { void resize(size_t n, Enum value) { const size_t old_size = size(); resize(n); - for (size_t i = old_size; i < n; ++i) { + for (size_t i = old_size; i < size(); ++i) { Set(i, value); } } @@ -990,14 +1021,18 @@ class EnumVectorField : public Field { if (!v.ok()) { return v.status(); } - push_back(static_cast(*v)); + if (!Push(static_cast(*v))) { + return internal::AllocationFailedError(); + } } } else { absl::StatusOr v = buffer.DeserializeVarint(); if (!v.ok()) { return v.status(); } - push_back(static_cast(*v)); + if (!Push(static_cast(*v))) { + return internal::AllocationFailedError(); + } } return absl::OkStatus(); } @@ -1098,13 +1133,13 @@ class MessageVectorField : public Field { T at(size_t index) { if (index >= size()) { - throw std::out_of_range("MessageVectorField::at"); + PHASER_THROW(std::out_of_range("MessageVectorField::at")); } return (*this)[static_cast(index)]; } T at(size_t index) const { if (index >= size()) { - throw std::out_of_range("MessageVectorField::at"); + PHASER_THROW(std::out_of_range("MessageVectorField::at")); } return (*this)[static_cast(index)]; } @@ -1190,14 +1225,22 @@ class MessageVectorField : public Field { T emplace_back() { return Add(); } + // Returns a detached message, leaving the vector unchanged, if the buffer + // has no space for the new element. T Add() { // Allocate a new message. void* binary = ::toolbelt::PayloadBuffer::Allocate(GetBufferAddr(), T::BinarySize()); + if (binary == nullptr) { + return DetachedMessage(); + } ::toolbelt::BufferOffset absolute_binary_offset = GetRuntime()->ToOffset(binary); - ::toolbelt::PayloadBuffer::VectorPush<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), absolute_binary_offset); + if (!::toolbelt::PayloadBuffer::VectorPush<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), absolute_binary_offset)) { + GetBuffer()->Free(GetRuntime()->ToAddress(absolute_binary_offset)); + return DetachedMessage(); + } T result(GetRuntime(), absolute_binary_offset); result.template InstallMetadata(); return result; @@ -1207,16 +1250,24 @@ class MessageVectorField : public Field { T Mutable(size_t index) { if (index >= size()) { - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), index + 1); + if (!::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), index + 1)) { + return DetachedMessage(); + } } auto hdr = ResolvedVectorHeader(); ::toolbelt::BufferOffset* data = GetRuntime()->template ToAddress<::toolbelt::BufferOffset>(hdr->data); + if (data == nullptr) { + return DetachedMessage(); + } if (data[index] == 0) { void* binary = ::toolbelt::PayloadBuffer::Allocate(GetBufferAddr(), T::BinarySize()); + if (binary == nullptr) { + return DetachedMessage(); + } ::toolbelt::BufferOffset absolute_binary_offset = GetRuntime()->ToOffset(binary); @@ -1233,12 +1284,18 @@ class MessageVectorField : public Field { void SetOffset(int index, toolbelt::BufferOffset offset) { if (static_cast(index) >= size()) { - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), static_cast(index) + 1); + if (!::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), + static_cast(index) + 1)) { + return; + } } auto hdr = ResolvedVectorHeader(); ::toolbelt::BufferOffset* data = GetRuntime()->template ToAddress<::toolbelt::BufferOffset>(hdr->data); + if (data == nullptr) { + return; + } if (data[index] != 0) { // Already set, free the current value. T(GetRuntime(), data[index]).Clear(); @@ -1252,11 +1309,18 @@ class MessageVectorField : public Field { std::vector result; result.reserve(n); Clear(); - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), n); + if (!::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), n)) { + return result; + } // Allocate memory for n messages in the payload buffer. std::vector addrs = ::toolbelt::PayloadBuffer::AllocateMany( GetBufferAddr(), T::BinarySize(), static_cast(n), true); + if (addrs.size() != n) { + (void)::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), 0); + return result; + } toolbelt::VectorHeader* hdr = ResolvedVectorHeader(); ::toolbelt::BufferOffset* data = @@ -1296,8 +1360,10 @@ class MessageVectorField : public Field { } } } - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), n); + if (!::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), n)) { + return; + } for (size_t i = old_size; i < n; ++i) { (void)Mutable(i); } @@ -1305,7 +1371,7 @@ class MessageVectorField : public Field { void resize(size_t n, const T& value) { const size_t old_size = size(); resize(n); - for (size_t i = old_size; i < n; ++i) { + for (size_t i = old_size; i < size(); ++i) { (void)Mutable(i).CloneFrom(value); } } @@ -1512,7 +1578,11 @@ class MessageVectorField : public Field { return v.status(); } ProtoBuffer msg_buffer(*v); + const size_t old_size = size(); T msg = Add(); + if (size() == old_size) { + return internal::AllocationFailedError(); + } if (absl::Status status = msg.Deserialize(msg_buffer); !status.ok()) { return status; } @@ -1526,6 +1596,12 @@ class MessageVectorField : public Field { } private: + static T DetachedMessage() { + T msg(InternalDefault{}); + internal::BindToAllocationFailureSink(msg); + return msg; + } + std::vector CopyValues() const { std::vector result; result.reserve(size()); @@ -1693,13 +1769,13 @@ class StringVectorField : public Field { NonEmbeddedStringField at(size_t index) { if (index >= size()) { - throw std::out_of_range("StringVectorField::at"); + PHASER_THROW(std::out_of_range("StringVectorField::at")); } return (*this)[static_cast(index)]; } std::string_view at(size_t index) const { if (index >= size()) { - throw std::out_of_range("StringVectorField::at"); + PHASER_THROW(std::out_of_range("StringVectorField::at")); } return Get(index); } @@ -1809,22 +1885,37 @@ class StringVectorField : public Field { // Allocate string header in buffer. void* str_hdr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), sizeof(toolbelt::StringHeader)); + if (str_hdr == nullptr) { + return; + } ::toolbelt::BufferOffset hdr_offset = GetRuntime()->ToOffset(str_hdr); - ::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), s, hdr_offset); + if (::toolbelt::PayloadBuffer::SetString(GetBufferAddr(), s, hdr_offset) == + nullptr) { + GetBuffer()->Free(GetRuntime()->ToAddress(hdr_offset)); + return; + } // Add an offset for the new string to the binary. - ::toolbelt::PayloadBuffer::VectorPush<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), hdr_offset); + if (!::toolbelt::PayloadBuffer::VectorPush<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), hdr_offset)) { + ::toolbelt::PayloadBuffer::ClearString(GetBufferAddr(), hdr_offset); + GetBuffer()->Free(GetRuntime()->ToAddress(hdr_offset)); + } } + // These return a detached string if the buffer has no space for the new + // element. template NonEmbeddedStringField emplace_back(Str s) { + const size_t old_size = size(); push_back(s); + if (size() == old_size) { + return {}; + } return back(); } NonEmbeddedStringField emplace_back() { - push_back(std::string_view()); - return back(); + return emplace_back(std::string_view()); } void Add(const char* s, size_t len) { push_back(std::string_view(s, len)); } @@ -1840,17 +1931,25 @@ class StringVectorField : public Field { template void Set(size_t index, Str s) { if (index >= size()) { - ::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( - GetBufferAddr(), ResolvedVectorHeader(), index + 1); + if (!::toolbelt::PayloadBuffer::VectorResize<::toolbelt::BufferOffset>( + GetBufferAddr(), ResolvedVectorHeader(), index + 1)) { + return; + } } auto hdr = ResolvedVectorHeader(); ::toolbelt::BufferOffset* data = GetRuntime()->template ToAddress<::toolbelt::BufferOffset>(hdr->data); + if (data == nullptr) { + return; + } if (data[index] == 0) { // Allocate string header in buffer. void* str_hdr = ::toolbelt::PayloadBuffer::Allocate( GetBufferAddr(), sizeof(toolbelt::StringHeader)); + if (str_hdr == nullptr) { + return; + } ::toolbelt::BufferOffset hdr_offset = GetRuntime()->ToOffset(str_hdr); hdr = ResolvedVectorHeader(); @@ -1895,7 +1994,7 @@ class StringVectorField : public Field { const std::string value_copy(value); const size_t old_size = size(); resize(n); - for (size_t i = old_size; i < n; ++i) { + for (size_t i = old_size; i < size(); ++i) { Set(i, value_copy); } } @@ -2080,7 +2179,11 @@ class StringVectorField : public Field { if (!v.ok()) { return v.status(); } + const size_t old_size = size(); push_back(*v); + if (size() == old_size) { + return internal::AllocationFailedError(); + } return absl::OkStatus(); } diff --git a/phaser/testdata/BUILD b/phaser/testdata/BUILD index 7f77192..1540978 100644 --- a/phaser/testdata/BUILD +++ b/phaser/testdata/BUILD @@ -103,6 +103,43 @@ phaser_library( deps = [":ros_compile_proto"], ) +proto_library( + name = "fixed_buffer_proto", + srcs = ["FixedBuffer.proto"], + deps = ["//phaser:options_proto"], +) + +cc_proto_library( + name = "fixed_buffer_cc_proto", + deps = [":fixed_buffer_proto"], +) + +# Generated without exceptions or RTTI to check that Phaser builds that way. +phaser_library( + name = "fixed_buffer_phaser", + add_namespace = "phaser", + copts = [ + "-fno-exceptions", + "-fno-rtti", + ], + runtime = "//phaser/runtime:phaser_runtime", + deps = [":fixed_buffer_proto"], +) + +# The ROS frontend generates fixed size arrays for the array_size fields. +phaser_library( + name = "fixed_buffer_ros_phaser", + add_namespace = "phaser_ros", + copts = [ + "-fno-exceptions", + "-fno-rtti", + ], + direct_header_symlinks = False, + frontend = "ros", + runtime = "//phaser/runtime:phaser_runtime", + deps = [":fixed_buffer_proto"], +) + proto_library( name = "service_only_proto", srcs = ["ServiceOnly.proto"], diff --git a/phaser/testdata/FixedBuffer.proto b/phaser/testdata/FixedBuffer.proto new file mode 100644 index 0000000..14d0e1e --- /dev/null +++ b/phaser/testdata/FixedBuffer.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +// Messages for fixed_buffer_test, which fills fixed size buffers until they +// run out of space. The fields cover every kind that allocates in the buffer. +package foo.bar; + +import "phaser/options.proto"; + +message FixedBufferInner { + int32 id = 1; + string label = 2; +} + +message FixedBufferMessage { + int32 x = 1; + string name = 2; + FixedBufferInner inner = 3; + repeated int32 values = 4; + repeated string names = 5; + repeated FixedBufferInner inners = 6; + oneof choice { + string choice_name = 7; + FixedBufferInner choice_inner = 8; + } + repeated int32 fixed_values = 9 [(phaser.array_size) = 4]; + repeated string fixed_names = 10 [(phaser.array_size) = 2]; + repeated FixedBufferInner fixed_inners = 11 [(phaser.array_size) = 2]; + bytes payload = 12; +} From 3d5eca68b74da5ad982a2768b9b54da728fd17f7 Mon Sep 17 00:00:00 2001 From: Dave Allison Date: Fri, 9 Oct 2026 11:01:02 -0700 Subject: [PATCH 2/2] Generate messages in dependency order, avoid presence accessor collisions, add a CMake build - Emit top-level messages after the same-file messages their fields (and their nested messages' fields) use, so a message can use one defined later in the .proto file. - When a message has fields named both and has_, name 's presence accessor _phaser_has_() so it doesn't collide with the has_ field's getter. - Add CMakeLists.txt: the phaser_runtime library, the protoc-gen-phaser plugin (host builds only; cross-compiles take PHASER_PLUGIN_EXECUTABLE), and phaser_add_library() to generate and build a library like phaser_library. - Add generator_edge_cases_test covering both generator fixes. --- CMakeLists.txt | 263 +++++++++++++++++++++++ README.md | 29 +++ phaser/BUILD.bazel | 12 ++ phaser/compiler/gen.cc | 53 ++++- phaser/compiler/message_gen.cc | 21 +- phaser/compiler/message_gen.h | 1 + phaser/generator_edge_cases_test.cc | 61 ++++++ phaser/testdata/BUILD | 12 ++ phaser/testdata/GeneratorEdgeCases.proto | 40 ++++ 9 files changed, 485 insertions(+), 7 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 phaser/generator_edge_cases_test.cc create mode 100644 phaser/testdata/GeneratorEdgeCases.proto diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..137069a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,263 @@ +# CMakeLists.txt for the phaser project +# +# Builds the phaser runtime library, the protoc-gen-phaser protoc plugin, and +# provides phaser_add_library() to generate a C++ library from .proto files. +# +# A parent project that already provides Abseil (absl::*), cpp_toolbelt +# (toolbelt) and protobuf (protobuf::libprotoc, protobuf::protoc) targets is +# used as is. Missing dependencies are fetched with FetchContent. +# +# When cross-compiling, the plugin must run on the build host, so it is not +# built. Build it for the host separately and pass its path in +# PHASER_PLUGIN_EXECUTABLE. + +cmake_minimum_required(VERSION 3.15) +project(phaser LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(CMAKE_CROSSCOMPILING) + set(PHASER_BUILD_COMPILER_DEFAULT OFF) +else() + set(PHASER_BUILD_COMPILER_DEFAULT ON) +endif() +option(PHASER_BUILD_COMPILER "Build the protoc-gen-phaser plugin" + ${PHASER_BUILD_COMPILER_DEFAULT}) +set(PHASER_PLUGIN_EXECUTABLE "" CACHE FILEPATH + "Host protoc-gen-phaser used when the plugin is not built here") + +include(FetchContent) + +if(NOT TARGET absl::base) + set(ABSL_PROPAGATE_CXX_STD ON) + # protobuf's install rules export targets that link Abseil. + set(ABSL_ENABLE_INSTALL ON) + FetchContent_Declare( + abseil + GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git + GIT_TAG 20250814.1 + ) + FetchContent_MakeAvailable(abseil) +endif() + +if(NOT TARGET toolbelt) + # Matches the cpp_toolbelt git_override in MODULE.bazel. + FetchContent_Declare( + cpp_toolbelt + GIT_REPOSITORY https://github.com/dallison/cpp_toolbelt.git + GIT_TAG 86b45cdd3da46a97a4476be2f2a61909677284a2 + ) + FetchContent_MakeAvailable(cpp_toolbelt) +endif() + +if(PHASER_BUILD_COMPILER AND NOT TARGET protobuf::libprotoc) + set(protobuf_BUILD_TESTS OFF CACHE INTERNAL "") + FetchContent_Declare( + protobuf + GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git + GIT_TAG v34.1 + ) + FetchContent_MakeAvailable(protobuf) +endif() + +# --- Runtime library --- +add_library(phaser_runtime STATIC + phaser/runtime/md5.cc + phaser/runtime/message.cc + phaser/runtime/phaser_bank.cc +) +add_library(phaser::runtime ALIAS phaser_runtime) +target_include_directories(phaser_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(phaser_runtime PUBLIC + absl::flat_hash_map + absl::status + absl::statusor + absl::strings + absl::str_format + absl::span + toolbelt +) + +# --- protoc plugin --- +if(PHASER_BUILD_COMPILER) + if(DEFINED protobuf_SOURCE_DIR) + set(PHASER_PROTOBUF_IMPORT_DIR "${protobuf_SOURCE_DIR}/src" CACHE PATH + "Directory containing google/protobuf/*.proto") + else() + set(PHASER_PROTOBUF_IMPORT_DIR "" CACHE PATH + "Directory containing google/protobuf/*.proto") + endif() + if(NOT PHASER_PROTOBUF_IMPORT_DIR) + message(FATAL_ERROR + "Set PHASER_PROTOBUF_IMPORT_DIR to the directory containing " + "google/protobuf/descriptor.proto") + endif() + + set(PHASER_OPTIONS_SRC "${CMAKE_CURRENT_BINARY_DIR}/phaser/options.pb.cc") + set(PHASER_OPTIONS_HDR "${CMAKE_CURRENT_BINARY_DIR}/phaser/options.pb.h") + add_custom_command( + OUTPUT ${PHASER_OPTIONS_SRC} ${PHASER_OPTIONS_HDR} + COMMAND $ + ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} + --proto_path=${CMAKE_CURRENT_SOURCE_DIR} + --proto_path=${PHASER_PROTOBUF_IMPORT_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/phaser/options.proto + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/phaser/options.proto protobuf::protoc + COMMENT "Generating C++ code from phaser/options.proto" + ) + + add_executable(protoc-gen-phaser + phaser/compiler/enum_gen.cc + phaser/compiler/gen.cc + phaser/compiler/main.cc + phaser/compiler/message_gen.cc + ${PHASER_OPTIONS_SRC} + ) + target_include_directories(protoc-gen-phaser PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ) + target_link_libraries(protoc-gen-phaser PRIVATE + absl::flat_hash_map + absl::flat_hash_set + absl::status + absl::statusor + absl::strings + absl::str_format + protobuf::libprotoc + protobuf::libprotobuf + ) +endif() + +# phaser_add_library( +# PROTOS ... +# IMPORT_DIRS ... +# [IMPORTS ...] +# [PACKAGE ] +# [ADD_NAMESPACE ] +# [FRONTEND protobuf|ros] +# [PROTOC ] +# [PLUGIN ] +# [COMPILE_OPTIONS