The current build.rs-based inclusion pattern is functional but reads badly:
pub mod proto {
include!(concat!(env!(\"OUT_DIR\"), \"/_connectrpc.rs\"));
}
Three nested macros to express "include the file the build script wrote." Every example in the repo (examples/streaming-tour, examples/middleware, tests/streaming) has this same triple, and every adopter using connectrpc-build writes it themselves.
Proposal
Ship a tiny macro in the connectrpc crate that hides the cascade:
#[macro_export]
macro_rules! include_generated {
() => {
include!(concat!(env!(\"OUT_DIR\"), \"/_connectrpc.rs\"));
};
($file:literal) => {
include!(concat!(env!(\"OUT_DIR\"), \"/\", $file));
};
}
Call site becomes:
pub mod proto {
connectrpc::include_generated!();
}
The optional $file arg supports projects that customized the include_file() name in their build.rs config.
Why not a single unified macro across both codegen paths
A single macro that works identically for both build.rs users and buf generate users isn't feasible because the two paths use different rustc-level inclusion mechanisms:
| Approach |
Output location |
Inclusion mechanism |
Why |
build.rs |
$OUT_DIR/... (build-hash path) |
include!(concat!(env!(\"OUT_DIR\"), ...)) |
OUT_DIR isn't a stable literal; macros can compose it at compile time, but #[path] can't |
buf generate |
src/generated/... (known path) |
#[path = \"generated/proto/mod.rs\"] pub mod proto; |
path is a literal; modules navigate normally; multi-file structure preserved |
#[path] requires a string literal at parse time and can't take concat!(env!(...)) as input, so anything written to OUT_DIR is forced down the include! path. Anything at a known relative path can use #[path] directly.
The pragmatic answer is matched-name macros that minimize the visible difference at the call site, plus side-by-side documentation:
// build.rs users:
pub mod proto { connectrpc::include_generated!(); }
// buf generate users:
#[path = \"generated/proto/mod.rs\"] pub mod proto;
Two lines, parallel structure, the underlying difference (OUT_DIR vs src) is honest and visible.
Scope
- Add the macro to the
connectrpc crate root (no feature flag - it's just a macro_rules!, no compile-time cost when unused).
- Update
examples/streaming-tour, examples/middleware, tests/streaming, and the connectrpc-build crate docs to use the new macro.
- Add a Code Generation section in
docs/guide.md showing both inclusion patterns side-by-side so users can see the parallel.
- Existing call sites continue to work - the macro is purely additive.
Alternatives considered
- Context-detecting unified macro: have the macro detect which path is in use via env var or proc-macro filesystem inspection. Adds magic, fragile, hard to debug.
- Build.rs writes to src/ instead of OUT_DIR: against Cargo conventions, would require workspace-wide
.gitignore changes, risks generated code drifting from source.
- Per-package output files (tonic-style): change
connectrpc-build from single include_file mode to multiple per-package files. Bigger structural change, separable from this issue.
Refs
- Existing call sites:
examples/streaming-tour/src/server.rs:23-25, examples/middleware/src/server.rs:34-36, tests/streaming/src/lib.rs
- Tonic does the same thing with
tonic::include_proto!(\"package.name\") - this issue mirrors that ergonomic affordance
The current build.rs-based inclusion pattern is functional but reads badly:
Three nested macros to express "include the file the build script wrote." Every example in the repo (
examples/streaming-tour,examples/middleware,tests/streaming) has this same triple, and every adopter usingconnectrpc-buildwrites it themselves.Proposal
Ship a tiny macro in the
connectrpccrate that hides the cascade:Call site becomes:
The optional
$filearg supports projects that customized theinclude_file()name in theirbuild.rsconfig.Why not a single unified macro across both codegen paths
A single macro that works identically for both
build.rsusers andbuf generateusers isn't feasible because the two paths use different rustc-level inclusion mechanisms:build.rs$OUT_DIR/...(build-hash path)include!(concat!(env!(\"OUT_DIR\"), ...))#[path]can'tbuf generatesrc/generated/...(known path)#[path = \"generated/proto/mod.rs\"] pub mod proto;#[path]requires a string literal at parse time and can't takeconcat!(env!(...))as input, so anything written to OUT_DIR is forced down theinclude!path. Anything at a known relative path can use#[path]directly.The pragmatic answer is matched-name macros that minimize the visible difference at the call site, plus side-by-side documentation:
Two lines, parallel structure, the underlying difference (OUT_DIR vs src) is honest and visible.
Scope
connectrpccrate root (no feature flag - it's just amacro_rules!, no compile-time cost when unused).examples/streaming-tour,examples/middleware,tests/streaming, and theconnectrpc-buildcrate docs to use the new macro.docs/guide.mdshowing both inclusion patterns side-by-side so users can see the parallel.Alternatives considered
.gitignorechanges, risks generated code drifting from source.connectrpc-buildfrom singleinclude_filemode to multiple per-package files. Bigger structural change, separable from this issue.Refs
examples/streaming-tour/src/server.rs:23-25,examples/middleware/src/server.rs:34-36,tests/streaming/src/lib.rstonic::include_proto!(\"package.name\")- this issue mirrors that ergonomic affordance