Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 $<TARGET_FILE:protobuf::protoc>
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(<name>
# PROTOS <proto>...
# IMPORT_DIRS <dir>...
# [IMPORTS <proto>...]
# [PACKAGE <package>]
# [ADD_NAMESPACE <namespace>]
# [FRONTEND protobuf|ros]
# [PROTOC <protoc>]
# [PLUGIN <protoc-gen-phaser>]
# [COMPILE_OPTIONS <option>...])
#
# Generates phaser C++ code for PROTOS and builds it into the static library
# <name>. Proto files are named by their import path and found in
# IMPORT_DIRS. IMPORTS lists the imported .proto files that need phaser code
# too, such as google/protobuf/any.proto. descriptor.proto and
# phaser/options.proto need none.
#
# As with the Bazel phaser_library rule, the generated files live in
# <package>/<name>/<proto path>.phaser.{h,cc} and each file in PROTOS also gets
# a short header, so "proto/foo.proto" is included as "proto/foo.phaser.h".
# PACKAGE defaults to the directory of the first file in PROTOS.
#
# PROTOC defaults to the protobuf::protoc target. PLUGIN defaults to the
# protoc-gen-phaser target, or PHASER_PLUGIN_EXECUTABLE when that is set.
function(phaser_add_library name)
cmake_parse_arguments(ARG ""
"PACKAGE;ADD_NAMESPACE;FRONTEND;PROTOC;PLUGIN"
"PROTOS;IMPORTS;IMPORT_DIRS;COMPILE_OPTIONS"
${ARGN})
if(NOT ARG_PROTOS)
message(FATAL_ERROR "phaser_add_library(${name}) needs PROTOS")
endif()

if(NOT DEFINED ARG_PACKAGE)
list(GET ARG_PROTOS 0 first_proto)
get_filename_component(ARG_PACKAGE "${first_proto}" DIRECTORY)
endif()
if(NOT ARG_FRONTEND)
set(ARG_FRONTEND protobuf)
endif()

set(tool_depends)
if(NOT ARG_PROTOC)
if(NOT TARGET protobuf::protoc)
message(FATAL_ERROR "phaser_add_library(${name}) needs PROTOC")
endif()
set(ARG_PROTOC $<TARGET_FILE:protobuf::protoc>)
list(APPEND tool_depends protobuf::protoc)
endif()
if(NOT ARG_PLUGIN)
if(PHASER_PLUGIN_EXECUTABLE)
set(ARG_PLUGIN "${PHASER_PLUGIN_EXECUTABLE}")
elseif(TARGET protoc-gen-phaser)
set(ARG_PLUGIN $<TARGET_FILE:protoc-gen-phaser>)
list(APPEND tool_depends protoc-gen-phaser)
else()
message(FATAL_ERROR
"phaser_add_library(${name}) needs a host protoc-gen-phaser: "
"pass PLUGIN or set PHASER_PLUGIN_EXECUTABLE")
endif()
endif()

set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/${name}_phaser")
file(MAKE_DIRECTORY "${out_dir}")
if(ARG_PACKAGE)
set(gen_dir "${out_dir}/${ARG_PACKAGE}/${name}")
set(gen_include "${ARG_PACKAGE}/${name}")
else()
set(gen_dir "${out_dir}/${name}")
set(gen_include "${name}")
endif()

set(proto_paths)
foreach(dir ${ARG_IMPORT_DIRS})
list(APPEND proto_paths "--proto_path=${dir}")
endforeach()

set(options "package_name=${ARG_PACKAGE},target_name=${name},frontend=${ARG_FRONTEND}")
if(ARG_ADD_NAMESPACE)
set(options "add_namespace=${ARG_ADD_NAMESPACE},${options}")
endif()

set(inputs)
set(outputs)
set(short_headers)
foreach(proto ${ARG_PROTOS} ${ARG_IMPORTS})
set(found)
foreach(dir ${ARG_IMPORT_DIRS})
if(NOT found AND EXISTS "${dir}/${proto}")
set(found "${dir}/${proto}")
endif()
endforeach()
if(NOT found)
message(FATAL_ERROR
"phaser_add_library(${name}): ${proto} is not in IMPORT_DIRS")
endif()
list(APPEND inputs "${found}")

string(REGEX REPLACE "\\.proto$" "" base "${proto}")
list(APPEND outputs "${gen_dir}/${base}.phaser.cc" "${gen_dir}/${base}.phaser.h")
if(proto IN_LIST ARG_PROTOS)
set(short_header "${out_dir}/${base}.phaser.h")
file(GENERATE OUTPUT "${short_header}"
CONTENT "#pragma once\n#include \"${gen_include}/${base}.phaser.h\"\n")
list(APPEND short_headers "${short_header}")
endif()
endforeach()

# The protos are named by import path. An absolute path could map to a
# second name when one import directory contains another.
add_custom_command(
OUTPUT ${outputs}
COMMAND ${ARG_PROTOC}
ARGS --plugin=protoc-gen-phaser=${ARG_PLUGIN}
--phaser_out=${options}:${out_dir}
${proto_paths}
${ARG_PROTOS} ${ARG_IMPORTS}
DEPENDS ${inputs} ${tool_depends}
WORKING_DIRECTORY "${out_dir}"
COMMENT "Generating phaser code for ${name}"
VERBATIM
)

add_library(${name} STATIC ${outputs} ${short_headers})
target_include_directories(${name} PUBLIC "${out_dir}")
target_link_libraries(${name} PUBLIC phaser_runtime)
if(ARG_COMPILE_OPTIONS)
target_compile_options(${name} PRIVATE ${ARG_COMPILE_OPTIONS})
endif()
endfunction()
12 changes: 10 additions & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module(
name = "phaser",
version = "2.1.2"
version = "2.1.3"
)

bazel_dep(name = "bazel_skylib", version = "1.9.0")
Expand All @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<foo::bar::phaser::TestMessage> 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
Expand Down Expand Up @@ -412,6 +444,35 @@ Output is written to `OUTPUT_DIR/PACKAGE/TARGET`. See the user guide for the ful
reference. Use `frontend=protobuf` for the default accessor API and optionally
add `active_message=true`.

### CMake

[`CMakeLists.txt`](CMakeLists.txt) builds the `phaser_runtime` library and the
`protoc-gen-phaser` plugin, and defines `phaser_add_library()`, which generates and
builds a library the way `phaser_library` does in Bazel. A parent project's Abseil,
cpp_toolbelt and protobuf targets are used when they exist; anything missing is fetched
with `FetchContent`.

```cmake
FetchContent_Declare(phaser GIT_REPOSITORY https://github.com/dallison/phaser.git GIT_TAG ...)
FetchContent_MakeAvailable(phaser)

phaser_add_library(foo_phaser
PROTOS foo/foo.proto
IMPORTS google/protobuf/any.proto
IMPORT_DIRS ${CMAKE_SOURCE_DIR} ${protobuf_SOURCE_DIR}/src
ADD_NAMESPACE phaser)
```

`PROTOS` and `IMPORTS` are named by import path. `IMPORTS` lists the imported files that
need phaser code too. The library's short header is included as `"foo/foo.phaser.h"`.
The plugin runs on the build host, so a cross-compile does not build it; build it for the
host and pass its path in `PHASER_PLUGIN_EXECUTABLE`.

### Accessor names

A field named `has_<name>` next to a field `<name>` would collide with `<name>`'s presence
accessor, so that presence accessor is named `_phaser_has_<name>()` instead.

## Project layout

| Path | Description |
Expand Down
28 changes: 28 additions & 0 deletions phaser/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ cc_test(
],
)

cc_test(
name = "generator_edge_cases_test",
srcs = ["generator_edge_cases_test.cc"],
copts = PHASER_COPTS,
data = ["valgrind.supp"],
deps = [
"//phaser/runtime:phaser_runtime",
"//phaser/testdata:generator_edge_cases_phaser",
"@com_google_googletest//:gtest_main",
],
)

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"],
Expand Down
Loading
Loading