diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index baf7cc8..e9f27d2 100644 --- a/codegen/src/generator.rs +++ b/codegen/src/generator.rs @@ -20,7 +20,7 @@ use comline_codegen::{GenRequest, GeneratedFile, Mode, PackageMeta}; /// `comline-runtime` — the crate the generated RPC code links against. Pinned /// by git rev (no crates.io yet); see design/runtime-repo-structure.md. const RUNTIME_GIT: &str = "https://github.com/ComlineProject/runtime"; -const RUNTIME_REV: &str = "0501812f57817d7b11165bc6a24f54afa45acb16"; +const RUNTIME_REV: &str = "3689cfdc480dc6b183b8ca33ade0a3cdf1a0d97c"; pub fn generate_rust(req: &GenRequest) -> Result> { match req.mode { @@ -118,13 +118,24 @@ fn schema_source(units: &[FrozenUnit]) -> String { output.push_str( "use comline_runtime::client::Client;\n\ use comline_runtime::contract::{\n \ - BufMut, CallError, Dispatch, Envelope, Kind, RuntimeError, WireFormat,\n\ + BufMut, CallError, Dispatch, Envelope, Handshake, Kind, RuntimeError,\n \ + WireFormat, FRAMING_DATAGRAM,\n\ };\n\ + use comline_runtime::serve::Server;\n\ use comline_runtime::transport::Transport;\n", ); } output.push('\n'); + if has_protocol { + output.push_str(&format!( + "/// Fingerprint of the frozen IR this file was generated from — the two\n\ + /// ends of a connection [`Handshake`] must agree on it.\n\ + pub const IR_HASH: u64 = {:#018x};\n\n", + ir_hash(units) + )); + } + for unit in units { match unit { FrozenUnit::Struct { name, fields, .. } => { @@ -146,6 +157,20 @@ fn schema_source(units: &[FrozenUnit]) -> String { output } +/// A deterministic fingerprint of the schema's frozen units — FNV-1a over their +/// `Debug` form. Stable for a given generator version; a generator bump changes +/// it, which is correct (different codegen ⇒ a new handshake identity). A +/// canonical cross-language hash from `core`'s CAS, threaded through +/// `GenRequest`, is the eventual form. +fn ir_hash(units: &[FrozenUnit]) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + for b in format!("{units:?}").as_bytes() { + h ^= u64::from(*b); + h = h.wrapping_mul(0x0000_0100_0000_01b3); + } + h +} + // ── data types ───────────────────────────────────────────────────────────── fn field_lines(fields: &[FrozenUnit]) -> String { @@ -419,6 +444,20 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap l.push(" }".into()); l.push("}".into()); l.push(String::new()); + l.push(format!("impl {proto}Dispatcher {{")); + l.push(" /// Serve this protocol over `transport`, running the connection".into()); + l.push(" /// handshake (`IR_HASH` + `format`'s name) against the peer first.".into()); + l.push( + " pub fn serve(self, transport: &mut T, format: W)".into(), + ); + l.push(" -> Result<(), RuntimeError> {".into()); + l.push( + " let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(), + ); + l.push(" Server::new(self, format).serve_handshaked(transport, hs)".into()); + l.push(" }".into()); + l.push("}".into()); + l.push(String::new()); l.push(String::new()); s.push_str(&l.join("\n")); @@ -432,6 +471,16 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap l.push(" pub fn new(client: Client) -> Self {".into()); l.push(" Self(client)".into()); l.push(" }".into()); + l.push(String::new()); + l.push(" /// Bind + run the connection handshake against the provider.".into()); + l.push( + " pub fn connect(transport: T, format: W) -> Result {".into(), + ); + l.push( + " let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);".into(), + ); + l.push(" Ok(Self(Client::connect(transport, format, hs)?))".into()); + l.push(" }".into()); for (i, f) in fns.iter().enumerate() { let args = sig_args(&f.args); let param_expr = match &f.params_ty { diff --git a/codegen/tests/generate.rs b/codegen/tests/generate.rs index 8157547..6d53108 100644 --- a/codegen/tests/generate.rs +++ b/codegen/tests/generate.rs @@ -121,6 +121,25 @@ fn code_mode_generates_enum_and_protocol() { assert!(src.contains( "pub fn get_user(&mut self, id: i32) -> Result>" )); + // handshake: an IR fingerprint + connect / serve helpers + assert!(src.contains("pub const IR_HASH: u64 = 0x")); + assert!(src.contains("pub fn connect(transport: T, format: W) -> Result")); + assert!(src.contains( + "let hs = Handshake::new(IR_HASH, format.name(), FRAMING_DATAGRAM, 0);" + )); + assert!(src.contains("impl UserServiceDispatcher {")); + assert!(src.contains( + "pub fn serve(self, transport: &mut T, format: W)" + )); + assert!(src.contains("Server::new(self, format).serve_handshaked(transport, hs)")); +} + +#[test] +fn a_schema_without_a_protocol_has_no_ir_hash() { + let schemas = vec![("plain".to_string(), vec![user_struct()])]; + let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents; + assert!(!src.contains("IR_HASH")); + assert!(!src.contains("comline_runtime")); } #[test]