Skip to content

Ship include_generated!() macro to clean up build.rs-based source inclusion #50

@iainmcgin

Description

@iainmcgin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions