Proposal D — target-language plugins
Status: under discussion · Blocked by a current constraint, not rejected · See the section overview for the question all five answer
The idea: Comline stops generating code. It compiles schemas, serialises
the request, and hands it to a plugin program — which may be written in any
language, most usefully the target's own. protoc's model.
Shape
comline generate
├─ compile schemas -> frozen IR
├─ serialise GenRequest (CBOR — core already speaks it)
└─ for each target:
spawn `comline-gen-<lang>`, write request to stdin,
read a file set back from stdout, write it to disk
comline-gen-typescript/ a Node package using ts-morph or the
TypeScript compiler's own factory API
comline-gen-python/ a Python package using ast / libcst
comline-gen-rust/ stays Rust, built in-process as today
The plugin contract becomes a versioned wire format rather than a Rust
trait: the same role GenRequest plays now, but serialisable and stable
across languages.
The case for it
- Each generator is written where its expertise lives. A TypeScript
emitter authored in TypeScript can use
ts-morphor the compiler's factory API — strictly better tools than anything reachable from Rust, and the people most able to judge idiomatic output can contribute without learning Rust. - It retires the pin cascade. Today
comline-core's rev is pinned at fourteen sites across seven repositories —generationalone holds five — and every generator change walks an ordered cascade, three of them in a single working session. Cargo refuses to unify two git dependencies unless the rev strings match byte for byte, so this is structural, not sloppiness. A wire contract replaces it with ordinary version negotiation. - Release cycles decouple. A generator ships on its own schedule, and a new language needs no change to the CLI, no cargo feature, and no coordination.
- It is the proven path to breadth. Every schema compiler that reached many languages did this.
- It composes with A. The resolved model can be part of the serialised request, so plugins inherit the resolution work instead of each re-deriving it — which is the failure mode of protoc plugins, where each re-invents naming and imports.
The case against
- The wasm playground blocks it.
playground/wasmcompiles the generators to wasm32 and runs them in the browser, on purpose — its manifest states the playground runs that code, "not a re-implementation." A browser cannot spawncomline-gen-typescript. Keeping the playground would mean either a server round-trip, abandoning the "same code" guarantee, or shipping each plugin as a wasm module — which the component model may eventually make reasonable, but not comfortably today. - It trades one install problem for another.
comline generatecurrently needs nothing but itself. Under D, generating TypeScript needs Node present and the right plugin version installed. For a tool whose selling point is a single binary, that is a real regression in the common case. - A wire contract is a much harder thing to change than a Rust trait.
GenRequesthas gained fields three times recently —default_framing,lang_version,external_std— each a one-line change with a compiler to catch every consumer. As a cross-language wire format, each of those is a compatibility negotiation. - It is the largest rework of the five, and it is mostly protocol engineering rather than better generated code.
- Fewer shared invariants. The accepted split-by-language proposal put codegen and runtime in one repo precisely so their agreement is a same-CI invariant. Plugins in different languages weaken that.
Example projects
| Project | Plugins? | Notes |
|---|---|---|
| Protocol Buffers / Buf | yes | CodeGeneratorRequest on stdin; plugins in Go, TypeScript, Rust, Swift, …; the canonical design |
| Cap'n Proto | yes | capnpc-<lang> executables; capnpc-rust is Rust |
| gRPC | yes | per-language plugins alongside protoc |
| Smithy | no | one JVM framework generating every target |
| wit-bindgen | no (in-process) | a Rust core with per-language bindgen crates — closer to A, and notably the most recent of these designs |
Worth noting that the newest entrant, built for a polyglot wasm world, chose the in-process kernel rather than the plugin protocol.
When this would be the right call
When the number of target languages passes the point where Rust-authored emitters stop being credible — plausibly around five or six, or the first time a language's idioms need a native expert — and the playground requirement has been resolved, either by accepting a server round-trip or by wasm plugins becoming routine.
Until then the useful action is preparation, not adoption: keep the kernel's request boundary serialisable, so this stays a port rather than a rewrite.