Skip to content
Draft
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
5 changes: 3 additions & 2 deletions .cursor/skills/subspace-builds/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ Android cross-compile so protobuf versions match:

```bash
cmake -S . -B build/host-protoc -DCMAKE_BUILD_TYPE=Release
cmake --build build/host-protoc --target protoc --parallel
cmake --build build/host-protoc --target protoc protoc-gen-phaser --parallel

cmake -S . -B build/android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-28 \
-DANDROID_STL=c++_shared \
-DCMAKE_BUILD_TYPE=Release \
-DPROTOC_EXECUTABLE="$PWD/build/host-protoc/_deps/protobuf-build/protoc"
-DPROTOC_EXECUTABLE="$PWD/build/host-protoc/_deps/protobuf-build/protoc" \
-DPHASER_PLUGIN_EXECUTABLE="$PWD/build/host-protoc/_deps/phaser-build/protoc-gen-phaser"

cmake --build build/android --parallel
```
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,10 @@ jobs:
- name: Set up Android NDK
run: echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> $GITHUB_ENV

- name: Build host protoc for cross-compilation
- name: Build host protoc and phaser plugin for cross-compilation
run: |
cmake -S . -B build/host-protoc -DCMAKE_BUILD_TYPE=Release
cmake --build build/host-protoc --target protoc --parallel $(nproc)
cmake --build build/host-protoc --target protoc protoc-gen-phaser --parallel $(nproc)

- name: Configure CMake for Android x86_64
run: |
Expand All @@ -483,7 +483,8 @@ jobs:
-DANDROID_PLATFORM=android-28 \
-DANDROID_STL=c++_shared \
-DCMAKE_BUILD_TYPE=Release \
-DPROTOC_EXECUTABLE="$PWD/build/host-protoc/_deps/protobuf-build/protoc"
-DPROTOC_EXECUTABLE="$PWD/build/host-protoc/_deps/protobuf-build/protoc" \
-DPHASER_PLUGIN_EXECUTABLE="$PWD/build/host-protoc/_deps/phaser-build/protoc-gen-phaser"

- name: Build
run: cmake --build build/android --parallel $(nproc)
Expand Down
58 changes: 58 additions & 0 deletions Android.bp
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,61 @@ cc_library_static {
export_shared_lib_headers: ["libprotobuf-cpp-full"],
rtti: false,
}

// Phaser messages generated from subspace.proto for the ASIL client's server
// connection. They are sent and received in protobuf wire format. As in the
// Bazel phaser_library rule, the plugin writes
// proto/subspace_phaser/<proto path>.phaser.{cc,h}, generating any.proto too,
// and a short proto/subspace.phaser.h header includes the generated one. The
// .cc and .h outputs come from separate genrules so the headers never end up
// in `generated_sources`.
subspace_phaser_cmd = "$(location aprotoc) " +
"--plugin=protoc-gen-phaser=$(location protoc-gen-phaser) " +
"--phaser_out=add_namespace=phaser,package_name=proto," +
"target_name=subspace_phaser,frontend=protobuf:$(genDir) " +
"-Iexternal/subspace -Iexternal/protobuf/src " +
"$(in) external/protobuf/src/google/protobuf/any.proto"

genrule {
name: "libsubspace_phaser_cc",
tools: [
"aprotoc",
"protoc-gen-phaser",
],
cmd: subspace_phaser_cmd,
srcs: ["proto/subspace.proto"],
out: [
"proto/subspace_phaser/proto/subspace.phaser.cc",
"proto/subspace_phaser/google/protobuf/any.phaser.cc",
],
}

genrule {
name: "libsubspace_phaser_h",
tools: [
"aprotoc",
"protoc-gen-phaser",
],
cmd: subspace_phaser_cmd + " && " +
"echo '#pragma once' > $(genDir)/proto/subspace.phaser.h && " +
"echo '#include \"proto/subspace_phaser/proto/subspace.phaser.h\"' " +
">> $(genDir)/proto/subspace.phaser.h",
srcs: ["proto/subspace.proto"],
out: [
"proto/subspace.phaser.h",
"proto/subspace_phaser/proto/subspace.phaser.h",
"proto/subspace_phaser/google/protobuf/any.phaser.h",
],
}

cc_library_static {
name: "libsubspace_phaser",
defaults: ["subspace_defaults"],
generated_sources: ["libsubspace_phaser_cc"],
generated_headers: ["libsubspace_phaser_h"],
export_generated_headers: ["libsubspace_phaser_h"],
cppflags: ["-fno-exceptions"],
rtti: false,
static_libs: ["libphaser_runtime"],
export_static_lib_headers: ["libphaser_runtime"],
}
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,72 @@

## Unreleased

### Static Channel Config
- `subspace_server --channel_config=FILE` reads a `StaticChannelConfig` in
protobuf text format and creates its channels and multiplexers before the
server accepts clients. Clients can then use only those channels and the
server's own, the channels stay when they have no users, and the server
defines their layout.
- Publishers on a static channel must set the new `server_defined` option
(`PublisherOptions::SetServerDefined`, `server_defined` in the C, Python,
Rust and ASIL clients) and can't set the slot size, number of slots, fixed
size, checksum size, metadata size, subscriber queue arena size or split
buffer setting. The server rejects `server_defined` on any other channel.
`CreatePublisherResponse` now reports the channel's slot size, checksum
size, metadata size, split buffer setting and fixed size, and a
`server_defined` publisher uses them.
- `subspace_publisher_options_server_defined()` in the C client gives
options for a static channel.
- The default `checksum_size` in the C++ and Rust `PublisherOptions` is now 0,
which means 4 bytes as before, so a `server_defined` publisher can use the
default options.
- A multiplexer with no publishers now reports its slot size.
- Publishers on different virtual channels of a multiplexer are checked
against each other for fixed size and locality, as they share the
multiplexer's buffers. Before, a second fixed-size publisher on another
virtual channel was rejected, so a static multiplexer could only have
publishers on one virtual channel.
- The server creates the message buffers of every static channel and
multiplexer when it starts, so subscribers can map them before any
publisher exists.
- `use_split_buffers` in a static channel or multiplexer config gives it
split buffers, which the server creates.
- `GetClientBuffersRequest` can ask for one prefix or slot of a split buffer
set with `filter_slot`, `is_prefix` and `slot_id`.

### ASIL Client
- `asil_client/` is a new C++17 client for safety-related software. It
shares channels with the standard clients through the same shared memory
protocol, using only the standard library and
POSIX. It is built without exceptions or RTTI, returns an error code from
every operation, and allocates no memory while publishing or reading.
Checksums, metadata, `ReadNewest`, dropped message counts and activation
messages work with the standard clients. The server handshake is behind a
replaceable `ServerConnection` interface. `PhaserServerConnection`
implements the standard protocol with phaser messages in fixed buffers,
sent and received in protobuf wire format, so the handshake makes no heap
allocations either. See `docs/asil-client.md`.
- The ASIL client also supports reliable publishers and subscribers, virtual
channels and multiplexer subscribers, subscribers holding up to 64
messages, subscriber queues, split buffers (with C callbacks for slots from
a custom allocator), and the memfd backend. Reading the newest message on
a reliable subscriber marks the skipped messages as read.
- ASIL publishers and subscribers map all of a channel's shared memory when
they open, and publishing and reading make no shared memory calls. A
subscriber needs the channel to have buffers when it opens: always true of
static channels, otherwise a publisher must have opened. Messages in a
buffer that a standard publisher created by resizing after the subscriber
opened return `kBufferNotMapped`.
- The standard C++ client builds its server requests and responses as
phaser messages too, in growable buffers. `ServerWire` in
`common/server_wire.h` encodes them for both C++ clients, with fixed or
growable buffers. The bytes on the socket are unchanged.
- Bazel, CMake and Soong build phaser and generate `subspace_phaser` from
`subspace.proto`. Android CMake cross-compiles need a host
`protoc-gen-phaser` in `PHASER_PLUGIN_EXECUTABLE`.
- Abseil 20250814.1, protobuf 33.4, cpp_toolbelt 2.1.6 and phaser 2.1.3 in
the Bazel build. CMake uses cpp_toolbelt 2.1.6.

### Stuck Subscriber Warning
- A subscriber now logs an error when a message has been waiting for 5
seconds that it can't read because it already holds `max_active_messages`
Expand Down
18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ endif()
FetchContent_Declare(
cpp_toolbelt
GIT_REPOSITORY https://github.com/dallison/cpp_toolbelt.git
GIT_TAG 2.1.2
# cpp_toolbelt 2.1.6, as in the git_override in MODULE.bazel.
GIT_TAG 86b45cdd3da46a97a4476be2f2a61909677284a2
CMAKE_ARGS
-DCMAKE_OSX_ARCHITECTURES="${CMAKE_OSX_ARCHITECTURES}"
)
Expand All @@ -165,6 +166,20 @@ FetchContent_MakeAvailable(protobuf)
# Protobuf provides targets like protobuf::libprotobuf and protobuf::protoc
# These targets can be used directly without find_package

# --- External Dependency: phaser (using FetchContent for native CMake) ---
# The C++ clients build their server requests and responses as phaser
# messages: in growable buffers for the standard client and fixed buffers for
# the ASIL client. Phaser uses the Abseil, cpp_toolbelt and protobuf fetched
# above.
# Cross-compiles take a host protoc-gen-phaser in PHASER_PLUGIN_EXECUTABLE.
FetchContent_Declare(
phaser
GIT_REPOSITORY https://github.com/dallison/phaser.git
# phaser 2.1.3, as in the git_override in MODULE.bazel.
GIT_TAG 3d5eca68b74da5ad982a2768b9b54da728fd17f7
)
FetchContent_MakeAvailable(phaser)

# --- Option: coroutine/networking backend for the server ---
# Mirrors the Bazel //:coro_backend flag. "co" (default) builds against the co
# coroutine library; "asio" builds the server against Boost.Asio. The value is
Expand Down Expand Up @@ -256,6 +271,7 @@ add_subdirectory(server)
add_subdirectory(plugins)
add_subdirectory(shadow)
add_subdirectory(c_client)
add_subdirectory(asil_client)
add_subdirectory(android)
add_subdirectory(rust_client)
add_subdirectory(rpc)
Expand Down
28 changes: 25 additions & 3 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,39 @@ use_repo(subspace, "subspace_max_channels")

bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "abseil-cpp", version = "20250512.1")
bazel_dep(name = "abseil-cpp", version = "20250814.1")
bazel_dep(name = "googletest", version = "1.17.0")
bazel_dep(name = "protobuf", version = "33.2")
bazel_dep(name = "protobuf", version = "33.4")

# Phaser asks for protobuf 34.1, whose rules_android dependency breaks the
# Android SDK toolchain here.
single_version_override(
module_name = "protobuf",
version = "33.4",
)

bazel_dep(name = "rules_cc", version = "0.2.17")
bazel_dep(name = "rules_java", version = "9.1.0")
bazel_dep(name = "rules_pkg", version = "1.0.1")
bazel_dep(name = "rules_android_ndk", version = "0.1.5")
bazel_dep(name = "zlib", version = "1.3.1.bcr.5")
bazel_dep(name = "coroutines", version = "3.3.5")
bazel_dep(name = "cpp_toolbelt", version = "2.1.3")
bazel_dep(name = "cpp_toolbelt", version = "2.1.6")
bazel_dep(name = "phaser", version = "2.1.3")

# Remove once cpp_toolbelt 2.1.6 and phaser 2.1.3 are in the Bazel Central
# Registry.
git_override(
module_name = "cpp_toolbelt",
commit = "86b45cdd3da46a97a4476be2f2a61909677284a2",
remote = "https://github.com/dallison/cpp_toolbelt.git",
)

git_override(
module_name = "phaser",
commit = "3d5eca68b74da5ad982a2768b9b54da728fd17f7",
remote = "https://github.com/dallison/phaser.git",
)
bazel_dep(name = "rules_python", version = "1.7.0")

# Boost (for asio_rpc)
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ See the file docs/subspace.pdf for full documentation. Additional documentation
- [Slot Sizes](docs/slot-sizes.md)
- [Channel Limit](docs/max-channels.md)
- [C Client API](docs/c-client.md)
- [ASIL Client](docs/asil-client.md)
- [Client Architecture](docs/client-architecture.md)
- [Server Architecture](docs/server-architecture.md)
- [Asio Backend and vsock Bridging](docs/asio-backend.md)
Expand Down Expand Up @@ -2008,6 +2009,61 @@ The `manual_tests/pub` tool takes `--local=false` and `--channel=NAME`, and
`manual_tests/sub` takes `--channel=NAME`, so you can also drive bridging by
hand against servers on two different computers.

## Static Channel Config

By default the server creates channels on demand as publishers and subscribers
arrive. For systems that need a fixed, known set of channels, start the server
with `--channel_config=FILE`. The file is a `StaticChannelConfig`
(`proto/subspace.proto`) in protobuf text format:

```
multiplexers { name: "/sensors" slot_size: 1024 num_slots: 16 type: "Sensor" }
channels { name: "/vehicle/state" slot_size: 256 num_slots: 8 type: "State" }
channels { name: "/sensors/lidar" mux: "/sensors" vchan_id: 0 }
channels { name: "/sensors/radar" mux: "/sensors" vchan_id: 1 }
```

The server creates every configured channel and its message buffers before it
accepts clients, so a subscriber that starts first gets the full channel
layout and maps the buffers instead of waiting for a publisher. With a config
loaded:

- Clients can use only the configured channels and the server's own
`/subspace/ChannelDirectory` and `/subspace/Statistics`. Creating a
publisher or subscriber for any other channel fails.
- Configured channels stay for the life of the server, including when they
have no publishers or subscribers.
- The server defines the layout. Publishers on a configured channel must set
`SetServerDefined(true)` and leave every option that shapes the shared
memory unset: `SetSlotSize`, `SetNumSlots`, `SetFixedSize`,
`SetChecksumSize`, `SetMetadataSize`, `SetSubscriberQueueArenaSize` and
`SetUseSplitBuffers`. The server rejects a publisher that sets any of them
or leaves out `server_defined`, and also a `server_defined` publisher on a
channel that isn't configured. The publisher takes its slot size, number of
slots, checksum size, metadata size and split buffer setting from the
server, and is always fixed size:

```c++
absl::StatusOr<subspace::Publisher> pub = client.CreatePublisher(
"/vehicle/state", subspace::PublisherOptions().SetServerDefined(true));
```

The C client has `subspace_publisher_options_server_defined()`, Python has
`set_server_defined(True)`, Rust has `set_server_defined(true)` and the ASIL
client has `PublisherOptions::server_defined`. Options that don't change
the layout, such as reliable, checksum, type and mux, work as usual.
- `use_split_buffers: true` gives a channel or multiplexer
[split buffers](docs/split-buffers.md), which the server creates.
- A channel on a multiplexer takes its slot size, number of slots, type,
prefix layout and split buffer setting from the multiplexer and needs an
explicit `vchan_id`.
- Names starting with `/subspace/` are reserved for the server.
- After a restart from a shadow, the recovered channels must match the config
or the server doesn't start.

The [ASIL client](docs/asil-client.md) is a C++17 client for safety-related
software that shares these channels with the standard clients.

## Shadow Server (Crash Recovery)

Subspace supports a **shadow process** that mirrors the server's channel,
Expand Down Expand Up @@ -2083,6 +2139,7 @@ its full state from whichever shadow is available.
|------|---------|-------------|
| `--shadow_socket` | `""` (disabled) | Unix socket path for the primary shadow process |
| `--secondary_shadow_socket` | `""` (disabled) | Unix socket path for the secondary shadow process |
| `--channel_config` | `""` (disabled) | `StaticChannelConfig` text format file. See [Static Channel Config](#static-channel-config). |

### Shadow Flags

Expand Down
36 changes: 36 additions & 0 deletions asil_client/Android.bp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2026 David Allison
// All Rights Reserved
// See LICENSE file for licensing information.

// The ASIL client core: C++17, the standard library and POSIX only. It is
// built without exceptions or RTTI.
cc_library_static {
name: "libsubspace_asil_client",
defaults: ["subspace_defaults"],
srcs: [
"channel_memory.cc",
"checksum.cc",
"client.cc",
"error.cc",
"publisher.cc",
"subscriber.cc",
],
include_dirs: ["external/subspace"],
cppflags: ["-fno-exceptions"],
rtti: false,
}

// The handshake with the server, using phaser messages in fixed buffers.
cc_library_static {
name: "libsubspace_asil_phaser_connection",
defaults: ["subspace_defaults"],
srcs: ["phaser_connection.cc"],
include_dirs: ["external/subspace"],
cppflags: ["-fno-exceptions"],
rtti: false,
static_libs: [
"libsubspace_asil_client",
"libsubspace_phaser",
],
export_static_lib_headers: ["libsubspace_phaser"],
}
Loading
Loading