Skip to main content

rusteal_codegen/
lib.rs

1// rusteal-codegen: reads UHT JSON, generates Rust bindings + C++ wrappers + FuncId tables.
2
3pub mod schema;
4pub mod naming;
5pub mod config;
6pub mod context;
7pub mod type_map;
8pub mod defaults;
9pub mod filter;
10pub mod rust_gen;
11pub mod cpp_gen;
12
13use std::path::Path;
14
15use crate::config::{ProjectConfig, ProjectLayout};
16use crate::schema::{ClassesFile, EnumsFile, StructsFile};
17
18/// Run the generate command. Main entry point for codegen.
19///
20/// `project_root` is the directory holding the .uproject and `rusteal.toml`.
21pub fn run_generate(project_root: &Path) {
22    let config = ProjectConfig::load(project_root).unwrap_or_else(|e| {
23        eprintln!("rusteal-codegen: {e}");
24        std::process::exit(1);
25    });
26    let codegen = &config.codegen;
27    let layout = ProjectLayout::new(project_root);
28
29    let uht_input = layout.uht_json();
30    let classes_path = uht_input.join("rusteal_classes.json");
31    let structs_path = uht_input.join("rusteal_structs.json");
32    let enums_path = uht_input.join("rusteal_enums.json");
33    // The generated crate's directory; its sources go under `src/`.
34    let rust_out = layout.bindings_crate();
35    let rust_src = rust_out.join("src");
36    let cpp_out = layout.cpp_generated();
37
38    eprintln!("rusteal-codegen: loading JSON...");
39
40    // Parse JSON files
41    let classes_json: ClassesFile = {
42        let data = std::fs::read_to_string(&classes_path)
43            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", classes_path.display()));
44        serde_json::from_str(&data)
45            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", classes_path.display()))
46    };
47
48    let structs_json: StructsFile = {
49        let data = std::fs::read_to_string(&structs_path)
50            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", structs_path.display()));
51        serde_json::from_str(&data)
52            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", structs_path.display()))
53    };
54
55    let enums_json: EnumsFile = {
56        let data = std::fs::read_to_string(&enums_path)
57            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", enums_path.display()));
58        serde_json::from_str(&data)
59            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", enums_path.display()))
60    };
61
62    eprintln!(
63        "  Loaded {} classes, {} structs, {} enums",
64        classes_json.classes.len(),
65        structs_json.structs.len(),
66        enums_json.enums.len()
67    );
68
69    // Build context
70    let mut ctx = context::CodegenContext::new(
71        classes_json.classes,
72        structs_json.structs,
73        enums_json.enums,
74        codegen,
75    );
76
77    eprintln!(
78        "  Enabled modules: {:?}",
79        ctx.enabled_modules.iter().collect::<Vec<_>>()
80    );
81    for (module, classes) in &ctx.module_classes {
82        eprintln!("    {}: {} classes", module, classes.len());
83    }
84    for (module, structs) in &ctx.module_structs {
85        eprintln!("    {}: {} structs", module, structs.len());
86    }
87    for (module, enums) in &ctx.module_enums {
88        eprintln!("    {}: {} enums", module, enums.len());
89    }
90
91    // Apply filters
92    eprintln!("rusteal-codegen: filtering...");
93    filter::apply_filters(&mut ctx, &codegen.blocklist);
94
95    // Build function table (assign FuncIds)
96    eprintln!("rusteal-codegen: building function table...");
97    build_func_table(&mut ctx);
98    eprintln!("  {} functions in func_table", ctx.func_table.len());
99
100    // Generate the Rust bindings crate: sources, extensions, prelude, manifest.
101    eprintln!("rusteal-codegen: generating Rust code...");
102    rust_gen::generate(&ctx, &rust_src);
103    rust_gen::manual::write_manual_module(&rust_src);
104    rust_gen::prelude::write_prelude(&rust_src, &ctx);
105    rust_gen::cargo_toml::write_crate_files(&rust_out, &ctx, codegen);
106
107    // Generate C++ code
108    eprintln!("rusteal-codegen: generating C++ code...");
109    cpp_gen::generate(&ctx, &cpp_out);
110
111    // Generate module_deps.txt for Rusteal.Build.cs, and list the plugins
112    // those modules come from in Rusteal.uplugin
113    generate_module_deps(codegen, &cpp_out);
114    update_plugin_dependencies(codegen, &layout.plugin_descriptor());
115
116    // Post-generate verification
117    eprintln!("rusteal-codegen: verifying output...");
118    verify_output(&ctx, &rust_src, &cpp_out);
119
120    eprintln!("rusteal-codegen: done!");
121}
122
123/// Generate module_deps.txt listing UE module names needed by enabled features.
124fn generate_module_deps(config: &crate::config::CodegenConfig, cpp_out: &Path) {
125    use std::collections::BTreeSet;
126
127    let enabled_features: std::collections::HashSet<&str> =
128        config.features.iter().map(|s| s.as_str()).collect();
129
130    // Collect UE package names whose feature is enabled.
131    // "Core" is always needed (UE base) and not in the modules map.
132    let mut ue_modules: BTreeSet<&str> = BTreeSet::new();
133    ue_modules.insert("Core");
134    for (pkg, mapping) in &config.modules {
135        if enabled_features.contains(mapping.feature.as_str()) {
136            ue_modules.insert(pkg.as_str());
137        }
138    }
139
140    let content = ue_modules.iter().copied().collect::<Vec<_>>().join("\n");
141
142    let path = cpp_out.join("module_deps.txt");
143    std::fs::write(&path, &content)
144        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display()));
145
146    eprintln!("  module_deps.txt: {:?}", ue_modules.iter().collect::<Vec<_>>());
147}
148
149/// Add the engine plugins of the enabled modules (`plugin = "StateTree"` in
150/// `[codegen.modules]`) to the Rusteal plugin's descriptor: UBT wants a plugin
151/// to list the plugins whose modules it links. Plugins already listed stay.
152fn update_plugin_dependencies(config: &crate::config::CodegenConfig, descriptor: &Path) {
153    let Ok(text) = std::fs::read_to_string(descriptor) else {
154        return; // no installed plugin (a codegen test): nothing to update
155    };
156    let mut json: serde_json::Value = serde_json::from_str(&text)
157        .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", descriptor.display()));
158    let enabled: std::collections::HashSet<&str> =
159        config.features.iter().map(|s| s.as_str()).collect();
160    let mut wanted: Vec<&str> = config
161        .modules
162        .values()
163        .filter(|m| enabled.contains(m.feature.as_str()))
164        .filter_map(|m| m.plugin.as_deref())
165        .collect();
166    wanted.sort();
167    wanted.dedup();
168
169    let plugins = json
170        .as_object_mut()
171        .expect("Rusteal.uplugin is a JSON object")
172        .entry("Plugins")
173        .or_insert_with(|| serde_json::Value::Array(Vec::new()))
174        .as_array_mut()
175        .expect("Rusteal.uplugin's Plugins is an array");
176    let mut added = Vec::new();
177    for name in wanted {
178        if plugins.iter().any(|p| p["Name"] == name) {
179            continue;
180        }
181        plugins.push(serde_json::json!({ "Name": name, "Enabled": true }));
182        added.push(name);
183    }
184    if added.is_empty() {
185        return;
186    }
187    let mut out = serde_json::to_string_pretty(&json).expect("serializable descriptor");
188    out.push('\n');
189    std::fs::write(descriptor, out)
190        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", descriptor.display()));
191    eprintln!("  Rusteal.uplugin: now depends on {added:?}");
192}
193
194/// Verify codegen output integrity.
195fn verify_output(ctx: &context::CodegenContext, rust_out: &Path, cpp_out: &Path) {
196    let mut errors: Vec<String> = Vec::new();
197
198    // 1. FuncId contiguity: IDs must be 0..N-1 with no gaps
199    for (i, entry) in ctx.func_table.iter().enumerate() {
200        if entry.func_id != i as u32 {
201            errors.push(format!(
202                "FuncId gap: expected {} for module '{}' {}.{}, got {} (total functions: {})",
203                i, entry.module_name, entry.class_name, entry.func_name,
204                entry.func_id, ctx.func_table.len()
205            ));
206            break; // one error is enough to flag the issue
207        }
208    }
209
210    // 2. Required Rust output files exist and are non-empty
211    let rust_required = ["lib.rs", "func_ids.rs"];
212    for name in &rust_required {
213        let path = rust_out.join(name);
214        match std::fs::metadata(&path) {
215            Ok(m) if m.len() == 0 => errors.push(format!("Rust output empty: {}", path.display())),
216            Err(_) => errors.push(format!("Rust output missing: {}", path.display())),
217            _ => {}
218        }
219    }
220
221    // 3. Per-module mod.rs exists for each enabled module
222    for module_name in ctx.enabled_modules.iter() {
223        let mod_path = rust_out.join(module_name).join("mod.rs");
224        if !mod_path.exists() {
225            errors.push(format!("Module mod.rs missing: {}", mod_path.display()));
226        }
227    }
228
229    // 4. Required C++ output files exist and are non-empty
230    let cpp_required = ["RustealFuncIds.h", "RustealFillFuncTable.cpp"];
231    for name in &cpp_required {
232        let path = cpp_out.join(name);
233        match std::fs::metadata(&path) {
234            Ok(m) if m.len() == 0 => errors.push(format!("C++ output empty: {}", path.display())),
235            Err(_) => errors.push(format!("C++ output missing: {}", path.display())),
236            _ => {}
237        }
238    }
239
240    // 5. Summary stats
241    let func_count = ctx.func_table.len();
242    let module_count = ctx.enabled_modules.len();
243    let class_count: usize = ctx.module_classes.values().map(|v| v.len()).sum();
244    let struct_count: usize = ctx.module_structs.values().map(|v| v.len()).sum();
245    let enum_count: usize = ctx.module_enums.values().map(|v| v.len()).sum();
246
247    if errors.is_empty() {
248        eprintln!(
249            "  OK: {} modules, {} classes, {} structs, {} enums, {} functions",
250            module_count, class_count, struct_count, enum_count, func_count
251        );
252    } else {
253        eprintln!("  Verification FAILED:");
254        for e in &errors {
255            eprintln!("    - {e}");
256        }
257        std::process::exit(1);
258    }
259}
260
261/// Assign deterministic FuncIds to all exportable functions.
262fn build_func_table(ctx: &mut context::CodegenContext) {
263    let mut entries = Vec::new();
264
265    for (module_name, classes) in &ctx.module_classes {
266        for class in classes {
267            // Skip UInterface classes — their functions can't be called directly
268            if class.super_class.as_deref() == Some("Interface") {
269                continue;
270            }
271            for func in &class.funcs {
272                // Skip functions with unsupported param types
273                let all_supported = func.params.iter().all(|p| {
274                    type_map::map_property_type(
275                        &p.prop_type,
276                        p.class_name.as_deref(),
277                        p.struct_name.as_deref(),
278                        p.enum_name.as_deref(),
279                        p.enum_underlying_type.as_deref(),
280                        p.meta_class_name.as_deref(),
281                        p.interface_name.as_deref(),
282                    )
283                    .supported
284                });
285                if !all_supported {
286                    continue;
287                }
288                entries.push(context::FuncEntry {
289                    func_id: 0, // assigned below
290                    module_name: module_name.clone(),
291                    class_name: class.name.clone(),
292                    func_name: func.name.clone(),
293                    rust_func_name: naming::to_snake_case(&func.name),
294                    func: func.clone(),
295                    cpp_class_name: class.cpp_name.clone(),
296                    header: class.header.clone(),
297                });
298            }
299        }
300    }
301
302    // Sort by (module, class, func) for deterministic IDs
303    entries.sort_by(|a, b| {
304        a.module_name
305            .cmp(&b.module_name)
306            .then_with(|| a.class_name.cmp(&b.class_name))
307            .then_with(|| a.func_name.cmp(&b.func_name))
308    });
309
310    // Assign sequential IDs
311    for (i, entry) in entries.iter_mut().enumerate() {
312        entry.func_id = i as u32;
313    }
314
315    ctx.func_table = entries;
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn plugin_dependencies_follow_enabled_modules() {
324        let config: crate::config::CodegenConfig = toml::from_str(
325            r#"
326            features = ["engine", "state-tree"]
327            [modules]
328            Engine = { module = "engine", feature = "engine" }
329            StateTreeModule = { module = "state_tree", feature = "state-tree", plugin = "StateTree" }
330            GameplayStateTreeModule = { module = "gameplay_state_tree", feature = "state-tree", plugin = "GameplayStateTree" }
331            Niagara = { module = "niagara", feature = "niagara", plugin = "Niagara" }
332            "#,
333        )
334        .unwrap();
335        let dir = std::env::temp_dir().join(format!("rusteal-uplugin-{}", std::process::id()));
336        std::fs::create_dir_all(&dir).unwrap();
337        let descriptor = dir.join("Rusteal.uplugin");
338        std::fs::write(
339            &descriptor,
340            r#"{ "VersionName": "0.0.0", "Plugins": [ { "Name": "EnhancedInput", "Enabled": true } ] }"#,
341        )
342        .unwrap();
343
344        update_plugin_dependencies(&config, &descriptor);
345        update_plugin_dependencies(&config, &descriptor);
346
347        let json: serde_json::Value =
348            serde_json::from_str(&std::fs::read_to_string(&descriptor).unwrap()).unwrap();
349        let names: Vec<&str> = json["Plugins"]
350            .as_array()
351            .unwrap()
352            .iter()
353            .map(|p| p["Name"].as_str().unwrap())
354            .collect();
355        assert_eq!(names, ["EnhancedInput", "GameplayStateTree", "StateTree"]);
356        assert_eq!(json["VersionName"], "0.0.0");
357        std::fs::remove_dir_all(&dir).unwrap();
358    }
359}