Skip to main content

imping_codegen/
lib.rs

1//! Generates Rust bindings from imping IR.
2//!
3//! [`Generator`] turns an [`imping_ir::Ir`] into formatted Rust source, one
4//! `pub mod` per IR module, and returns it as [`Bindings`] with any
5//! [`Warning`]s. The generated code depends only on `imping-runtime`.
6//!
7//! # Examples
8//!
9//! ```
10//! let ir = imping_ir::Ir::from_json_str(r#"{
11//!     "format_version": { "major": 1, "minor": 0 },
12//!     "modules": [{
13//!         "name": "target.exe", "architecture": "x64",
14//!         "image_base": "0x140000000", "timestamp": "0x64b8f2a1", "image_size": "0xa2b000",
15//!         "types": [
16//!             { "id": 0, "name": "float", "kind": "primitive", "primitive": "f32" },
17//!             { "id": 1, "name": "Vector2", "kind": "struct", "size": 8, "align": 4,
18//!               "fields": [
19//!                 { "name": "x", "type": 0, "offset": 0 },
20//!                 { "name": "y", "type": 0, "offset": 4 }
21//!               ] }
22//!         ]
23//!     }]
24//! }"#)?;
25//!
26//! let bindings = imping_codegen::Generator::new().generate(&[("steam", &ir)])?;
27//! assert!(bindings.source().contains("pub mod target"));
28//! assert!(bindings.source().contains("pub struct Vector2"));
29//! assert!(bindings.warnings().is_empty());
30//! # Ok::<(), Box<dyn std::error::Error>>(())
31//! ```
32//!
33//! # Several builds
34//!
35//! Each IR file describes one build of the target, and
36//! [`generate`](Generator::generate) takes them all, each with a label such
37//! as `steam_2021`. One set of bindings then serves every build: types,
38//! functions and globals are matched by namespace and name, must agree on
39//! their layouts and signatures, and keep each build's address. An item only
40//! some IRs have is generated too; on the other builds it's found by another
41//! build's byte pattern.
42//!
43//! # Generated code
44//!
45//! - Each module gets a `MODULE` identity, a `Build` enum with a variant per
46//!   label, an `init()` that checks the running build and returns which it
47//!   is, and `build()`.
48//! - Types, functions and globals go in one module per namespace
49//!   (`target::hw::cVec4` for the type `Hw::cVec4`).
50//! - Structs become `#[repr(C)]` with explicit padding, bitfield accessors and
51//!   compile-time checks on size, alignment and every field offset; unions get
52//!   the same checks.
53//! - A struct whose IR field is marked as its base class (a struct at offset
54//!   0) derefs to that base and implements `imping_runtime::Inherits` for it
55//!   and the base's own bases, so `upcast()` turns pointers into base pointers.
56//! - Enums become integer newtypes with associated constants, typedefs become
57//!   type aliases, and opaque types become byte arrays or zero-sized markers.
58//! - Functions become an `unsafe fn` plus a module of the same name with the
59//!   function's address in each build (`RVA_STEAM_2021`), `rva()` for the
60//!   loaded build, `Signature`, `ptr()` and `hook()`. A hook's closure gets
61//!   the function's `Original` first; `original.forward()` safely makes the
62//!   call the target was making. A parameter named like a static or enum type
63//!   in its module gets a trailing `_`.
64//! - Functions tagged `imping-safe` get a safe `fn` whose pointer parameters
65//!   are references, unless a parameter has no safe form (a [`Warning`] says
66//!   so). The tag means the function is always fine to call with valid
67//!   arguments and keeps none of its pointer arguments after returning.
68//! - Globals become `imping_runtime::Global` statics. Structs, unions and enums
69//!   whose every bit pattern is valid implement `imping_runtime::Plain`, which
70//!   makes their globals safe to read and write.
71
72mod emit;
73mod error;
74mod format;
75mod functions;
76mod layout;
77mod merge;
78mod names;
79mod namespaces;
80mod types;
81
82pub use crate::error::{BuildsDiffer, Error, Warning};
83
84use imping_ir::{Ir, Module};
85use std::collections::HashMap;
86
87/// Generates Rust bindings from the [`Ir`] of one or more builds.
88///
89/// Create one with [`Generator::new`], optionally configure it, then call
90/// [`generate`](Self::generate).
91///
92/// # Examples
93///
94/// ```
95/// let generator = imping_codegen::Generator::new()
96///     .module_name("TargetCore.dll", "target")
97///     .accept_build("steam", "TargetCore.dll", 0x64b8f2a1, 0xa3c000);
98/// ```
99#[derive(Debug, Clone, Default)]
100pub struct Generator {
101    /// IR module name -> Rust module name overrides, in call order.
102    module_names: Vec<(String, String)>,
103    /// Extra accepted builds: build label, IR module name, timestamp, image
104    /// size.
105    accepted_builds: Vec<(String, String, u32, u32)>,
106}
107
108impl Generator {
109    /// Creates a generator with default settings.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Names the Rust module generated for the IR module `ir_name`.
115    ///
116    /// By default it's the IR module's file name without the extension, in
117    /// snake case: `TargetCore.dll` becomes `target_core`. If the same
118    /// `ir_name` is given more than once, the last call wins.
119    pub fn module_name(mut self, ir_name: impl Into<String>, rust_name: impl Into<String>) -> Self {
120        self.module_names.push((ir_name.into(), rust_name.into()));
121        self
122    }
123
124    /// Makes the generated `init()` of the IR module `ir_name` also accept, as
125    /// the build labelled `label`, the image with this timestamp and image
126    /// size.
127    ///
128    /// Meant for the same code in a different image, such as an executable
129    /// wrapped by a packer or DRM layer that keeps the timestamp but grows the
130    /// image. The bindings promise the IR describes every build they accept, so
131    /// only add images whose code and data match the analysed one.
132    pub fn accept_build(
133        mut self,
134        label: impl Into<String>,
135        ir_name: impl Into<String>,
136        timestamp: u32,
137        image_size: u32,
138    ) -> Self {
139        self.accepted_builds
140            .push((label.into(), ir_name.into(), timestamp, image_size));
141        self
142    }
143
144    /// Generates one set of bindings for every module in `builds`, the IR of
145    /// each build with its label.
146    ///
147    /// Validates each IR first. A module's items are matched across the
148    /// builds that have it; see [Several builds](crate#several-builds).
149    /// Anything the generator can't represent becomes a [`Warning`] instead
150    /// of an error. With no builds, the bindings are empty.
151    ///
152    /// # Errors
153    ///
154    /// - [`Error::InvalidBuildLabel`] if a label isn't lowercase letters,
155    ///   digits and single underscores, starting with a letter.
156    /// - [`Error::DuplicateBuild`] if two builds have the same label, or
157    ///   labels that make the same `Build` variant.
158    /// - [`Error::InvalidIr`] if an IR fails [`Ir::validate`].
159    /// - [`Error::UnknownBuild`] if an [`accept_build`](Self::accept_build)
160    ///   names a label none of `builds` has.
161    /// - [`Error::UnknownModule`] if a [`module_name`](Self::module_name) names
162    ///   a module no IR has, or an [`accept_build`](Self::accept_build) one its
163    ///   build's IR doesn't have.
164    /// - [`Error::InvalidModuleName`] if a [`module_name`](Self::module_name)
165    ///   isn't a valid Rust module name.
166    /// - [`Error::ModuleNameClash`] if two IR modules get the same Rust module
167    ///   name.
168    /// - [`Error::BuildsDiffer`] if two builds disagree on a type, a function's
169    ///   signature or a global's type.
170    pub fn generate(&self, builds: &[(&str, &Ir)]) -> Result<Bindings, Error> {
171        let mut variants: HashMap<String, &str> = HashMap::new();
172        for (label, ir) in builds {
173            if !names::is_build_label(label) {
174                return Err(Error::InvalidBuildLabel {
175                    label: (*label).to_owned(),
176                });
177            }
178            if variants
179                .insert(names::build_variant(label), label)
180                .is_some()
181            {
182                return Err(Error::DuplicateBuild {
183                    label: (*label).to_owned(),
184                });
185            }
186            ir.validate().map_err(Error::InvalidIr)?;
187        }
188
189        // Each module name, in the order first seen, with the builds that have it.
190        let mut order: Vec<&str> = Vec::new();
191        let mut per_module: HashMap<&str, Vec<(usize, &Module)>> = HashMap::new();
192        for (build, (_, ir)) in builds.iter().enumerate() {
193            for module in &ir.modules {
194                let entry = per_module.entry(module.name.as_str()).or_insert_with(|| {
195                    order.push(module.name.as_str());
196                    Vec::new()
197                });
198                entry.push((build, module));
199            }
200        }
201
202        let mut overrides: HashMap<&str, &str> = HashMap::new();
203        for (ir_name, rust_name) in &self.module_names {
204            if !per_module.contains_key(ir_name.as_str()) {
205                return Err(Error::UnknownModule {
206                    option: "module_name",
207                    name: ir_name.clone(),
208                });
209            }
210            if !names::is_plain_identifier(rust_name) {
211                return Err(Error::InvalidModuleName {
212                    name: rust_name.clone(),
213                });
214            }
215            overrides.insert(ir_name, rust_name);
216        }
217
218        // Per module: (build, timestamp, image size), the build as an index
219        // into `builds`.
220        let mut extra_builds: HashMap<&str, Vec<(usize, u32, u32)>> = HashMap::new();
221        for (label, ir_name, timestamp, image_size) in &self.accepted_builds {
222            let Some(build) = builds.iter().position(|(given, _)| given == label) else {
223                return Err(Error::UnknownBuild {
224                    label: label.clone(),
225                });
226            };
227            if !builds[build]
228                .1
229                .modules
230                .iter()
231                .any(|module| module.name == *ir_name)
232            {
233                return Err(Error::UnknownModule {
234                    option: "accept_build",
235                    name: ir_name.clone(),
236                });
237            }
238            extra_builds
239                .entry(ir_name)
240                .or_default()
241                .push((build, *timestamp, *image_size));
242        }
243
244        let mut taken: HashMap<String, &str> = HashMap::new();
245        let mut rust_names = Vec::with_capacity(order.len());
246        for name in &order {
247            let rust_name = match overrides.get(name) {
248                Some(rust_name) => (*rust_name).to_owned(),
249                None => names::module_name(name),
250            };
251            if let Some(first) = taken.insert(rust_name.clone(), name) {
252                return Err(Error::ModuleNameClash {
253                    first: first.to_owned(),
254                    second: (*name).to_owned(),
255                    rust_name,
256                });
257            }
258            rust_names.push(rust_name);
259        }
260
261        let mut warnings = Vec::new();
262        let mut items = Vec::with_capacity(order.len());
263        for (name, rust_name) in order.iter().zip(&rust_names) {
264            let present = &per_module[name];
265            let labelled: Vec<(&str, &Module)> = present
266                .iter()
267                .map(|&(build, module)| (builds[build].0, module))
268                .collect();
269            // Accepted images as indices into this module's builds.
270            let extra: Vec<(usize, u32, u32)> = extra_builds
271                .get(name)
272                .map_or(&[][..], Vec::as_slice)
273                .iter()
274                .filter_map(|&(build, timestamp, image_size)| {
275                    let index = present.iter().position(|&(present, _)| present == build)?;
276                    Some((index, timestamp, image_size))
277                })
278                .collect();
279            let merged = merge::merge(&labelled, &extra)?;
280            items.push(emit::module(&merged, rust_name, &mut warnings));
281        }
282        let source = format::render(quote::quote! { #(#items)* });
283        Ok(Bindings { source, warnings })
284    }
285}
286
287/// Generated Rust source plus the warnings raised while generating it.
288///
289/// Returned by [`Generator::generate`].
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct Bindings {
292    source: String,
293    warnings: Vec<Warning>,
294}
295
296impl Bindings {
297    /// Returns the generated Rust source, formatted.
298    pub fn source(&self) -> &str {
299        &self.source
300    }
301
302    /// Consumes the bindings and returns the generated Rust source.
303    pub fn into_source(self) -> String {
304        self.source
305    }
306
307    /// Returns everything the generator skipped or simplified, in the order
308    /// found.
309    pub fn warnings(&self) -> &[Warning] {
310        &self.warnings
311    }
312}