diff --git a/tools/rust/xenbindgen/src/c_lang.rs b/tools/rust/xenbindgen/src/c_lang.rs
new file mode 100644
index 000000000000..f05e36bb362f
--- /dev/null
+++ b/tools/rust/xenbindgen/src/c_lang.rs
@@ -0,0 +1,73 @@
+//! C backend
+//!
+//! A backend for the C programming language. Enums and bitmaps appear as their
+//! backing primitive type. This is in order to mandate size and alignment at
+//! the ABI boundary.
+//!
+//! Otherwise, enums and struct are declared with their native C counterparts,
+//! whereas bitmaps are declared as `#define` items.
+//!
+//! There's an expectation that the supporting library will have the
+//! `(u)int64_aligned_t` types and `XEN_GUEST_HANDLE_64()`, These are important
+//! in order to allow 32bit domains to interact with 64bit hypervisors.
+//!
+//! As far as definitions go, `enums` are stored in native `enums`, but bitmaps
+//! are given in `#define` instead, with an empty struct on top to provide grep
+//! fodder and a tag to follow using an LSP, global, cscope, etc.
+
+use std::fmt::Write;
+
+use crate::spec::OutFileDef;
+
+use convert_case::{Case, Casing};
+
+/// An abstract indentation level. 0 is no indentation, 1 is [`INDENT_WIDTH`]
+/// and so on.
+#[derive(Copy, Clone)]
+struct Indentation(usize);
+
+/// Default width of each level of indentation
+const INDENT_WIDTH: usize = 4;
+
+/// Add a comment to a struct or a field.
+fn comment(out: &mut String, comment: &str, ind: Indentation) {
+    let spaces = " ".repeat(INDENT_WIDTH * ind.0);
+
+    if comment.contains('\n') {
+        writeln!(out, "{spaces}/*").unwrap();
+        for line in comment.split('\n') {
+            write!(out, "{spaces} *").unwrap();
+            if !line.is_empty() {
+                write!(out, " {line}").unwrap();
+            }
+            writeln!(out).unwrap();
+        }
+        writeln!(out, "{spaces} */").unwrap();
+    } else {
+        writeln!(out, "{spaces}/* {comment} */").unwrap();
+    }
+}
+
+/// Generates a single `.h` file.
+///
+/// `filedef` is a language-agnostic high level description of what the output
+/// must contain. The function returns the contents of the new
+///
+/// # Aborts
+/// Aborts the process with `rc=1` on known illegal specifications.
+pub fn parse(filedef: &OutFileDef) -> String {
+    let mut out = String::new();
+    let name = filedef
+        .name
+        .from_case(Case::Kebab)
+        .to_case(Case::UpperSnake);
+    let hdr = format!("{}\n\nAUTOGENERATED. DO NOT MODIFY", filedef.name);
+
+    comment(&mut out, &hdr, Indentation(0));
+    writeln!(out, "#ifndef __XEN_AUTOGEN_{name}_H").unwrap();
+    writeln!(out, "#define __XEN_AUTOGEN_{name}_H\n").unwrap();
+
+    writeln!(out, "#endif /* __XEN_AUTOGEN_{name}_H */\n").unwrap();
+
+    out
+}
diff --git a/tools/rust/xenbindgen/src/main.rs b/tools/rust/xenbindgen/src/main.rs
index 497cf39d3bbd..00abf5ed7f33 100644
--- a/tools/rust/xenbindgen/src/main.rs
+++ b/tools/rust/xenbindgen/src/main.rs
@@ -4,11 +4,15 @@
 
 mod spec;
 
-use std::path::PathBuf;
+mod c_lang;
+
+use std::{io::Write, path::PathBuf};
 
 use clap::Parser;
+use convert_case::{Case, Casing};
 use env_logger::Env;
 use log::{error, info};
+use spec::OutFileDef;
 
 /// A CLI tool to generate struct definitions in several languages.
 #[derive(Parser, Debug)]
@@ -17,6 +21,20 @@ struct Cli {
     /// Path to the input directory containing the hypercall specification
     #[arg(short, long)]
     indir: PathBuf,
+    /// Path to the output directory for the generated bindings.
+    #[arg(short, long)]
+    outdir: PathBuf,
+    /// Target language for the contents of `outdir`.
+    #[arg(short, long, value_enum)]
+    lang: Lang,
+}
+
+/// Supported target languages
+#[derive(clap::ValueEnum, Clone, Debug)]
+#[clap(rename_all = "kebab_case")]
+enum Lang {
+    #[doc(hidden)]
+    C,
 }
 
 fn main() {
@@ -25,7 +43,7 @@ fn main() {
     let cli = Cli::parse();
     info!("args: {:?}", cli);
 
-    let _specification = match spec::Spec::new(&cli.indir) {
+    let specification = match spec::Spec::new(&cli.indir) {
         Ok(x) => x,
         Err(spec::Error::Toml(x)) => {
             error!("TOML parsing error:");
@@ -39,5 +57,35 @@ fn main() {
         }
     };
 
-    todo!("generate output files");
+    let (extension, parser): (&str, fn(&OutFileDef) -> String) = match cli.lang {
+        Lang::C => (".h", c_lang::parse),
+    };
+
+    if let Err(x) = std::fs::create_dir_all(&cli.outdir) {
+        error!("Can't create outdir {:?}: {x}", cli.outdir);
+        std::process::exit(1);
+    }
+
+    for outfile in &specification.0 {
+        let mut path = cli.outdir.clone();
+        let name = outfile.name.from_case(Case::Kebab).to_case(Case::Snake);
+        path.push(format!("{name}{extension}"));
+
+        info!("Generating {path:?}");
+
+        // Parse the input file before creating the output
+        let output = parser(outfile);
+
+        let Ok(mut file) = std::fs::OpenOptions::new()
+            .write(true)
+            .create(true)
+            .truncate(true)
+            .open(path)
+        else {
+            error!("Can't open {}", outfile.name);
+            std::process::exit(1);
+        };
+
+        file.write_all(output.as_bytes()).unwrap();
+    }
 }
diff --git a/tools/rust/xenbindgen/src/spec.rs b/tools/rust/xenbindgen/src/spec.rs
index e69f7c78dc7a..08c4dc3a7eba 100644
--- a/tools/rust/xenbindgen/src/spec.rs
+++ b/tools/rust/xenbindgen/src/spec.rs
@@ -40,7 +40,7 @@ impl OutFileDef {
     pub fn new(name: String, dir: &Path) -> Result<Self, Error> {
         info!("Reading {dir:?} to generate an output file");
 
-        let mut ret = Self { name };
+        let ret = Self { name };
 
         for entry in from_ioerr(dir.read_dir())? {
             let path = from_ioerr(entry)?.path();
