From 804ff3ead07497ba3f52ce6c236e71670a341f0e Mon Sep 17 00:00:00 2001 From: charles Date: Tue, 12 May 2026 13:44:53 -0700 Subject: [PATCH] Checkpoint for gRPC implementation --- Cargo.lock | 355 ++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 24 +- codegen/src/generator.rs | 7 +- codegen/tests/build_generated_code.rs | 2 +- codegen/tests/test_map_build.rs | 2 +- examples/hello_world/Cargo.toml | 29 ++ examples/hello_world/build.rs | 27 ++ examples/hello_world/proto/hello.proto | 15 ++ examples/hello_world/proto/hello.rs | 210 +++++++++++++++ examples/hello_world/src/bin/client.rs | 64 +++++ examples/hello_world/src/bin/server.rs | 165 ++++++++++++ 12 files changed, 882 insertions(+), 19 deletions(-) create mode 100644 examples/hello_world/Cargo.toml create mode 100644 examples/hello_world/build.rs create mode 100644 examples/hello_world/proto/hello.proto create mode 100644 examples/hello_world/proto/hello.rs create mode 100644 examples/hello_world/src/bin/client.rs create mode 100644 examples/hello_world/src/bin/server.rs diff --git a/Cargo.lock b/Cargo.lock index 2d6307e..40c1b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,15 +383,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "futures-channel" version = "0.3.32" @@ -407,6 +425,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -426,6 +455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -442,6 +472,19 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "h2" version = "0.4.14" @@ -478,6 +521,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -490,6 +542,25 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hello-world" +version = "0.1.0" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "prost", + "roto-runtime", + "roto-tonic", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "tower 0.4.13", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -596,6 +667,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "1.9.3" @@ -614,6 +691,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -684,12 +763,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -734,6 +825,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "num-traits" version = "0.2.19" @@ -790,6 +887,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + [[package]] name = "pin-project" version = "1.1.12" @@ -868,6 +975,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -887,6 +1004,26 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.13.5" @@ -900,6 +1037,15 @@ dependencies = [ "syn", ] +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "protos" version = "0.1.0" @@ -913,6 +1059,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.6" @@ -940,7 +1092,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1039,6 +1191,19 @@ dependencies = [ "tonic", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1060,6 +1225,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -1168,6 +1339,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "test_grpc_project" version = "0.1.0" @@ -1273,6 +1457,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + [[package]] name = "tower" version = "0.4.13" @@ -1325,6 +1523,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1362,6 +1561,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1393,6 +1598,24 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-bindgen" version = "0.2.120" @@ -1438,6 +1661,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.97" @@ -1545,6 +1802,100 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "zerocopy" version = "0.8.48" diff --git a/Cargo.toml b/Cargo.toml index d4336c9..48bd71e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "protos", "benches", "roto-tonic", "test_grpc_project", + "examples/hello_world", ] exclude = [ diff --git a/README.md b/README.md index 89f7db7..d670716 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,11 @@ Instead of deserializing binary protobuf data into Rust structs, roto scans a me 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. -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. +It also provides a first-class integration with the `tonic` gRPC framework via the `roto-tonic` crate, +enabling zero-allocation request/response processing. + +Writing works the same way: you provide a fixed buffer (or a `bytes::BufMut`) and a builder writes +fields directly into it, returning a slice of the bytes written. ## Design @@ -28,10 +31,15 @@ This will generate a file, src/hackers.rs. ## Generated code -For each protobuf message roto generates two types: +For each protobuf message roto generates three types: - **Reader struct** `MessageName<'a>` — borrows the original byte slice, zero-copy. -- **Builder struct** `MessageNameBuilder<'b>` — writes into a caller-provided `&mut [u8]`. +- **Builder struct** `MessageNameBuilder<'b>` — writes into a caller-provided `&mut [u8]` or `BufMut`. +- **Owned struct** `OwnedMessageName` — owns the byte buffer and implements `RotoOwned`, providing a bridge to the `Reader`. + +For each protobuf service, roto generates: + +- **Service Trait** `ServiceName` — a `tonic`-compatible async trait for gRPC service implementations. Nested message types are placed in a `pub mod message_name { ... }` module (snake_case of the parent message name) within the same generated file. @@ -314,12 +322,4 @@ The goal is to validate roto's implementation against the Proto3 specification. ### Unsupported Features - **Reserved Fields**: `reserved` statements are ignored. -- **Services**: `service` and `rpc` definitions are ignored. - **Options**: Field and message options are ignored. - -### Tasks - -- [x] Analyze `roto/codegen` to determine which protobuf constructs are supported during code generation. -- [x] Analyze `roto/runtime` to determine which wire types and protobuf types are supported during reading and writing. -- [x] Compare findings with the Proto3 spec (https://protobuf.dev/reference/protobuf/proto3-spec/). -- [x] Document supported and unsupported features in the README. diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index 72ec47a..f093759 100644 --- a/codegen/src/generator.rs +++ b/codegen/src/generator.rs @@ -439,14 +439,15 @@ fn write_message(msg_proto: &DescriptorProto, output: &mut String) { output.push_str("}\n\n"); output.push_str(&format!("impl roto_runtime::RotoOwned for Owned{} {{\n", msg_name)); + output.push_str(&format!(" type Reader<'a> = {}<'a>;\n", msg_name)); output.push_str(&format!(" fn reader(&self) -> {}<'_> {{\n", msg_name)); output.push_str(&format!(" {}::new(&self.data).expect(\"failed to create reader\")\n", msg_name)); output.push_str(" }\n"); output.push_str("}\n\n"); output.push_str(&format!("impl roto_runtime::RotoMessage for Owned{} {{\n", msg_name)); - output.push_str(" fn decode(buf: bytes::Bytes) -> Self {\n"); - output.push_str(&format!(" Owned{} {{ data: buf }}\n", msg_name)); + output.push_str(" fn decode(buf: bytes::Bytes) -> roto_runtime::Result {\n"); + output.push_str(&format!(" Ok(Owned{} {{ data: buf }})\n", msg_name)); output.push_str(" }\n\n"); output.push_str(" fn bytes(&self) -> bytes::Bytes {\n"); output.push_str(" self.data.clone()\n"); @@ -675,7 +676,7 @@ fn write_service(svc_proto: &ServiceDescriptorProto, output: &mut String) { }; output.push_str(&format!( - " async fn {}(&self, request: {}) -> Result<{}, Status>;\n", + " async fn {}(&self, request: {}) -> std::result::Result<{}, Status>;\n", method_name, req_type, resp_type )); } diff --git a/codegen/tests/build_generated_code.rs b/codegen/tests/build_generated_code.rs index 0f7c574..661ce14 100644 --- a/codegen/tests/build_generated_code.rs +++ b/codegen/tests/build_generated_code.rs @@ -58,7 +58,7 @@ fn test_generated_code_builds() { let cargo_toml_content = fs::read_to_string(&cargo_toml_path).expect("Failed to read Cargo.toml"); let updated_cargo_toml = format!( - "{}\n\nroto-codegen = {{ path = \"..\" }}\nroto-runtime = {{ path = \"../../runtime\" }}\n\n[workspace]\n", + "{}\n\nroto-codegen = {{ path = \"..\" }}\nroto-runtime = {{ path = \"../../runtime\" }}\nbytes = \"1.0\"\ntonic = \"0.12\"\ntokio-stream = \"0.1\"\n\n[workspace]\n", cargo_toml_content ); fs::write(cargo_toml_path, updated_cargo_toml).expect("Failed to write Cargo.toml"); diff --git a/codegen/tests/test_map_build.rs b/codegen/tests/test_map_build.rs index cf16e11..e7dd5b9 100644 --- a/codegen/tests/test_map_build.rs +++ b/codegen/tests/test_map_build.rs @@ -38,7 +38,7 @@ fn test_map_generated_code_builds() { let cargo_toml_content = fs::read_to_string(&cargo_toml_path).expect("Failed to read Cargo.toml"); let updated_cargo_toml = format!( - "{}\n\nroto-codegen = {{ path = \"..\" }}\nroto-runtime = {{ path = \"../../runtime\" }}\n\n[workspace]\n", + "{}\n\nroto-codegen = {{ path = \"..\" }}\nroto-runtime = {{ path = \"../../runtime\" }}\nbytes = \"1.0\"\ntonic = \"0.12\"\ntokio-stream = \"0.1\"\n\n[workspace]\n", cargo_toml_content ); fs::write(cargo_toml_path, updated_cargo_toml).expect("Failed to write Cargo.toml"); diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml new file mode 100644 index 0000000..9fb2af8 --- /dev/null +++ b/examples/hello_world/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "server" +path = "src/bin/server.rs" + +[[bin]] +name = "client" +path = "src/bin/client.rs" + +[dependencies] +roto-runtime = { path = "../../runtime" } +roto-tonic = { path = "../../roto-tonic" } +tonic = "0.12" +tokio = { version = "1.38", features = ["full"] } +tokio-stream = "0.1" +bytes = "1.7" +prost = "0.13" +tower = "0.4" +futures-util = "0.3" +http-body-util = "0.1" +http = "1.1" +http-body = "1.0" + +[build-dependencies] +tonic-build = "0.12" diff --git a/examples/hello_world/build.rs b/examples/hello_world/build.rs new file mode 100644 index 0000000..35bd65f --- /dev/null +++ b/examples/hello_world/build.rs @@ -0,0 +1,27 @@ +fn main() { + let proto_file = "proto/hello.proto"; + let out_dir = std::env::var("OUT_DIR").unwrap(); + let dest_path = std::path::Path::new(&out_dir).join("hello.rs"); + + // Find the protoc-gen-roto binary + // In a real scenario, this should be passed as an environment variable or found in PATH + // For this example, we'll try to find it in the target directory + let target_dir = std::env::current_dir().unwrap().join("../../target/debug"); + let plugin_path = target_dir.join("protoc-gen-roto"); + + if !plugin_path.exists() { + panic!("protoc-gen-roto plugin not found at {:?}", plugin_path); + } + + let status = std::process::Command::new("protoc") + .arg(format!("--plugin=protoc-gen-roto={}", plugin_path.display())) + .arg(format!("--roto_out={}", out_dir)) + .arg(format!("--roto_opt=src=proto")) // Assuming the plugin handles this or we just pass it + .arg(proto_file) + .status() + .expect("Failed to execute protoc"); + + if !status.success() { + panic!("protoc failed with status {}", status); + } +} diff --git a/examples/hello_world/proto/hello.proto b/examples/hello_world/proto/hello.proto new file mode 100644 index 0000000..dd23f69 --- /dev/null +++ b/examples/hello_world/proto/hello.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package hello; + +service HelloWorldService { + rpc HelloWorld (HelloRequest) returns (HelloResponse); +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + string message = 1; +} diff --git a/examples/hello_world/proto/hello.rs b/examples/hello_world/proto/hello.rs new file mode 100644 index 0000000..fa6bf2a --- /dev/null +++ b/examples/hello_world/proto/hello.rs @@ -0,0 +1,210 @@ +// @generated by protoc-gen-roto — do not edit +#[allow(unused_imports)] + +use roto_runtime::{ProtoAccessor, ProtoBuilder, Result, RotoError, read_varint, RepeatedFieldIterator}; +use std::str; +use bytes::Bytes; +use tonic::{Request, Response, Status}; +use tokio_stream::Stream; +use std::pin::Pin; + + +pub struct HelloRequest<'a> { + accessor: roto_runtime::ProtoAccessor<'a>, + name_offset: Option, +} + +impl<'a> HelloRequest<'a> { + pub fn new(data: &'a [u8]) -> roto_runtime::Result { + let accessor = roto_runtime::ProtoAccessor::new(data)?; + let mut name_offset = None; + for item in accessor.fields() { + let (offset, tag, _) = item?; + if tag.field_number == 1 { name_offset = Some(offset); } + } + + Ok(Self { + accessor, +name_offset, + }) + } + + pub fn name(&self) -> roto_runtime::Result<&'a str> { + let offset = self.name_offset.ok_or(roto_runtime::RotoError::FieldNotFound)?; + let (bytes, _) = self.accessor.get_value_at(offset)?; + str::from_utf8(bytes).map_err(|_| roto_runtime::RotoError::WireFormatViolation) + } + + pub fn name_or_default(&self) -> roto_runtime::Result<&'a str> { + self.name().or(Ok("")) + } + + pub fn has_name(&self) -> bool { self.name_offset.is_some() } + + pub fn raw_fields(&self) -> roto_runtime::RawFieldIterator<'a> { + self.accessor.raw_fields() + } + +} + +pub struct HelloRequestBuilder<'b> { + builder: roto_runtime::ProtoBuilder<'b>, + name_written: bool, +} + +impl<'b> HelloRequestBuilder<'b> { + pub fn builder(buf: &mut [u8]) -> HelloRequestBuilder<'_> { + HelloRequestBuilder { + builder: roto_runtime::ProtoBuilder::new(buf), + name_written: false, + } + } + + pub fn name(mut self, value: &str) -> roto_runtime::Result { + self.builder.write_string(1, value)?; + self.name_written = true; + Ok(self) + } + + pub fn with(mut self, msg: &HelloRequest<'_>) -> roto_runtime::Result { + for item in msg.raw_fields() { + let (field_number, raw_bytes) = item?; + let is_written = match field_number { + 1 => self.name_written, + _ => false, + }; + if !is_written { + self.builder.write_raw(raw_bytes)?; + } + } + Ok(self) + } + + pub fn finish(self) -> roto_runtime::Result<&'b mut [u8]> { + self.builder.finish() + } +} + +pub struct OwnedHelloRequest { + pub data: bytes::Bytes, +} + +impl roto_runtime::RotoOwned for OwnedHelloRequest { + type Reader<'a> = HelloRequest<'a>; + fn reader(&self) -> HelloRequest<'_> { + HelloRequest::new(&self.data).expect("failed to create reader") + } +} + +impl roto_runtime::RotoMessage for OwnedHelloRequest { + fn decode(buf: bytes::Bytes) -> roto_runtime::Result { + Ok(OwnedHelloRequest { data: buf }) + } + + fn bytes(&self) -> bytes::Bytes { + self.data.clone() + } +} + +pub struct HelloResponse<'a> { + accessor: roto_runtime::ProtoAccessor<'a>, + message_offset: Option, +} + +impl<'a> HelloResponse<'a> { + pub fn new(data: &'a [u8]) -> roto_runtime::Result { + let accessor = roto_runtime::ProtoAccessor::new(data)?; + let mut message_offset = None; + for item in accessor.fields() { + let (offset, tag, _) = item?; + if tag.field_number == 1 { message_offset = Some(offset); } + } + + Ok(Self { + accessor, +message_offset, + }) + } + + pub fn message(&self) -> roto_runtime::Result<&'a str> { + let offset = self.message_offset.ok_or(roto_runtime::RotoError::FieldNotFound)?; + let (bytes, _) = self.accessor.get_value_at(offset)?; + str::from_utf8(bytes).map_err(|_| roto_runtime::RotoError::WireFormatViolation) + } + + pub fn message_or_default(&self) -> roto_runtime::Result<&'a str> { + self.message().or(Ok("")) + } + + pub fn has_message(&self) -> bool { self.message_offset.is_some() } + + pub fn raw_fields(&self) -> roto_runtime::RawFieldIterator<'a> { + self.accessor.raw_fields() + } + +} + +pub struct HelloResponseBuilder<'b> { + builder: roto_runtime::ProtoBuilder<'b>, + message_written: bool, +} + +impl<'b> HelloResponseBuilder<'b> { + pub fn builder(buf: &mut [u8]) -> HelloResponseBuilder<'_> { + HelloResponseBuilder { + builder: roto_runtime::ProtoBuilder::new(buf), + message_written: false, + } + } + + pub fn message(mut self, value: &str) -> roto_runtime::Result { + self.builder.write_string(1, value)?; + self.message_written = true; + Ok(self) + } + + pub fn with(mut self, msg: &HelloResponse<'_>) -> roto_runtime::Result { + for item in msg.raw_fields() { + let (field_number, raw_bytes) = item?; + let is_written = match field_number { + 1 => self.message_written, + _ => false, + }; + if !is_written { + self.builder.write_raw(raw_bytes)?; + } + } + Ok(self) + } + + pub fn finish(self) -> roto_runtime::Result<&'b mut [u8]> { + self.builder.finish() + } +} + +pub struct OwnedHelloResponse { + pub data: bytes::Bytes, +} + +impl roto_runtime::RotoOwned for OwnedHelloResponse { + type Reader<'a> = HelloResponse<'a>; + fn reader(&self) -> HelloResponse<'_> { + HelloResponse::new(&self.data).expect("failed to create reader") + } +} + +impl roto_runtime::RotoMessage for OwnedHelloResponse { + fn decode(buf: bytes::Bytes) -> roto_runtime::Result { + Ok(OwnedHelloResponse { data: buf }) + } + + fn bytes(&self) -> bytes::Bytes { + self.data.clone() + } +} + +#[tonic::async_trait] +pub trait HelloWorldService: Send + Sync + 'static { + async fn hello_world(&self, request: Request) -> std::result::Result, Status>; +} + diff --git a/examples/hello_world/src/bin/client.rs b/examples/hello_world/src/bin/client.rs new file mode 100644 index 0000000..5e4eaaa --- /dev/null +++ b/examples/hello_world/src/bin/client.rs @@ -0,0 +1,64 @@ +use tonic::Request; +use roto_tonic::RotoCodec; +use hello::{HelloWorldService, OwnedHelloRequest, OwnedHelloResponse}; +use roto_runtime::RotoOwned; +use std::task::{Context, Poll}; +use tower::Service; + +pub mod hello { + include!("../../proto/hello.rs"); +} + +struct ReadyService(S); + +impl Service for ReadyService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Req) -> S::Future { + self.0.call(req) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let channel = tonic::transport::Channel::from_static("http://[::1]:50051") + .connect() + .await?; + + let ready_channel = ReadyService(channel); + let mut client = tonic::client::Grpc::new(ready_channel); + + // We need to specify the method path. For HelloWorldService/HelloWorld, it is "/hello.HelloWorldService/HelloWorld" + let mut buf = vec![0u8; 1024]; + let slice = hello::HelloRequestBuilder::builder(&mut buf) + .name("Roto").unwrap() + .finish().unwrap(); + + let request = OwnedHelloRequest { + data: bytes::Bytes::copy_from_slice(slice), + }; + + // In tonic's Grpc client, we specify the codec separately. + let response = client + .unary( + Request::new(request), + http::uri::PathAndQuery::from_static("/hello.HelloWorldService/HelloWorld"), + RotoCodec::::default(), + ) + .await?; + + let response_msg: OwnedHelloResponse = response.into_inner(); + let reader = response_msg.reader(); + println!("Server responded: {}", reader.message().unwrap_or("No message")); + + Ok(()) +} diff --git a/examples/hello_world/src/bin/server.rs b/examples/hello_world/src/bin/server.rs new file mode 100644 index 0000000..d3d0760 --- /dev/null +++ b/examples/hello_world/src/bin/server.rs @@ -0,0 +1,165 @@ +use std::pin::Pin; +use std::future::Future; +use std::task::{Context, Poll}; +use std::sync::Arc; +use tonic::{transport::Server, Request, Response, Status}; +use roto_tonic::RotoCodec; +use hello::{HelloWorldService, OwnedHelloRequest, OwnedHelloResponse}; +use tower::Service; +use bytes::{Bytes, Buf, BufMut}; +use tonic::body::BoxBody; +use futures_util::StreamExt; +use roto_runtime::{RotoOwned, RotoMessage}; +use http_body_util::BodyExt; +use http_body::Body; + +pub mod hello { + include!("../../proto/hello.rs"); +} + +#[derive(Default, Clone)] +pub struct MyHelloWorld {} + +#[tonic::async_trait] +impl HelloWorldService for MyHelloWorld { + async fn hello_world( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let reader = req.reader(); + let name = reader.name().unwrap_or("Unknown"); + + let mut buf = vec![0u8; 1024]; + let slice = hello::HelloResponseBuilder::builder(&mut buf) + .message(&format!("Hello {}!", name)).unwrap() + .finish().unwrap(); + + let reply = OwnedHelloResponse { + data: bytes::Bytes::copy_from_slice(slice), + }; + + Ok(Response::new(reply)) + } +} + +// --- Tonic Glue --- + +#[derive(Clone)] +pub struct HelloWorldServer { + inner: Arc, +} + +impl HelloWorldServer { + pub fn new(inner: MyHelloWorld) -> Self { + Self { inner: Arc::new(inner) } + } +} + +impl tonic::server::NamedService for HelloWorldServer { + const NAME: &'static str = "hello.HelloWorldService"; +} + +struct StatusBody(Option); + +impl Body for StatusBody { + type Data = Bytes; + type Error = Status; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + if let Some(data) = self.0.take() { + Poll::Ready(Some(Ok(http_body::Frame::data(data)))) + } else { + Poll::Ready(None) + } + } +} + +impl Service> for HelloWorldServer { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + println!("Server received request: {} {}", req.method(), req.uri()); + + Box::pin(async move { + let body = req.into_body(); + let bytes_vec = body.collect().await.map_err(|e| { + println!("Body collect error: {}", e); + panic!("Body collect error: {}", e); + })?.to_bytes(); + println!("Collected body bytes: {} bytes", bytes_vec.len()); + + if bytes_vec.len() < 5 { + println!("Body too short: {} bytes", bytes_vec.len()); + let res_body = BoxBody::new(StatusBody(Some(Bytes::from(vec![0, 0, 0, 0, 0])))); + return Ok(http::Response::builder() + .status(200) + .body(res_body) + .unwrap()); + } + + let data = &bytes_vec[5..]; + println!("Decoding request from {} bytes", data.len()); + let request_msg = match OwnedHelloRequest::decode(Bytes::copy_from_slice(data)) { + Ok(msg) => msg, + Err(e) => { + println!("Decode error: {}", e); + let res_body = BoxBody::new(StatusBody(Some(Bytes::from(vec![0, 0, 0, 0, 0])))); + return Ok(http::Response::builder().status(200).body(res_body).unwrap()); + } + }; + + println!("Request decoded successfully"); + let response = match inner.hello_world(Request::new(request_msg)).await { + Ok(res) => res, + Err(e) => { + println!("Service error: {}", e); + let res_body = BoxBody::new(StatusBody(Some(Bytes::from(vec![0, 0, 0, 0, 0])))); + return Ok(http::Response::builder().status(200).body(res_body).unwrap()); + } + }; + + let response_msg = response.into_inner(); + let response_bytes = response_msg.bytes(); + println!("Service responded with {} bytes", response_bytes.len()); + + let mut res_buf = vec![0u8; 5 + response_bytes.len()]; + res_buf[0] = 0; + let len = response_bytes.len() as u32; + res_buf[1..5].copy_from_slice(&len.to_be_bytes()); + res_buf[5..].copy_from_slice(&response_bytes); + + let res_body = BoxBody::new(StatusBody(Some(Bytes::from(res_buf)))); + Ok(http::Response::builder() + .status(200) + .header("content-type", "application/grpc") + .body(res_body) + .unwrap()) + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr: std::net::SocketAddr = "[::1]:50051".parse()?; + let hello = MyHelloWorld::default(); + + println!("Server listening on {}", addr); + + Server::builder() + .add_service(HelloWorldServer::new(hello)) + .serve(addr) + .await?; + + Ok(()) +}