Compare commits

..

4 Commits

Author SHA1 Message Date
charles 6821bd1cca Add performance results section to README 2026-05-04 14:53:49 -07:00
charles 4a6a09cff1 Add benchmark 2026-05-04 14:40:11 -07:00
charles b03ec9eba9 fix: tests 2026-05-04 13:46:05 -07:00
charles ca81a2d010 Update README 2026-05-04 13:45:18 -07:00
18 changed files with 4178 additions and 98 deletions
Generated
+445
View File
@@ -11,6 +11,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "1.0.0"
@@ -61,6 +67,57 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.6.1"
@@ -107,6 +164,79 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "1.0.1"
@@ -130,18 +260,85 @@ dependencies = [
"log",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.24"
@@ -166,6 +363,24 @@ dependencies = [
"syn",
]
[[package]]
name = "js-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.29"
@@ -178,12 +393,67 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -217,6 +487,26 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -251,10 +541,36 @@ name = "roto"
version = "0.1.0"
dependencies = [
"clap",
"criterion",
"env_logger",
"log",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -275,6 +591,25 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -292,6 +627,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -304,6 +649,80 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@@ -318,3 +737,29 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+7
View File
@@ -7,3 +7,10 @@ edition = "2024"
clap = { version = "4", features = ["derive"] }
log = "0.4"
env_logger = "0.11"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "hackers_bench"
harness = false
+245 -46
View File
@@ -1,29 +1,46 @@
# roto
Rust protos without the pointers.
Zero-allocation Rust protobuf reader and writer.
The codegen is different; we don't create data structures.
We mark what where each field is, and only read it when asked.
The binary blob is never decompressed by the library; it is your
job to figure out how to store the data if you need to access it
more than once.
## Overview
And building protos? You use a builder. We don't make some fancy
structure and give you a marshal function, nah. You give us a blob
to write data into, and we write what you tell us, no questions asked.
Instead of deserializing binary protobuf data into Rust structs, roto scans a message _once_ on
construction — recording the byte offset of each field — then reads fields on demand directly from
the original bytes. No heap allocation, no data copying, no full deserialization upfront.
### Design
Writing works the same way: you provide a fixed buffer and a builder writes fields directly into it,
returning a slice of the bytes written.
The `protoc` command generates a CodeGeneratorRequest message; `protoc-gen-roto` (from src/bin/protoc-gen-roto.rs)
reads this message from stdin, and generated a CodeGeneratorResponse, which it sends to stdout.
## Design
The generated files get written to disk by protoc; these should be included in the Rust code being developed to
use the protobuffers in question.
`protoc` generates a `CodeGeneratorRequest` message; `protoc-gen-roto` (in
`src/bin/protoc-gen-roto.rs`) reads this from stdin, generates Rust source files, and writes a
`CodeGeneratorResponse` to stdout. `protoc` then writes those `.rs` files to disk. The generated
files are included directly in the crate that uses the protobuffers.
### Sample usage
Sample usage:
```rust
/*
```
protoc -Iproto/ proto/hackers.proto --plugin=./target/debug/protoc-gen-roto --roto_out=src/
```
This will generate a file, src/hackers.rs.
## Generated code
For each protobuf message roto generates two types:
- **Reader struct** `MessageName<'a>` — borrows the original byte slice, zero-copy.
- **Builder struct** `MessageNameBuilder<'b>` — writes into a caller-provided `&mut [u8]`.
Nested message types are placed in a `pub mod message_name { ... }` module (snake_case of the
parent message name) within the same generated file.
## Sample usage
Given this proto definition:
```proto
message Hello {
string hello_world = 1;
message InnerWorld {
@@ -31,42 +48,224 @@ message Hello {
}
InnerWorld inner_world = 2;
}
*/
fn parse_proto(data: &[u8]) -> Result<String> {
// Scans the data, marks where each flag is as an offset
// into the proto.
let accessor = HelloProto::new(data)?;
// Load the hello world string; returns bytes, not
// a Rust string.
let hello_world = accessor.hello_world()?;
// Inspect a nested message; accessing inner_world scans it
// for flag locations and returns a similiar access struct
let inner_world = accessor.inner_world()?.thought()?;
format!("{} is about {}", hello_world, inner_world)
}
```
### Sample builder usage
### Reading
```rust
let mut buf = [0u8; 1024];
let mut builder = HelloProto::Builder::new(&mut buf)
.hello_world("some world")
.inner_world() // Returns an HelloProto::InnerWorld::Builder
.thought("some thought")
.done(); // returns the HelloProto::Builder
let bytes_written = builder.finish()?; // returns the number of bytes written to buffer
fn parse_proto(data: &[u8]) -> roto::Result<String> {
// Scan the data once, recording field offsets
let hello = Hello::new(data)?;
// String fields return &str borrowed from the original bytes (zero-copy)
let hello_world: &str = hello.hello_world()?;
// Nested message fields return &[u8]; construct the nested reader from those bytes
let inner_bytes: &[u8] = hello.inner_world()?;
let inner_world = hello::InnerWorld::new(inner_bytes)?;
let thought: &str = inner_world.thought()?;
Ok(format!("{} is about {}", hello_world, thought))
}
```
### High level design
Fields absent from the binary data return `Err(roto::RotoError::FieldNotFound)`.
The runtime library offers an iterator over the fields in a message, using the protobuf wire format provide
objects of flag and type. Codegen creates a 'wrapper' that iterates over the message, and records the
byte offset of each element. Helper methods in the wrapper give the user access to the name fields,
casted to the appropriate data type.
### Writing
### Literature
Nested messages must be serialized into a scratch buffer first, then embedded as raw bytes in the
outer builder.
```rust
fn build_proto(buf: &mut [u8]) -> roto::Result<&[u8]> {
// Serialize the inner message first
let mut inner_buf = [0u8; 256];
let inner_bytes = hello::InnerWorldBuilder::builder(&mut inner_buf)
.thought("some thought")?
.finish()?;
// Build the outer message, embedding the serialized inner bytes
HelloBuilder::builder(buf)
.hello_world("some world")?
.inner_world(inner_bytes)?
.finish() // returns Result<&'b mut [u8]> — the written portion of buf
}
```
Builder methods consume `self` and return `Result<Self>`, enabling `?`-based chaining.
`finish()` returns `Result<&'b mut [u8]>` — a slice of the portion of the buffer that was written.
### Repeated fields
Repeated fields return a `RepeatedFieldIterator<'a>`. Each item yields `Result<(&[u8], WireType)>`.
```rust
let hello = Hello::new(data)?;
for item in hello.tags() {
let (value_bytes, _wire_type) = item?;
// decode value_bytes according to the expected wire type
}
```
## Runtime API
The core runtime in `src/lib.rs` provides:
- `ProtoAccessor<'a>` — scans a message's fields and reads values at recorded offsets.
- `ProtoBuilder<'a>` — writes fields into a provided `&mut [u8]` buffer.
- `FieldIterator<'a>` / `RepeatedFieldIterator<'a>` — iterators over fields and repeated fields.
- `Tag`, `WireType` — protobuf encoding primitives.
- `read_varint`, `write_varint`, `skip_value` — low-level wire-format helpers.
- `RotoError`, `Result<T>` — error type and alias.
## High-level design
On construction (`MessageName::new(data)`), the generated reader struct iterates the binary once
using `FieldIterator` and records the byte offset of each field's tag. Subsequent field accesses
call `ProtoAccessor::get_value_at(offset)` — no re-scanning. For repeated fields, the start and
end offsets of the field range are recorded to bound iteration efficiently.
## Benchmarks
Two benchmark suites share the same binary data files and the same four
measurement groups:
| Group | What is timed |
| --------------- | ------------------------------------------------------- |
| `shallow_parse` | Become ready to read any field (one scan / full decode) |
| `deep_parse` | Walk the full tree: Campaign → Operations → Hackers |
| `field_access` | Read individual fields on an already-parsed message |
| `iterate` | Count top-level and nested repeated fields |
### 1 — Generate the shared data files (do this once)
Data files are written to `data/bench/`.
```sh
cargo run --release --bin gen_bench_data -- --preset tiny
cargo run --release --bin gen_bench_data -- --preset small
cargo run --release --bin gen_bench_data -- --preset medium
cargo run --release --bin gen_bench_data -- --preset large
```
For even larger inputs use `--preset huge` (~500 MB) or set the knobs
directly:
```sh
# ~50 MB: 500 operations × 100 KB stolen_data each
cargo run --release --bin gen_bench_data -- --ops 500 --stolen-kb 100 --output data/bench/50mb.pb
```
### 2 — Rust benchmark (criterion)
```sh
cargo bench --bench hackers_bench
```
HTML reports are written to `target/criterion/`. Run a single group:
```sh
cargo bench --bench hackers_bench -- shallow_parse
```
### 3 — C / upb benchmark
Requires protobuf ≥ 21 with `protoc-gen-upb` (ships with modern `protoc`).
```sh
cd upb_test
make # compiles hackers_bench from the pre-generated upb files
./hackers_bench
```
To regenerate the upb C files from `proto/hackers.proto`:
```sh
cd upb_test && make regen
```
### 4 — Results
Measured on Linux x86-64 with the four standard presets. Rust times are
criterion medians; C/upb times are the custom runner's mean over ≥ 0.5 s.
#### `shallow_parse` — cost to become ready to read any field
| Size | Bytes | roto (ns) | upb (ns) | roto speedup |
| ------ | ----------: | --------: | -----------: | -----------: |
| tiny | 588 | 32.7 | 606.2 | **18.5×** |
| small | 20,265 | 182.9 | 22,619.2 | **123.7×** |
| medium | 2,071,053 | 16,632.0 | 5,346,977.2 | **321×** |
| large | 102,608,384 | 1,618.6 | 41,132,079.7 | **25,411×** |
> roto's cost is O(number of top-level fields): it records field offsets by
> jumping past nested blobs using their length prefixes. upb fully decodes the
> entire tree — including all nested messages and raw byte payloads — into
> arena-allocated structs.
#### `deep_parse` — parse + walk Campaign → Operations → every Hacker handle
| Size | Bytes | roto (ns) | upb (ns) | roto speedup |
| ------ | --------: | ----------: | ----------: | -----------: |
| tiny | 588 | 385.3 | 596.8 | **1.55×** |
| small | 20,265 | 13,374.0 | 22,321.6 | **1.67×** |
| medium | 2,071,053 | 1,454,400.0 | 4,227,384.3 | **2.91×** |
> roto pays one extra `::new()` scan per nesting level; upb's walk is pure
> pointer-chasing because everything was decoded upfront. roto is still
> faster overall because its per-level scans cost less than upb's full decode.
#### `field_access` — individual field reads on a pre-parsed message (`small` preset)
| Field | roto (ns) | upb (ns) | upb speedup |
| ------------------------------ | --------: | -------: | ----------: |
| `campaign::name` | 14.3 | 1.11 | **12.9×** |
| `campaign::total_bytes_stolen` | 7.1 | 1.74 | **4.1×** |
| `operation::codename` | 13.8 | 1.76 | **7.8×** |
| `operation::timestamp` | 9.7 | 1.40 | **6.9×** |
| `operation::successful` | 7.0 | 1.13 | **6.1×** |
| `hacker::handle` | 14.4 | 1.56 | **9.2×** |
| `hacker::skill_level` (f32) | 7.7 | 1.76 | **4.4×** |
| `hacker::is_elite` (bool) | 7.5 | 1.14 | **6.6×** |
| `worm::polymorphic` (bool) | 7.5 | 1.76 | **4.2×** |
| `worm::payload` (bytes) | 16.6 | 1.75 | **9.5×** |
> After parsing, upb field reads are direct struct-member lookups (~12 ns).
> roto re-decodes the value at its pre-recorded byte offset on every call
> (~717 ns). This is the one area where upb holds a clear advantage.
#### `iterate` — count repeated fields (parse included in every iteration)
| Benchmark | Size | roto (ns) | upb (ns) | roto speedup |
| ------------------ | ------ | --------: | ----------: | -----------: |
| `count_operations` | tiny | 50.0 | 600.2 | **12.0×** |
| `count_operations` | small | 393.7 | 22,702.9 | **57.7×** |
| `count_operations` | medium | 36,628.0 | 4,193,874.0 | **114.5×** |
| `count_all_crew` | tiny | 235.3 | 610.2 | **2.6×** |
| `count_all_crew` | small | 4,369.5 | 23,109.0 | **5.3×** |
| `count_all_crew` | medium | 444,930.0 | 4,151,181.5 | **9.3×** |
> `count_operations` includes parsing; upb's O(1) array-length read is
> dominated by its full-decode cost, so roto wins by the same margin as
> `shallow_parse`. `count_all_crew` also parses each `Operation` sub-message;
> roto's per-level scans remain cheaper than upb's full decode.
### Interpreting the comparison
The two libraries have fundamentally different models:
- **roto `shallow_parse`** does one linear scan recording byte offsets — no
allocation, no field decoding. Subsequent field reads decode on demand at
the stored offset.
- **upb `Campaign_parse`** fully decodes the entire message tree into
arena-allocated structs upfront. Subsequent field reads are direct struct
member lookups (~1 ns).
The result: roto's parse is faster and allocation-free; upb's field access
after parsing is faster. For workloads that read every field the costs
invert; for workloads that read a handful of fields from large messages roto
wins.
## Literature
https://protobuf.dev/programming-guides/encoding/
+215
View File
@@ -0,0 +1,215 @@
//! Benchmark suite for roto — themed after the 1995 film *Hackers*.
//!
//! Proto schema: `proto/hackers.proto`
//! Generated types: `src/hackers.rs` (via `protoc-gen-roto`)
//!
//! # Setup
//!
//! Generate the data files once before running benchmarks:
//!
//! ```sh
//! cargo run --release --bin gen_bench_data -- --preset tiny
//! cargo run --release --bin gen_bench_data -- --preset small
//! cargo run --release --bin gen_bench_data -- --preset medium
//! cargo run --release --bin gen_bench_data -- --preset large
//! ```
//!
//! Then run:
//!
//! ```sh
//! cargo bench --bench hackers_bench
//! ```
//!
//! Benchmark groups:
//! - `shallow_parse` — `Campaign::new(data)`, one scan of the whole blob
//! - `deep_parse` — Campaign → Operations → Hackers (a `::new()` per level)
//! - `field_access` — individual field reads on pre-parsed messages (O(1))
//! - `iterate` — counting repeated fields at different nesting depths
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use roto::hackers::{Campaign, Hacker, Operation, Worm};
use std::hint::black_box;
// =============================================================================
// Data loading
// =============================================================================
/// Load a pre-generated data file from `data/bench/<name>.pb`.
/// Returns `None` (and prints a hint) if the file does not exist.
fn load(name: &str) -> Option<Vec<u8>> {
let path = format!("data/bench/{name}.pb");
match std::fs::read(&path) {
Ok(data) => Some(data),
Err(_) => {
eprintln!(
"[skip] {path} not found — \
run `cargo run --release --bin gen_bench_data -- --preset {name}` first"
);
None
}
}
}
// =============================================================================
// Benchmarks
// =============================================================================
/// `Campaign::new()` — one linear scan to record field offsets, no allocation.
/// Throughput reported in MB/s so different sizes are directly comparable.
fn bench_shallow_parse(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
("large", load("large")),
];
let mut group = c.benchmark_group("shallow_parse");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(BenchmarkId::new("Campaign::new", label), data, |b, data| {
b.iter(|| Campaign::new(black_box(data)).unwrap())
});
}
group.finish();
}
/// Walk every level of the tree: Campaign → Operations → Hackers.
/// Each `::new()` is an additional linear scan of that sub-message's bytes.
fn bench_deep_parse(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
];
let mut group = c.benchmark_group("deep_parse");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(
BenchmarkId::new("Campaign+Ops+Hackers", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
let mut hacker_count = 0usize;
for op_res in campaign.operations() {
let (op_bytes, _) = op_res.unwrap();
let op = Operation::new(op_bytes).unwrap();
for crew_res in op.crew() {
let (hacker_bytes, _) = crew_res.unwrap();
let hacker = Hacker::new(hacker_bytes).unwrap();
let _ = black_box(hacker.handle().unwrap());
hacker_count += 1;
}
}
black_box(hacker_count)
})
},
);
}
group.finish();
}
/// O(1) field accesses on pre-parsed messages.
/// Measures only the decode step at a known offset — not the scan.
fn bench_field_access(c: &mut Criterion) {
let Some(data) = load("small") else { return };
let campaign = Campaign::new(&data).unwrap();
let (op_bytes, _) = campaign.operations().next().unwrap().unwrap();
let op = Operation::new(op_bytes).unwrap();
let (hacker_bytes, _) = op.crew().next().unwrap().unwrap();
let hacker = Hacker::new(hacker_bytes).unwrap();
let worm = Worm::new(op.worm().unwrap()).unwrap();
let mut group = c.benchmark_group("field_access");
group.bench_function("campaign::name", |b| {
b.iter(|| black_box(campaign.name().unwrap()))
});
group.bench_function("campaign::total_bytes_stolen", |b| {
b.iter(|| black_box(campaign.total_bytes_stolen().unwrap()))
});
group.bench_function("operation::codename", |b| {
b.iter(|| black_box(op.codename().unwrap()))
});
group.bench_function("operation::timestamp", |b| {
b.iter(|| black_box(op.timestamp().unwrap()))
});
group.bench_function("operation::successful", |b| {
b.iter(|| black_box(op.successful().unwrap()))
});
group.bench_function("hacker::handle", |b| {
b.iter(|| black_box(hacker.handle().unwrap()))
});
group.bench_function("hacker::skill_level (f32)", |b| {
b.iter(|| black_box(hacker.skill_level().unwrap()))
});
group.bench_function("hacker::is_elite (bool)", |b| {
b.iter(|| black_box(hacker.is_elite().unwrap()))
});
group.bench_function("worm::polymorphic (bool)", |b| {
b.iter(|| black_box(worm.polymorphic().unwrap()))
});
group.bench_function("worm::payload (bytes)", |b| {
b.iter(|| black_box(worm.payload().unwrap()))
});
group.finish();
}
/// Iterate repeated fields at different depths.
fn bench_iterate(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
];
let mut group = c.benchmark_group("iterate");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
// Top-level repeated field — walk Operation blobs, no inner parse.
group.bench_with_input(
BenchmarkId::new("count_operations", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
black_box(campaign.operations().count())
})
},
);
// Nested repeated field — parse each Operation to reach its crew.
group.bench_with_input(
BenchmarkId::new("count_all_crew", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
let mut n = 0usize;
for op_res in campaign.operations() {
let (op_bytes, _) = op_res.unwrap();
n += Operation::new(op_bytes).unwrap().crew().count();
}
black_box(n)
})
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_shallow_parse,
bench_deep_parse,
bench_field_access,
bench_iterate
);
criterion_main!(benches);
+1
View File
@@ -0,0 +1 @@
bench/
+56
View File
@@ -0,0 +1,56 @@
syntax = "proto3";
message Tool {
string name = 1;
string version = 2;
bytes payload = 3;
bool is_active = 4;
int32 exploit_count = 5;
}
message Connection {
string host = 1;
int32 port = 2;
bool encrypted = 3;
int64 bandwidth_bps = 4;
bytes session_key = 5;
}
message Hacker {
string handle = 1;
string real_name = 2;
int32 age = 3;
float skill_level = 4; // Fixed32
bool is_elite = 5;
int64 crew_id = 6;
repeated string exploits = 7;
repeated Tool tools = 8;
Connection active_connection = 9;
}
message Worm {
string name = 1;
int32 variant = 2;
int64 size_bytes = 3;
bytes payload = 4;
bool polymorphic = 5;
repeated string targets = 6;
}
message Operation {
string codename = 1;
string target_corp = 2;
int64 timestamp = 3;
bool successful = 4;
bytes stolen_data = 5;
repeated Hacker crew = 6;
Worm worm = 7;
repeated string log_entries = 8;
int32 severity = 9;
}
message Campaign {
string name = 1;
repeated Operation operations = 2;
int64 total_bytes_stolen = 3;
}
+477
View File
@@ -0,0 +1,477 @@
//! Generates Hackers-themed benchmark proto binaries using the roto builder API.
//!
//! Run this once to create the data files that `hackers_bench` loads:
//!
//! ```sh
//! cargo run --release --bin gen_bench_data -- --preset tiny
//! cargo run --release --bin gen_bench_data -- --preset small
//! cargo run --release --bin gen_bench_data -- --preset medium
//! cargo run --release --bin gen_bench_data -- --preset large
//! cargo run --release --bin gen_bench_data -- --preset huge
//!
//! # Custom: ~50 MB — 500 ops × 100 KB stolen_data each
//! cargo run --release --bin gen_bench_data -- \
//! --ops 500 --stolen-kb 100 --output data/bench/50mb.pb
//! ```
//!
//! Files land in `data/bench/` by default.
use clap::Parser;
use roto::hackers::{
CampaignBuilder, ConnectionBuilder, HackerBuilder, OperationBuilder, ToolBuilder, WormBuilder,
};
use std::io::{self, Write};
use std::path::Path;
// =============================================================================
// CLI
// =============================================================================
#[derive(Parser)]
#[command(
name = "gen_bench_data",
about = "Generate Hackers-themed proto binaries for benchmarks"
)]
struct Args {
/// Output file. Defaults to data/bench/<preset>.pb when --preset is used.
#[arg(short, long)]
output: Option<String>,
/// Named size preset: tiny | small | medium | large | huge
#[arg(short, long)]
preset: Option<String>,
/// Number of Operation messages.
#[arg(long, default_value_t = 100)]
ops: usize,
/// Kilobytes of random stolen_data padding per Operation.
#[arg(long, default_value_t = 0)]
stolen_kb: usize,
/// Hacker crew members per Operation.
#[arg(long, default_value_t = 3)]
crew: usize,
/// RNG seed.
#[arg(long, default_value_t = 42)]
seed: u64,
}
// =============================================================================
// Minimal xorshift64 RNG
// =============================================================================
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() as usize) % n
}
fn range(&mut self, lo: u64, hi: u64) -> u64 {
lo + self.next() % (hi - lo)
}
fn bool(&mut self) -> bool {
self.next() & 1 == 0
}
fn pick<'a, T>(&mut self, s: &'a [T]) -> &'a T {
&s[self.below(s.len())]
}
fn bytes(&mut self, n: usize) -> Vec<u8> {
(0..n).map(|_| self.next() as u8).collect()
}
}
// =============================================================================
// Flavour text
// =============================================================================
const HANDLES: &[&str] = &[
"Zero Cool",
"Acid Burn",
"Phantom Phreak",
"Cereal Killer",
"Lord Nikon",
"The Plague",
"Crash Override",
];
const REAL_NAMES: &[&str] = &[
"Dade Murphy",
"Kate Libby",
"Richard Gill",
"Emmanuel Goldstein",
"Paul Cook",
"Eugene Belford",
];
const EXPLOITS: &[&str] = &[
"buffer overflow",
"stack smash",
"heap spray",
"race condition",
"SQL injection",
"CSRF",
"XSS",
"RCE",
"privesc",
"kernel panic",
];
const TOOL_NAMES: &[&str] = &[
"nmap",
"metasploit",
"netcat",
"tcpdump",
"Wireshark",
"sqlmap",
"Burp Suite",
"hashcat",
"john",
];
const CORPS: &[&str] = &[
"ELLINGSON MINERAL",
"Cyberdelia",
"The Gibson",
"Prism BBS",
"Elite BBS",
"CRT Systems",
];
const LOG_LINES: &[&str] = &[
"Hack the planet!",
"Mess with the best, die like the rest.",
"I'm in.",
"They're tracing us.",
"It's a Unix system! I know this!",
"You are elite.",
"Garbage file accessed.",
];
const WORM_TARGETS: &[&str] = &[
"ELLINGSON MINERAL",
"Cyberdelia",
"The Gibson",
"Prism",
"CRT BBS",
];
const OP_NAMES: &[&str] = &[
"OPERATION HACK THE PLANET",
"GIBSON BREACH",
"ELLINGSON STING",
"WORM UNLEASHED",
"PHANTOM ACCESS",
"DA VINCI",
];
// =============================================================================
// Message builders using the generated roto::hackers API
// =============================================================================
fn gen_tool(rng: &mut Rng) -> Vec<u8> {
let payload_n = rng.range(8, 64) as usize;
let payload = rng.bytes(payload_n);
let version = format!("{}.{}", rng.range(1, 9), rng.range(0, 99));
let mut buf = vec![0u8; 512];
ToolBuilder::builder(&mut buf)
.name(*rng.pick(TOOL_NAMES))
.unwrap()
.version(&version)
.unwrap()
.payload(&payload)
.unwrap()
.is_active(rng.bool() as u64)
.unwrap()
.exploit_count(rng.range(0, 50) as i32)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_connection(rng: &mut Rng) -> Vec<u8> {
let host = format!("192.168.{}.{}", rng.range(1, 254), rng.range(1, 254));
let session_key = rng.bytes(32);
let mut buf = vec![0u8; 256];
ConnectionBuilder::builder(&mut buf)
.host(&host)
.unwrap()
.port(rng.range(1024, 65535) as i32)
.unwrap()
.encrypted(rng.bool() as u64)
.unwrap()
.bandwidth_bps(rng.range(1200, 1_000_000_000))
.unwrap()
.session_key(&session_key)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_hacker(rng: &mut Rng) -> Vec<u8> {
let tools: Vec<Vec<u8>> = (0..rng.range(1, 4)).map(|_| gen_tool(rng)).collect();
let connection = gen_connection(rng);
// Float (skill_level) is written as raw bytes by the generated builder
let skill_bits = (rng.range(10, 100) as f32 / 10.0).to_bits().to_le_bytes();
let handle = *rng.pick(HANDLES);
let real_name = *rng.pick(REAL_NAMES);
let n_exploits = rng.range(2, 5) as usize;
let exploits: Vec<&str> = (0..n_exploits).map(|_| *rng.pick(EXPLOITS)).collect();
let crew_id = rng.next();
let mut buf = vec![0u8; 8 * 1024];
let mut b = HackerBuilder::builder(&mut buf)
.handle(handle)
.unwrap()
.real_name(real_name)
.unwrap()
.age(rng.range(16, 35) as i32)
.unwrap()
.skill_level(&skill_bits)
.unwrap()
.is_elite(rng.bool() as u64)
.unwrap()
.crew_id(crew_id)
.unwrap();
for e in &exploits {
b = b.exploits(e).unwrap();
}
for t in &tools {
b = b.tools(t).unwrap();
}
b.active_connection(&connection)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_worm(rng: &mut Rng) -> Vec<u8> {
let payload = rng.bytes(64);
let name = format!("da_vinci.{}", rng.range(1, 99));
let n_targets = rng.range(1, 4) as usize;
let targets: Vec<&str> = (0..n_targets).map(|_| *rng.pick(WORM_TARGETS)).collect();
let mut buf = vec![0u8; 1024];
let mut b = WormBuilder::builder(&mut buf)
.name(&name)
.unwrap()
.variant(rng.range(1, 5) as i32)
.unwrap()
.size_bytes(rng.range(1024, 10_000_000))
.unwrap()
.payload(&payload)
.unwrap()
.polymorphic(rng.bool() as u64)
.unwrap();
for t in &targets {
b = b.targets(t).unwrap();
}
b.finish().unwrap().to_vec()
}
fn gen_operation(rng: &mut Rng, crew_count: usize, stolen_bytes: usize) -> Vec<u8> {
let crew: Vec<Vec<u8>> = (0..crew_count).map(|_| gen_hacker(rng)).collect();
let worm = gen_worm(rng);
let stolen = if stolen_bytes > 0 {
rng.bytes(stolen_bytes)
} else {
vec![]
};
let codename = *rng.pick(OP_NAMES);
let corp = *rng.pick(CORPS);
let ts = 810_000_000u64 + rng.range(0, 10_000_000);
let n_logs = rng.range(2, 6) as usize;
let logs: Vec<&str> = (0..n_logs).map(|_| *rng.pick(LOG_LINES)).collect();
// Operation buffer: crew + worm + stolen_data + small overhead
let crew_size: usize = crew.iter().map(|h| h.len() + 5).sum();
let buf_size = crew_size + worm.len() + stolen_bytes + 1024;
let mut buf = vec![0u8; buf_size];
let mut b = OperationBuilder::builder(&mut buf)
.codename(codename)
.unwrap()
.target_corp(corp)
.unwrap()
.timestamp(ts)
.unwrap()
.successful(rng.bool() as u64)
.unwrap();
if stolen_bytes > 0 {
b = b.stolen_data(&stolen).unwrap();
}
for h in &crew {
b = b.crew(h).unwrap();
}
b = b.worm(&worm).unwrap();
for l in &logs {
b = b.log_entries(l).unwrap();
}
b.severity(rng.range(1, 10) as i32)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_campaign(
rng: &mut Rng,
op_count: usize,
crew_per_op: usize,
stolen_bytes: usize,
) -> Vec<u8> {
let ops: Vec<Vec<u8>> = (0..op_count)
.map(|i| {
if i > 0 && i % 100 == 0 {
eprintln!(
" {i}/{op_count} operations ({:.1} MB in ops so far)…",
i * (stolen_bytes + 10_000) / 1_000_000
);
}
gen_operation(rng, crew_per_op, stolen_bytes)
})
.collect();
// Pre-compute campaign buffer size: for each op the wire encoding is
// tag(1B) + varint_length(1-5B) + op_bytes
let ops_wire_size: usize = ops
.iter()
.map(|o| 1 + varint_len(o.len() as u64) + o.len())
.sum();
let mut buf = vec![0u8; ops_wire_size + 64];
let mut b = CampaignBuilder::builder(&mut buf)
.name("HACK THE PLANET CAMPAIGN")
.unwrap();
for op in &ops {
b = b.operations(op).unwrap();
}
b.total_bytes_stolen((stolen_bytes * op_count) as u64)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
/// Number of bytes needed to encode `v` as a varint.
fn varint_len(mut v: u64) -> usize {
let mut n = 1usize;
while v >= 128 {
v >>= 7;
n += 1;
}
n
}
// =============================================================================
// Preset table
// =============================================================================
struct Preset {
ops: usize,
crew: usize,
stolen_kb: usize,
}
fn resolve(name: &str) -> Option<Preset> {
match name {
// ops crew stolen_kb approx size
"tiny" => Some(Preset {
ops: 1,
crew: 1,
stolen_kb: 0,
}), // ~400 B
"small" => Some(Preset {
ops: 20,
crew: 3,
stolen_kb: 0,
}), // ~25 KB
"medium" => Some(Preset {
ops: 2_000,
crew: 3,
stolen_kb: 0,
}), // ~2 MB
"large" => Some(Preset {
ops: 200,
crew: 3,
stolen_kb: 500,
}), // ~100 MB
"huge" => Some(Preset {
ops: 1_000,
crew: 3,
stolen_kb: 500,
}), // ~500 MB
_ => None,
}
}
// =============================================================================
// main
// =============================================================================
fn main() {
let mut args = Args::parse();
if let Some(ref name) = args.preset.clone() {
match resolve(name) {
Some(p) => {
args.ops = p.ops;
args.crew = p.crew;
args.stolen_kb = p.stolen_kb;
}
None => {
eprintln!("Unknown preset '{name}'. Valid: tiny, small, medium, large, huge");
std::process::exit(1);
}
}
}
let stolen_bytes = args.stolen_kb * 1024;
let approx_mb = args.ops * (700 + args.crew * 300 + stolen_bytes) / 1_000_000;
eprintln!(
"Generating: {} ops × {} crew, {} KB stolen_data each → ~{} MB",
args.ops, args.crew, args.stolen_kb, approx_mb
);
let mut rng = Rng::new(args.seed);
let data = gen_campaign(&mut rng, args.ops, args.crew, stolen_bytes);
eprintln!(
"Generated {} bytes ({:.2} MB)",
data.len(),
data.len() as f64 / 1_000_000.0
);
// Default output: data/bench/<preset>.pb, or stdout if no output and no preset
let out_path = args
.output
.clone()
.or_else(|| args.preset.as_ref().map(|p| format!("data/bench/{}.pb", p)));
match out_path {
Some(ref path) => {
if let Some(parent) = Path::new(path).parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).unwrap_or_else(|e| eprintln!("Warning: {e}"));
}
}
std::fs::write(path, &data).unwrap_or_else(|e| {
eprintln!("Error writing {path}: {e}");
std::process::exit(1);
});
eprintln!("Saved to {path}");
}
None => {
io::stdout().write_all(&data).unwrap_or_else(|e| {
eprintln!("Error: {e}");
std::process::exit(1);
});
}
}
}
+95 -41
View File
@@ -1,9 +1,10 @@
use crate::google::protobuf::descriptor::{
DescriptorProto, EnumDescriptorProto, FileDescriptorProto, FieldDescriptorProto, FileDescriptorSet
};
use crate::ProtoAccessor;
use std::str;
use crate::google::protobuf::descriptor::{
DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, FileDescriptorProto,
FileDescriptorSet,
};
use std::collections::{HashMap, HashSet};
use std::str;
pub fn to_pascal_case(s: &str) -> String {
s.split('_')
@@ -52,7 +53,7 @@ fn map_type_to_rust_accessor(field_type: i32, label: i32) -> (String, String) {
), // TYPE_DOUBLE
2 => (
"f32".to_string(),
"f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?)".to_string(),
"Ok(f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?))".to_string(),
), // TYPE_FLOAT
3 | 5 | 15 | 17 => (
"i32".to_string(),
@@ -91,7 +92,8 @@ fn write_enum(enum_proto: &EnumDescriptorProto, output: &mut String) {
let mut zero_variant_name = None;
while let Some(val_res) = values.next() {
let (val_data, _) = val_res.expect("Failed to iterate enum");
let accessor = ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto");
let accessor =
ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto");
let (name_bytes, _) = accessor.get_value(1).expect("Enum value name missing");
let name = str::from_utf8(name_bytes).expect("Enum value name invalid utf8");
let (num_bytes, _) = accessor.get_value(2).expect("Enum value number missing");
@@ -119,16 +121,26 @@ fn write_enum(enum_proto: &EnumDescriptorProto, output: &mut String) {
let mut values = enum_proto.value();
while let Some(val_res) = values.next() {
let (val_data, _) = val_res.expect("Failed to read enum value");
let accessor = ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto");
let accessor =
ProtoAccessor::new(val_data).expect("Failed to parse EnumValueDescriptorProto");
let (name_bytes, _) = accessor.get_value(1).expect("Enum value name missing");
let name = str::from_utf8(name_bytes).expect("Enum value name invalid utf8");
let (num_bytes, _) = accessor.get_value(2).expect("Enum value number missing");
let (num, _) = crate::read_varint(num_bytes).expect("Enum value number invalid varint");
output.push_str(&format!(" {} => {}::{},\n", num, enum_name, to_pascal_case(name)));
output.push_str(&format!(
" {} => {}::{},\n",
num,
enum_name,
to_pascal_case(name)
));
}
output.push_str(&format!(" _ => {}::{},\n", enum_name, zero_variant_name.as_ref().unwrap()));
output.push_str(&format!(
" _ => {}::{},\n",
enum_name,
zero_variant_name.as_ref().unwrap()
));
output.push_str(" }\n }\n}\n\n");
}
@@ -138,7 +150,8 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
let mut fields_info = Vec::new();
for field_res in msg_proto.field() {
let (field_data, _) = field_res.expect("Failed to iterate field");
let field_proto = FieldDescriptorProto::new(field_data).expect("Failed to parse FieldDescriptorProto");
let field_proto =
FieldDescriptorProto::new(field_data).expect("Failed to parse FieldDescriptorProto");
let field_name = field_proto.name().unwrap();
let tag = field_proto.number().unwrap();
@@ -148,13 +161,8 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
fields_info.push((field_name.to_string(), tag, f_type, f_label));
}
output.push_str(&format!(
"pub struct {}<'a> {{\n",
msg_name
));
if !fields_info.is_empty() {
output.push_str(" accessor: crate::ProtoAccessor<'a>,\n");
}
output.push_str(&format!("pub struct {}<'a> {{\n", msg_name));
output.push_str(" accessor: crate::ProtoAccessor<'a>,\n");
for (field_name, _tag, _f_type, f_label) in &fields_info {
if *f_label == 3 {
@@ -168,9 +176,8 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
output.push_str(&format!("impl<'a> {}<'a> {{\n", msg_name));
output.push_str(" pub fn new(data: &'a [u8]) -> crate::Result<Self> {\n");
output.push_str(" let accessor = crate::ProtoAccessor::new(data)?;\n");
if !fields_info.is_empty() {
output.push_str(" let accessor = crate::ProtoAccessor::new(data)?;\n");
for (name, _, _, label) in &fields_info {
if *label == 3 {
output.push_str(&format!(" let mut {}_start = None;\n", name));
@@ -186,22 +193,24 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
for (name, tag, _, label) in &fields_info {
if *label == 3 {
output.push_str(&format!(" if tag.field_number == {} {{\n", tag));
output.push_str(&format!(" if {}_start.is_none() {{ {}_start = Some(offset); }}\n", name, name));
output.push_str(&format!(
" if {}_start.is_none() {{ {}_start = Some(offset); }}\n",
name, name
));
output.push_str(&format!(" {}_end = Some(offset);\n", name));
output.push_str(" }\n");
} else {
output.push_str(&format!(" if tag.field_number == {} {{ {}_offset = Some(offset); }}\n", tag, name));
output.push_str(&format!(
" if tag.field_number == {} {{ {}_offset = Some(offset); }}\n",
tag, name
));
}
}
output.push_str(" }\n\n");
} else {
output.push_str(" let _ = crate::ProtoAccessor::new(data)?;\n");
}
output.push_str(" Ok(Self {\n");
if !fields_info.is_empty() {
output.push_str(" accessor,\n");
}
output.push_str(" accessor,\n");
for (name, _, _, label) in &fields_info {
if *label == 3 {
output.push_str(&format!("{}_start, {}_end,\n", name, name));
@@ -213,17 +222,36 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
for (field_name, tag, f_type, f_label) in fields_info {
let (rust_type, logic) = map_type_to_rust_accessor(f_type, f_label);
let safe_name = if field_name == "type" { format!("r#{}", field_name) } else { field_name.clone() };
let safe_name = if field_name == "type" {
format!("r#{}", field_name)
} else {
field_name.clone()
};
if f_label == 3 {
output.push_str(&format!(" pub fn {}(&self) -> {} {{\n", safe_name, rust_type));
output.push_str(&format!(" match (self.{}_start, self.{}_end) {{\n", field_name, field_name));
output.push_str(&format!(
" pub fn {}(&self) -> {} {{\n",
safe_name, rust_type
));
output.push_str(&format!(
" match (self.{}_start, self.{}_end) {{\n",
field_name, field_name
));
output.push_str(&format!(" (Some(start), Some(end)) => self.accessor.iter_repeated_range({}, start, end),\n", tag));
output.push_str(&format!(" _ => self.accessor.iter_repeated({}),\n", tag));
output.push_str(&format!(
" _ => self.accessor.iter_repeated({}),\n",
tag
));
output.push_str(" }\n }\n\n");
} else {
output.push_str(&format!(" pub fn {}(&self) -> crate::Result<{}> {{\n", safe_name, rust_type));
output.push_str(&format!(" let offset = self.{}_offset.ok_or(crate::RotoError::FieldNotFound)?;\n", field_name));
output.push_str(&format!(
" pub fn {}(&self) -> crate::Result<{}> {{\n",
safe_name, rust_type
));
output.push_str(&format!(
" let offset = self.{}_offset.ok_or(crate::RotoError::FieldNotFound)?;\n",
field_name
));
output.push_str(" let (bytes, _) = self.accessor.get_value_at(offset)?;\n");
output.push_str(&format!(" {}\n", logic));
output.push_str(" }\n\n");
@@ -243,9 +271,14 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
for field_res in msg_proto.field() {
let (field_data, _) = field_res.expect("Failed to iterate field");
let field_proto = FieldDescriptorProto::new(field_data).expect("Failed to parse FieldDescriptorProto");
let field_proto =
FieldDescriptorProto::new(field_data).expect("Failed to parse FieldDescriptorProto");
let field_name = field_proto.name().unwrap();
let safe_name = if field_name == "type" { format!("r#{}", field_name) } else { field_name.to_string() };
let safe_name = if field_name == "type" {
format!("r#{}", field_name)
} else {
field_name.to_string()
};
let tag = field_proto.number().unwrap();
let f_type = field_proto.r#type().unwrap() as i32;
let (rust_type, method) = map_type_to_rust_builder(f_type);
@@ -258,21 +291,32 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) {
let mut nested_enums = Vec::new();
for e_res in msg_proto.enum_type() {
if let Ok((e, _)) = e_res { nested_enums.push(e); }
if let Ok((e, _)) = e_res {
nested_enums.push(e);
}
}
let mut nested_msgs = Vec::new();
for m_res in msg_proto.nested_type() {
if let Ok((m, _)) = m_res { nested_msgs.push(m); }
if let Ok((m, _)) = m_res {
nested_msgs.push(m);
}
}
if !nested_enums.is_empty() || !nested_msgs.is_empty() {
let mod_name = to_snake_case(msg_proto.name().unwrap());
output.push_str(&format!("pub mod {} {{\n", mod_name));
for e_data in nested_enums {
write_enum(&EnumDescriptorProto::new(e_data).expect("Failed to parse nested EnumDescriptorProto"), output);
write_enum(
&EnumDescriptorProto::new(e_data)
.expect("Failed to parse nested EnumDescriptorProto"),
output,
);
}
for m_data in nested_msgs {
write_message(&DescriptorProto::new(m_data).expect("Failed to parse nested DescriptorProto"), output);
write_message(
&DescriptorProto::new(m_data).expect("Failed to parse nested DescriptorProto"),
output,
);
}
output.push_str("}\n\n");
}
@@ -299,7 +343,8 @@ pub fn generate_rust_code(
for file_res in set.file() {
let (file_data, _) = file_res.expect("Failed to iterate file");
let file_proto = FileDescriptorProto::new(file_data).expect("Failed to parse FileDescriptorProto");
let file_proto =
FileDescriptorProto::new(file_data).expect("Failed to parse FileDescriptorProto");
let proto_name = file_proto.name().expect("File proto name missing");
if let Some(filter) = files_to_generate {
@@ -311,6 +356,7 @@ pub fn generate_rust_code(
let rust_file_name = format!("{}.rs", proto_name.replace(".proto", ""));
let mut output = String::new();
output.push_str("// @generated by protoc-gen-roto — do not edit\n\n");
output.push_str("use crate::{ProtoAccessor, ProtoBuilder, Result, RotoError, read_varint, RepeatedFieldIterator};\n");
output.push_str("use std::str;\n\n");
@@ -325,13 +371,19 @@ pub fn generate_rust_code(
// Enums
for enum_res in file_proto.enum_type() {
let (enum_data, _) = enum_res.expect("Failed to iterate enum");
write_enum(&EnumDescriptorProto::new(enum_data).expect("Failed to parse EnumDescriptorProto"), &mut output);
write_enum(
&EnumDescriptorProto::new(enum_data).expect("Failed to parse EnumDescriptorProto"),
&mut output,
);
}
// Messages
for msg_res in file_proto.message_type() {
let (msg_data, _) = msg_res.expect("Failed to iterate message");
write_message(&DescriptorProto::new(msg_data).expect("Failed to parse DescriptorProto"), &mut output);
write_message(
&DescriptorProto::new(msg_data).expect("Failed to parse DescriptorProto"),
&mut output,
);
}
generated_files.push((rust_file_name, output));
}
@@ -365,6 +417,7 @@ pub fn generate_rust_code(
}
let mut root_mod_content = String::new();
root_mod_content.push_str("// @generated by protoc-gen-roto — do not edit\n\n");
let mut sorted_root_mods: Vec<_> = root_mods.into_iter().collect();
sorted_root_mods.sort();
for m in sorted_root_mods {
@@ -374,6 +427,7 @@ pub fn generate_rust_code(
for (mod_path, sub_mods) in mod_files {
let mut content = String::new();
content.push_str("// @generated by protoc-gen-roto — do not edit\n\n");
let mut sorted_subs: Vec<_> = sub_mods.into_iter().collect();
sorted_subs.sort();
for sub in sorted_subs {
+801
View File
@@ -0,0 +1,801 @@
// @generated by protoc-gen-roto — do not edit
use crate::{ProtoAccessor, ProtoBuilder, Result, RotoError, read_varint, RepeatedFieldIterator};
use std::str;
pub struct Tool<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
version_offset: Option<usize>,
payload_offset: Option<usize>,
is_active_offset: Option<usize>,
exploit_count_offset: Option<usize>,
}
impl<'a> Tool<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut version_offset = None;
let mut payload_offset = None;
let mut is_active_offset = None;
let mut exploit_count_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 { version_offset = Some(offset); }
if tag.field_number == 3 { payload_offset = Some(offset); }
if tag.field_number == 4 { is_active_offset = Some(offset); }
if tag.field_number == 5 { exploit_count_offset = Some(offset); }
}
Ok(Self {
accessor,
name_offset,
version_offset,
payload_offset,
is_active_offset,
exploit_count_offset,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn version(&self) -> crate::Result<&'a str> {
let offset = self.version_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn payload(&self) -> crate::Result<&'a [u8]> {
let offset = self.payload_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn is_active(&self) -> crate::Result<bool> {
let offset = self.is_active_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn exploit_count(&self) -> crate::Result<i32> {
let offset = self.exploit_count_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct ToolBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> ToolBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> ToolBuilder<'_> {
ToolBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn version(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn payload(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(3, value)?;
Ok(self)
}
pub fn is_active(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn exploit_count(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(5, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Connection<'a> {
accessor: crate::ProtoAccessor<'a>,
host_offset: Option<usize>,
port_offset: Option<usize>,
encrypted_offset: Option<usize>,
bandwidth_bps_offset: Option<usize>,
session_key_offset: Option<usize>,
}
impl<'a> Connection<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut host_offset = None;
let mut port_offset = None;
let mut encrypted_offset = None;
let mut bandwidth_bps_offset = None;
let mut session_key_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { host_offset = Some(offset); }
if tag.field_number == 2 { port_offset = Some(offset); }
if tag.field_number == 3 { encrypted_offset = Some(offset); }
if tag.field_number == 4 { bandwidth_bps_offset = Some(offset); }
if tag.field_number == 5 { session_key_offset = Some(offset); }
}
Ok(Self {
accessor,
host_offset,
port_offset,
encrypted_offset,
bandwidth_bps_offset,
session_key_offset,
})
}
pub fn host(&self) -> crate::Result<&'a str> {
let offset = self.host_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn port(&self) -> crate::Result<i32> {
let offset = self.port_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn encrypted(&self) -> crate::Result<bool> {
let offset = self.encrypted_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn bandwidth_bps(&self) -> crate::Result<i32> {
let offset = self.bandwidth_bps_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn session_key(&self) -> crate::Result<&'a [u8]> {
let offset = self.session_key_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
}
pub struct ConnectionBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> ConnectionBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> ConnectionBuilder<'_> {
ConnectionBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn host(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn port(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(2, value)?;
Ok(self)
}
pub fn encrypted(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn bandwidth_bps(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn session_key(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(5, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Hacker<'a> {
accessor: crate::ProtoAccessor<'a>,
handle_offset: Option<usize>,
real_name_offset: Option<usize>,
age_offset: Option<usize>,
skill_level_offset: Option<usize>,
is_elite_offset: Option<usize>,
crew_id_offset: Option<usize>,
exploits_start: Option<usize>,
exploits_end: Option<usize>,
tools_start: Option<usize>,
tools_end: Option<usize>,
active_connection_offset: Option<usize>,
}
impl<'a> Hacker<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut handle_offset = None;
let mut real_name_offset = None;
let mut age_offset = None;
let mut skill_level_offset = None;
let mut is_elite_offset = None;
let mut crew_id_offset = None;
let mut exploits_start = None;
let mut exploits_end = None;
let mut tools_start = None;
let mut tools_end = None;
let mut active_connection_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { handle_offset = Some(offset); }
if tag.field_number == 2 { real_name_offset = Some(offset); }
if tag.field_number == 3 { age_offset = Some(offset); }
if tag.field_number == 4 { skill_level_offset = Some(offset); }
if tag.field_number == 5 { is_elite_offset = Some(offset); }
if tag.field_number == 6 { crew_id_offset = Some(offset); }
if tag.field_number == 7 {
if exploits_start.is_none() { exploits_start = Some(offset); }
exploits_end = Some(offset);
}
if tag.field_number == 8 {
if tools_start.is_none() { tools_start = Some(offset); }
tools_end = Some(offset);
}
if tag.field_number == 9 { active_connection_offset = Some(offset); }
}
Ok(Self {
accessor,
handle_offset,
real_name_offset,
age_offset,
skill_level_offset,
is_elite_offset,
crew_id_offset,
exploits_start, exploits_end,
tools_start, tools_end,
active_connection_offset,
})
}
pub fn handle(&self) -> crate::Result<&'a str> {
let offset = self.handle_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn real_name(&self) -> crate::Result<&'a str> {
let offset = self.real_name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn age(&self) -> crate::Result<i32> {
let offset = self.age_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn skill_level(&self) -> crate::Result<f32> {
let offset = self.skill_level_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?))
}
pub fn is_elite(&self) -> crate::Result<bool> {
let offset = self.is_elite_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn crew_id(&self) -> crate::Result<i32> {
let offset = self.crew_id_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn exploits(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.exploits_start, self.exploits_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(7, start, end),
_ => self.accessor.iter_repeated(7),
}
}
pub fn tools(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.tools_start, self.tools_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(8, start, end),
_ => self.accessor.iter_repeated(8),
}
}
pub fn active_connection(&self) -> crate::Result<&'a [u8]> {
let offset = self.active_connection_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
}
pub struct HackerBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> HackerBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> HackerBuilder<'_> {
HackerBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn handle(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn real_name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn age(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(3, value)?;
Ok(self)
}
pub fn skill_level(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(4, value)?;
Ok(self)
}
pub fn is_elite(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(5, value)?;
Ok(self)
}
pub fn crew_id(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(6, value)?;
Ok(self)
}
pub fn exploits(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(7, value)?;
Ok(self)
}
pub fn tools(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(8, value)?;
Ok(self)
}
pub fn active_connection(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(9, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Worm<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
variant_offset: Option<usize>,
size_bytes_offset: Option<usize>,
payload_offset: Option<usize>,
polymorphic_offset: Option<usize>,
targets_start: Option<usize>,
targets_end: Option<usize>,
}
impl<'a> Worm<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut variant_offset = None;
let mut size_bytes_offset = None;
let mut payload_offset = None;
let mut polymorphic_offset = None;
let mut targets_start = None;
let mut targets_end = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 { variant_offset = Some(offset); }
if tag.field_number == 3 { size_bytes_offset = Some(offset); }
if tag.field_number == 4 { payload_offset = Some(offset); }
if tag.field_number == 5 { polymorphic_offset = Some(offset); }
if tag.field_number == 6 {
if targets_start.is_none() { targets_start = Some(offset); }
targets_end = Some(offset);
}
}
Ok(Self {
accessor,
name_offset,
variant_offset,
size_bytes_offset,
payload_offset,
polymorphic_offset,
targets_start, targets_end,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn variant(&self) -> crate::Result<i32> {
let offset = self.variant_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn size_bytes(&self) -> crate::Result<i32> {
let offset = self.size_bytes_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn payload(&self) -> crate::Result<&'a [u8]> {
let offset = self.payload_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn polymorphic(&self) -> crate::Result<bool> {
let offset = self.polymorphic_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn targets(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.targets_start, self.targets_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(6, start, end),
_ => self.accessor.iter_repeated(6),
}
}
}
pub struct WormBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> WormBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> WormBuilder<'_> {
WormBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn variant(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(2, value)?;
Ok(self)
}
pub fn size_bytes(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn payload(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(4, value)?;
Ok(self)
}
pub fn polymorphic(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(5, value)?;
Ok(self)
}
pub fn targets(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(6, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Operation<'a> {
accessor: crate::ProtoAccessor<'a>,
codename_offset: Option<usize>,
target_corp_offset: Option<usize>,
timestamp_offset: Option<usize>,
successful_offset: Option<usize>,
stolen_data_offset: Option<usize>,
crew_start: Option<usize>,
crew_end: Option<usize>,
worm_offset: Option<usize>,
log_entries_start: Option<usize>,
log_entries_end: Option<usize>,
severity_offset: Option<usize>,
}
impl<'a> Operation<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut codename_offset = None;
let mut target_corp_offset = None;
let mut timestamp_offset = None;
let mut successful_offset = None;
let mut stolen_data_offset = None;
let mut crew_start = None;
let mut crew_end = None;
let mut worm_offset = None;
let mut log_entries_start = None;
let mut log_entries_end = None;
let mut severity_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { codename_offset = Some(offset); }
if tag.field_number == 2 { target_corp_offset = Some(offset); }
if tag.field_number == 3 { timestamp_offset = Some(offset); }
if tag.field_number == 4 { successful_offset = Some(offset); }
if tag.field_number == 5 { stolen_data_offset = Some(offset); }
if tag.field_number == 6 {
if crew_start.is_none() { crew_start = Some(offset); }
crew_end = Some(offset);
}
if tag.field_number == 7 { worm_offset = Some(offset); }
if tag.field_number == 8 {
if log_entries_start.is_none() { log_entries_start = Some(offset); }
log_entries_end = Some(offset);
}
if tag.field_number == 9 { severity_offset = Some(offset); }
}
Ok(Self {
accessor,
codename_offset,
target_corp_offset,
timestamp_offset,
successful_offset,
stolen_data_offset,
crew_start, crew_end,
worm_offset,
log_entries_start, log_entries_end,
severity_offset,
})
}
pub fn codename(&self) -> crate::Result<&'a str> {
let offset = self.codename_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn target_corp(&self) -> crate::Result<&'a str> {
let offset = self.target_corp_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn timestamp(&self) -> crate::Result<i32> {
let offset = self.timestamp_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn successful(&self) -> crate::Result<bool> {
let offset = self.successful_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn stolen_data(&self) -> crate::Result<&'a [u8]> {
let offset = self.stolen_data_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn crew(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.crew_start, self.crew_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(6, start, end),
_ => self.accessor.iter_repeated(6),
}
}
pub fn worm(&self) -> crate::Result<&'a [u8]> {
let offset = self.worm_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn log_entries(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.log_entries_start, self.log_entries_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(8, start, end),
_ => self.accessor.iter_repeated(8),
}
}
pub fn severity(&self) -> crate::Result<i32> {
let offset = self.severity_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct OperationBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> OperationBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> OperationBuilder<'_> {
OperationBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn codename(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn target_corp(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn timestamp(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn successful(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn stolen_data(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(5, value)?;
Ok(self)
}
pub fn crew(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(6, value)?;
Ok(self)
}
pub fn worm(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(7, value)?;
Ok(self)
}
pub fn log_entries(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(8, value)?;
Ok(self)
}
pub fn severity(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(9, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Campaign<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
operations_start: Option<usize>,
operations_end: Option<usize>,
total_bytes_stolen_offset: Option<usize>,
}
impl<'a> Campaign<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut operations_start = None;
let mut operations_end = None;
let mut total_bytes_stolen_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 {
if operations_start.is_none() { operations_start = Some(offset); }
operations_end = Some(offset);
}
if tag.field_number == 3 { total_bytes_stolen_offset = Some(offset); }
}
Ok(Self {
accessor,
name_offset,
operations_start, operations_end,
total_bytes_stolen_offset,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn operations(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.operations_start, self.operations_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(2, start, end),
_ => self.accessor.iter_repeated(2),
}
}
pub fn total_bytes_stolen(&self) -> crate::Result<i32> {
let offset = self.total_bytes_stolen_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct CampaignBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> CampaignBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> CampaignBuilder<'_> {
CampaignBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn operations(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(2, value)?;
Ok(self)
}
pub fn total_bytes_stolen(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
+29 -11
View File
@@ -1,5 +1,6 @@
pub mod generator;
pub mod google;
pub mod hackers;
// Uncomment this to check if the code compiles
// #[path = "../proto/google/protobuf/descriptor.rs"]
// pub mod descriptor;
@@ -220,11 +221,19 @@ impl<'a> ProtoAccessor<'a> {
}
_ => (cursor_after_tag, value_len),
};
Ok((&self.data[value_offset..value_offset + actual_value_len], tag.wire_type))
Ok((
&self.data[value_offset..value_offset + actual_value_len],
tag.wire_type,
))
}
/// Returns an iterator that scans a specific range of the buffer for all occurrences of the specified field.
pub fn iter_repeated_range(&self, field_number: u32, start: usize, end: usize) -> RepeatedFieldIterator<'a> {
pub fn iter_repeated_range(
&self,
field_number: u32,
start: usize,
end: usize,
) -> RepeatedFieldIterator<'a> {
RepeatedFieldIterator::new_range(self.data, field_number, start, end)
}
}
@@ -280,7 +289,11 @@ impl<'a> Iterator for FieldIterator<'a> {
self.cursor = cursor_after_tag + value_len;
Some(Ok((self.cursor - tag_len - value_len, tag, &self.data[value_offset..value_offset + actual_value_len])))
Some(Ok((
self.cursor - tag_len - value_len,
tag,
&self.data[value_offset..value_offset + actual_value_len],
)))
}
}
@@ -293,10 +306,7 @@ pub struct RepeatedFieldIterator<'a> {
impl<'a> RepeatedFieldIterator<'a> {
pub fn new(data: &'a [u8], field_number: u32) -> Self {
Self {
iterator: FieldIterator {
data,
cursor: 0,
},
iterator: FieldIterator { data, cursor: 0 },
field_number,
end_offset: None,
}
@@ -491,7 +501,8 @@ mod tests {
}
assert_eq!(i32_vals, vec![1, 2, 3, 4, 5]);
let repeated_strings: Vec<_> = acc.iter_repeated(18)
let repeated_strings: Vec<_> = acc
.iter_repeated(18)
.map(|r| {
let (val, _) = r.expect("Failed to decode repeated string");
std::str::from_utf8(val).expect("Invalid utf8")
@@ -499,7 +510,8 @@ mod tests {
.collect();
assert_eq!(repeated_strings, vec!["one", "two", "three"]);
let repeated_nested: Vec<_> = acc.iter_repeated(19)
let repeated_nested: Vec<_> = acc
.iter_repeated(19)
.map(|r| {
let (val, _) = r.expect("Failed to decode repeated nested");
let nested_acc = ProtoAccessor::new(val).unwrap();
@@ -519,7 +531,8 @@ mod tests {
assert_eq!(id, 200);
// Validate that fields appear in the expected relative order
let field_numbers: Vec<u32> = acc.fields()
let field_numbers: Vec<u32> = acc
.fields()
.map(|r| r.expect("Failed to decode field").1.field_number)
.collect();
@@ -528,7 +541,12 @@ mod tests {
let mut found_count = 0;
for &f in &field_numbers {
if essential_fields.contains(&f) {
assert!(f >= last_field, "Fields appeared out of order: {} came after {}", f, last_field);
assert!(
f >= last_field,
"Fields appeared out of order: {} came after {}",
f,
last_field
);
last_field = f;
found_count += 1;
}
+2
View File
@@ -0,0 +1,2 @@
test
hackers_bench
+22
View File
@@ -0,0 +1,22 @@
CC = cc
CFLAGS = -O2 -std=c11 -D_POSIX_C_SOURCE=199309L -I. -I/usr/include -Wall -Wextra
LDFLAGS = -lupb -lutf8_range
SRCS = hackers_bench.c hackers.upb.c hackers.upb_minitable.c
TARGET = hackers_bench
.PHONY: all clean regen
all: $(TARGET)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# Re-generate upb files from proto/hackers.proto
regen:
protoc -I ../proto ../proto/hackers.proto \
--upb_out=. \
--upb_minitable_out=.
clean:
rm -f $(TARGET)
+1
View File
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
+175
View File
@@ -0,0 +1,175 @@
/* This file was generated by upb_generator from the input file:
*
* hackers.proto
*
* Do not edit -- your changes will be discarded when the file is
* regenerated.
* NO CHECKED-IN PROTOBUF GENCODE */
#include <stddef.h>
#include "upb/generated_code_support.h"
#include "hackers.upb_minitable.h"
// Must be last.
#include "upb/port/def.inc"
extern const struct upb_MiniTable UPB_PRIVATE(_kUpb_MiniTable_StaticallyTreeShaken);
static const upb_MiniTableField Tool__fields[5] = {
{1, 16, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(24, 32), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(32, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{4, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{5, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Tool_msg_init = {
NULL,
&Tool__fields[0],
UPB_SIZE(40, 64), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Tool",
#endif
};
const upb_MiniTable* Tool_msg_init_ptr = &Tool_msg_init;
static const upb_MiniTableField Connection__fields[5] = {
{1, 16, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{3, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{4, UPB_SIZE(32, 48), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{5, UPB_SIZE(24, 32), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Connection_msg_init = {
NULL,
&Connection__fields[0],
UPB_SIZE(40, 56), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Connection",
#endif
};
const upb_MiniTable* Connection_msg_init_ptr = &Connection_msg_init;
static const upb_MiniTableSubInternal Hacker__submsgs[2] = {
{.UPB_PRIVATE(submsg) = &Tool_msg_init_ptr},
{.UPB_PRIVATE(submsg) = &Connection_msg_init_ptr},
};
static const upb_MiniTableField Hacker__fields[9] = {
{1, UPB_SIZE(32, 24), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 40, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{4, 16, 0, kUpb_NoSub, 2, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{5, 9, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(48, 56), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{7, UPB_SIZE(20, 64), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{8, UPB_SIZE(24, 72), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{9, UPB_SIZE(28, 80), 64, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Hacker_msg_init = {
&Hacker__submsgs[0],
&Hacker__fields[0],
UPB_SIZE(56, 88), 9, kUpb_ExtMode_NonExtendable, 9, UPB_FASTTABLE_MASK(56), 0,
#ifdef UPB_TRACING_ENABLED
"Hacker",
#endif
UPB_FASTTABLE_INIT({
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x001000003f000025, &upb_DecodeFast_Fixed32_Scalar_Tag1Byte},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
})
};
const upb_MiniTable* Hacker_msg_init_ptr = &Hacker_msg_init;
static const upb_MiniTableField Worm__fields[6] = {
{1, UPB_SIZE(20, 16), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(40, 48), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{4, UPB_SIZE(28, 32), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{5, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(16, 56), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Worm_msg_init = {
NULL,
&Worm__fields[0],
UPB_SIZE(48, 64), 6, kUpb_ExtMode_NonExtendable, 6, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Worm",
#endif
};
const upb_MiniTable* Worm_msg_init_ptr = &Worm_msg_init;
static const upb_MiniTableSubInternal Operation__submsgs[2] = {
{.UPB_PRIVATE(submsg) = &Hacker_msg_init_ptr},
{.UPB_PRIVATE(submsg) = &Worm_msg_init_ptr},
};
static const upb_MiniTableField Operation__fields[9] = {
{1, UPB_SIZE(28, 16), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(36, 32), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(56, 64), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{4, 9, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{5, UPB_SIZE(44, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(12, 72), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{7, UPB_SIZE(16, 80), 64, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{8, UPB_SIZE(20, 88), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{9, UPB_SIZE(24, 12), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Operation_msg_init = {
&Operation__submsgs[0],
&Operation__fields[0],
UPB_SIZE(64, 96), 9, kUpb_ExtMode_NonExtendable, 9, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Operation",
#endif
};
const upb_MiniTable* Operation_msg_init_ptr = &Operation_msg_init;
static const upb_MiniTableSubInternal Campaign__submsgs[1] = {
{.UPB_PRIVATE(submsg) = &Operation_msg_init_ptr},
};
static const upb_MiniTableField Campaign__fields[3] = {
{1, UPB_SIZE(12, 8), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(8, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(24, 32), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Campaign_msg_init = {
&Campaign__submsgs[0],
&Campaign__fields[0],
UPB_SIZE(32, 40), 3, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Campaign",
#endif
};
const upb_MiniTable* Campaign_msg_init_ptr = &Campaign_msg_init;
static const upb_MiniTable *messages_layout[6] = {
&Tool_msg_init,
&Connection_msg_init,
&Hacker_msg_init,
&Worm_msg_init,
&Operation_msg_init,
&Campaign_msg_init,
};
const upb_MiniTableFile hackers_proto_upb_file_layout = {
messages_layout,
NULL,
NULL,
6,
0,
0,
};
#include "upb/port/undef.inc"
+42
View File
@@ -0,0 +1,42 @@
/* This file was generated by upb_generator from the input file:
*
* hackers.proto
*
* Do not edit -- your changes will be discarded when the file is
* regenerated.
* NO CHECKED-IN PROTOBUF GENCODE */
#ifndef HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_
#define HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_
#include "upb/generated_code_support.h"
// Must be last.
#include "upb/port/def.inc"
#ifdef __cplusplus
extern "C" {
#endif
extern const upb_MiniTable Tool_msg_init;
extern const upb_MiniTable* Tool_msg_init_ptr;
extern const upb_MiniTable Connection_msg_init;
extern const upb_MiniTable* Connection_msg_init_ptr;
extern const upb_MiniTable Hacker_msg_init;
extern const upb_MiniTable* Hacker_msg_init_ptr;
extern const upb_MiniTable Worm_msg_init;
extern const upb_MiniTable* Worm_msg_init_ptr;
extern const upb_MiniTable Operation_msg_init;
extern const upb_MiniTable* Operation_msg_init_ptr;
extern const upb_MiniTable Campaign_msg_init;
extern const upb_MiniTable* Campaign_msg_init_ptr;
extern const upb_MiniTableFile hackers_proto_upb_file_layout;
#ifdef __cplusplus
} /* extern "C" */
#endif
#include "upb/port/undef.inc"
#endif /* HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_ */
+380
View File
@@ -0,0 +1,380 @@
/*
* hackers_bench.c — C/upb benchmark mirroring benches/hackers_bench.rs
*
* Proto: proto/hackers.proto
* Generated files: hackers.upb.h / .c, hackers.upb_minitable.h / .c
*
* Build: make
* Run: ./hackers_bench
*
* Data files are read from ../data/bench/<name>.pb — the same files
* produced by `cargo run --release --bin gen_bench_data -- --preset <name>`.
*
* The four benchmark groups match the Rust/criterion groups exactly:
*
* shallow_parse — Campaign_parse() + Arena_Free() per iteration.
* upb fully decodes the message; roto merely scans
* for field offsets. This is the most important
* comparison: total cost to "be ready to read".
*
* deep_parse — parse + walk Campaign → Operations → every Hacker,
* touching each Hacker's handle field.
*
* field_access — message pre-parsed once outside the loop; each
* micro-benchmark times a single field read.
* upb: direct struct lookup. roto: decode at offset.
*
* iterate — count_operations: parse + count top-level repeated.
* count_all_crew: parse + count nested repeated.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include "hackers.upb.h"
#include "hackers.upb_minitable.h"
/* ==========================================================================
* Timing
* ========================================================================== */
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
}
/* ==========================================================================
* Black-box sink — prevents the compiler from optimising away benchmark work.
* We write the result of every meaningful computation here.
* ========================================================================== */
static volatile uintptr_t g_sink;
/* ==========================================================================
* File I/O
* ========================================================================== */
typedef struct {
uint8_t *data;
size_t len;
char path[256];
} BenchData;
static bool load_bench_data(BenchData *out, const char *name) {
snprintf(out->path, sizeof(out->path), "../data/bench/%s.pb", name);
FILE *f = fopen(out->path, "rb");
if (!f) {
printf("[skip] %s not found — "
"run `cargo run --release --bin gen_bench_data -- --preset %s` first\n",
out->path, name);
out->data = NULL;
out->len = 0;
return false;
}
fseek(f, 0, SEEK_END);
out->len = (size_t)ftell(f);
rewind(f);
out->data = malloc(out->len);
if (!out->data) { fclose(f); return false; }
fread(out->data, 1, out->len, f);
fclose(f);
return true;
}
static void free_bench_data(BenchData *d) {
free(d->data);
d->data = NULL;
}
/* ==========================================================================
* Benchmark runner
*
* Finds a batch size such that one batch takes ≥1 ms, then runs batches
* until at least BENCH_MIN_SECS of wall time has elapsed. Reports the
* mean ns/iter and, if bytes > 0, the MB/s throughput.
* ========================================================================== */
#define BENCH_MIN_SECS 0.5
typedef void (*bench_fn)(void *state);
static void run_bench(bench_fn fn, void *state, size_t bytes, const char *label) {
/* warmup */
for (int i = 0; i < 5; i++) fn(state);
/* calibrate: find batch size so one batch ≥ 1 ms */
uint64_t batch = 1;
while (batch < 10000000ULL) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
if (now_ns() - t0 >= 1000000ULL) break; /* 1 ms */
batch *= 4;
}
/* measure */
uint64_t target_ns = (uint64_t)(BENCH_MIN_SECS * 1e9);
uint64_t total_ns = 0;
uint64_t total_its = 0;
while (total_ns < target_ns) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
total_ns += now_ns() - t0;
total_its += batch;
}
double ns_per_iter = (double)total_ns / (double)total_its;
if (bytes > 0) {
double mb_per_sec = (double)bytes / ns_per_iter * 1000.0;
printf(" %-46s %9.2f ns/iter %8.2f MB/s\n",
label, ns_per_iter, mb_per_sec);
} else {
printf(" %-46s %9.2f ns/iter\n", label, ns_per_iter);
}
}
/* ==========================================================================
* shallow_parse — Campaign_parse() + upb_Arena_Free() per iteration
*
* Measures the full cost of becoming "ready to access any field", matching
* the Rust `Campaign::new()` benchmark. upb fully decodes; roto only scans.
* ========================================================================== */
static void fn_shallow_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
g_sink = (uintptr_t)c;
upb_Arena_Free(arena);
}
static void bench_shallow_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", "large", NULL};
printf("\n=== shallow_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign_parse/%s [%zu B]", sizes[i], d.len);
run_bench(fn_shallow_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* deep_parse — parse + walk Campaign → Operations → Hackers
*
* After Campaign_parse(), upb has already decoded everything. The "deep"
* walk is pointer-chasing through the decoded tree. In roto each level
* calls ::new(), paying another linear scan over that sub-message's bytes.
* ========================================================================== */
static void fn_deep_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t hacker_count = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
const Hacker * const *crew = Operation_crew(ops[i], &n_crew);
for (size_t j = 0; j < n_crew; j++) {
upb_StringView handle = Hacker_handle(crew[j]);
g_sink = (uintptr_t)handle.data;
hacker_count++;
}
}
g_sink = hacker_count;
upb_Arena_Free(arena);
}
static void bench_deep_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== deep_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign+Ops+Hackers/%s [%zu B]", sizes[i], d.len);
run_bench(fn_deep_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* field_access — individual field reads on a pre-parsed message
*
* Parse once outside the loop; each micro-benchmark measures the accessor
* call itself. upb: a struct-field read with a MiniTable lookup.
* roto: decode the value at a pre-recorded byte offset.
* ========================================================================== */
typedef struct {
upb_Arena *arena;
Campaign *campaign;
Operation *op;
Hacker *hacker;
Worm *worm;
} FieldState;
static void fn_field_campaign_name(void *s) {
upb_StringView v = Campaign_name(((FieldState *)s)->campaign);
g_sink = (uintptr_t)v.data;
}
static void fn_field_total_bytes_stolen(void *s) {
g_sink = (uintptr_t)(uint64_t)Campaign_total_bytes_stolen(((FieldState *)s)->campaign);
}
static void fn_field_op_codename(void *s) {
upb_StringView v = Operation_codename(((FieldState *)s)->op);
g_sink = (uintptr_t)v.data;
}
static void fn_field_op_timestamp(void *s) {
g_sink = (uintptr_t)(uint64_t)Operation_timestamp(((FieldState *)s)->op);
}
static void fn_field_op_successful(void *s) {
g_sink = (uintptr_t)Operation_successful(((FieldState *)s)->op);
}
static void fn_field_hacker_handle(void *s) {
upb_StringView v = Hacker_handle(((FieldState *)s)->hacker);
g_sink = (uintptr_t)v.data;
}
static void fn_field_hacker_skill_level(void *s) {
/* store float bits to avoid FPU → int conversion costs */
float f = Hacker_skill_level(((FieldState *)s)->hacker);
uint32_t bits; memcpy(&bits, &f, 4);
g_sink = bits;
}
static void fn_field_hacker_is_elite(void *s) {
g_sink = (uintptr_t)Hacker_is_elite(((FieldState *)s)->hacker);
}
static void fn_field_worm_polymorphic(void *s) {
g_sink = (uintptr_t)Worm_polymorphic(((FieldState *)s)->worm);
}
static void fn_field_worm_payload(void *s) {
upb_StringView v = Worm_payload(((FieldState *)s)->worm);
g_sink = (uintptr_t)v.data;
}
static void bench_field_access(void) {
BenchData d;
if (!load_bench_data(&d, "small")) return;
upb_Arena *arena = upb_Arena_New();
Campaign *campaign = Campaign_parse((const char *)d.data, d.len, arena);
if (!campaign) { fprintf(stderr, "parse failed\n"); return; }
size_t n_ops;
const Operation * const *ops = Campaign_operations(campaign, &n_ops);
if (n_ops == 0) { fprintf(stderr, "no operations\n"); return; }
Operation *op = (Operation *)ops[0]; /* cast away const for state */
size_t n_crew;
const Hacker * const *crew = Operation_crew(op, &n_crew);
if (n_crew == 0) { fprintf(stderr, "no crew\n"); return; }
Hacker *hacker = (Hacker *)crew[0];
const Worm *worm = Operation_worm(op);
if (!worm) { fprintf(stderr, "no worm\n"); return; }
FieldState state = {
.arena = arena,
.campaign = campaign,
.op = op,
.hacker = hacker,
.worm = (Worm *)worm,
};
printf("\n=== field_access ===\n");
run_bench(fn_field_campaign_name, &state, 0, "campaign::name");
run_bench(fn_field_total_bytes_stolen, &state, 0, "campaign::total_bytes_stolen");
run_bench(fn_field_op_codename, &state, 0, "operation::codename");
run_bench(fn_field_op_timestamp, &state, 0, "operation::timestamp");
run_bench(fn_field_op_successful, &state, 0, "operation::successful");
run_bench(fn_field_hacker_handle, &state, 0, "hacker::handle");
run_bench(fn_field_hacker_skill_level, &state, 0, "hacker::skill_level (f32)");
run_bench(fn_field_hacker_is_elite, &state, 0, "hacker::is_elite (bool)");
run_bench(fn_field_worm_polymorphic, &state, 0, "worm::polymorphic (bool)");
run_bench(fn_field_worm_payload, &state, 0, "worm::payload (bytes)");
upb_Arena_Free(arena);
free_bench_data(&d);
}
/* ==========================================================================
* iterate — count repeated fields at different depths
*
* count_operations: after parsing, Campaign_operations() returns pointer+count
* in O(1) — upb already decoded the array.
* roto's Campaign::new() scan IS the counting work.
*
* count_all_crew: parse + walk ops + sum crew sizes.
* ========================================================================== */
static void fn_count_operations(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n;
Campaign_operations(c, &n);
g_sink = n;
upb_Arena_Free(arena);
}
static void fn_count_all_crew(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t total = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
Operation_crew(ops[i], &n_crew);
total += n_crew;
}
g_sink = total;
upb_Arena_Free(arena);
}
static void bench_iterate(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== iterate ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "count_operations/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_operations, &d, d.len, label);
snprintf(label, sizeof(label), "count_all_crew/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_all_crew, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* main
* ========================================================================== */
int main(void) {
printf("hackers_bench (upb / protobuf %s)\n", "33.1");
printf("Data files: ../data/bench/<name>.pb\n");
printf("Run `cargo run --release --bin gen_bench_data -- --preset <name>` to generate.\n");
bench_shallow_parse();
bench_deep_parse();
bench_field_access();
bench_iterate();
printf("\n");
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#include <stdio.h>
#include <stdlib.h>
#include "hackers.upb.h"
#include "hackers.upb_minitable.h"
int main(void) {
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_new(arena);
(void)c;
printf("name: %.*s\n", (int)Campaign_name(c).size, Campaign_name(c).data);
upb_Arena_Free(arena);
return 0;
}