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}