Skip to main content

uika_codegen/
lib.rs

1// uika-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, PathBuf};
14
15use crate::config::UikaConfig;
16use crate::schema::{ClassesFile, EnumsFile, StructsFile};
17
18/// Run the generate command. Main entry point for codegen.
19pub fn run_generate(config_path: &Path) {
20    // Load config
21    let config_str = std::fs::read_to_string(config_path)
22        .unwrap_or_else(|e| panic!("Failed to read {}: {e}", config_path.display()));
23    let uika_config: UikaConfig = toml::from_str(&config_str)
24        .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", config_path.display()));
25    let codegen = &uika_config.codegen;
26
27    // Derive JSON paths from config
28    let uht_input = PathBuf::from(&codegen.paths.uht_input);
29    let classes_path = uht_input.join("uika_classes.json");
30    let structs_path = uht_input.join("uika_structs.json");
31    let enums_path = uht_input.join("uika_enums.json");
32    let rust_out = PathBuf::from(&codegen.paths.rust_out);
33    let cpp_out = PathBuf::from(&codegen.paths.cpp_out);
34
35    eprintln!("uika-codegen: loading JSON...");
36
37    // Parse JSON files
38    let classes_json: ClassesFile = {
39        let data = std::fs::read_to_string(&classes_path)
40            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", classes_path.display()));
41        serde_json::from_str(&data)
42            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", classes_path.display()))
43    };
44
45    let structs_json: StructsFile = {
46        let data = std::fs::read_to_string(&structs_path)
47            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", structs_path.display()));
48        serde_json::from_str(&data)
49            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", structs_path.display()))
50    };
51
52    let enums_json: EnumsFile = {
53        let data = std::fs::read_to_string(&enums_path)
54            .unwrap_or_else(|e| panic!("Failed to read {}: {e}", enums_path.display()));
55        serde_json::from_str(&data)
56            .unwrap_or_else(|e| panic!("Failed to parse {}: {e}", enums_path.display()))
57    };
58
59    eprintln!(
60        "  Loaded {} classes, {} structs, {} enums",
61        classes_json.classes.len(),
62        structs_json.structs.len(),
63        enums_json.enums.len()
64    );
65
66    // Build context
67    let mut ctx = context::CodegenContext::new(
68        classes_json.classes,
69        structs_json.structs,
70        enums_json.enums,
71        codegen,
72    );
73
74    eprintln!(
75        "  Enabled modules: {:?}",
76        ctx.enabled_modules.iter().collect::<Vec<_>>()
77    );
78    for (module, classes) in &ctx.module_classes {
79        eprintln!("    {}: {} classes", module, classes.len());
80    }
81    for (module, structs) in &ctx.module_structs {
82        eprintln!("    {}: {} structs", module, structs.len());
83    }
84    for (module, enums) in &ctx.module_enums {
85        eprintln!("    {}: {} enums", module, enums.len());
86    }
87
88    // Apply filters
89    eprintln!("uika-codegen: filtering...");
90    filter::apply_filters(&mut ctx, &codegen.blocklist);
91
92    // Build function table (assign FuncIds)
93    eprintln!("uika-codegen: building function table...");
94    build_func_table(&mut ctx);
95    eprintln!("  {} functions in func_table", ctx.func_table.len());
96
97    // Generate Rust code
98    eprintln!("uika-codegen: generating Rust code...");
99    rust_gen::generate(&ctx, &rust_out);
100
101    // Generate C++ code
102    eprintln!("uika-codegen: generating C++ code...");
103    cpp_gen::generate(&ctx, &cpp_out);
104
105    // Generate module_deps.txt for Uika.Build.cs
106    generate_module_deps(codegen, &cpp_out);
107
108    // Post-generate verification
109    eprintln!("uika-codegen: verifying output...");
110    verify_output(&ctx, &rust_out, &cpp_out);
111
112    eprintln!("uika-codegen: done!");
113}
114
115/// Generate module_deps.txt listing UE module names needed by enabled features.
116fn generate_module_deps(config: &crate::config::CodegenConfig, cpp_out: &Path) {
117    use std::collections::BTreeSet;
118
119    let enabled_features: std::collections::HashSet<&str> =
120        config.features.iter().map(|s| s.as_str()).collect();
121
122    // Collect UE package names whose feature is enabled.
123    // "Core" is always needed (UE base) and not in the modules map.
124    let mut ue_modules: BTreeSet<&str> = BTreeSet::new();
125    ue_modules.insert("Core");
126    for (pkg, mapping) in &config.modules {
127        if enabled_features.contains(mapping.feature.as_str()) {
128            ue_modules.insert(pkg.as_str());
129        }
130    }
131
132    let content = ue_modules
133        .iter()
134        .map(|m| *m)
135        .collect::<Vec<_>>()
136        .join("\n");
137
138    let path = cpp_out.join("module_deps.txt");
139    std::fs::write(&path, &content)
140        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display()));
141
142    eprintln!("  module_deps.txt: {:?}", ue_modules.iter().collect::<Vec<_>>());
143}
144
145/// Verify codegen output integrity.
146fn verify_output(ctx: &context::CodegenContext, rust_out: &Path, cpp_out: &Path) {
147    let mut errors: Vec<String> = Vec::new();
148
149    // 1. FuncId contiguity: IDs must be 0..N-1 with no gaps
150    for (i, entry) in ctx.func_table.iter().enumerate() {
151        if entry.func_id != i as u32 {
152            errors.push(format!(
153                "FuncId gap: expected {} for {}.{}, got {}",
154                i, entry.class_name, entry.func_name, entry.func_id
155            ));
156            break; // one error is enough to flag the issue
157        }
158    }
159
160    // 2. Required Rust output files exist and are non-empty
161    let rust_required = ["lib.rs", "func_ids.rs"];
162    for name in &rust_required {
163        let path = rust_out.join(name);
164        match std::fs::metadata(&path) {
165            Ok(m) if m.len() == 0 => errors.push(format!("Rust output empty: {}", path.display())),
166            Err(_) => errors.push(format!("Rust output missing: {}", path.display())),
167            _ => {}
168        }
169    }
170
171    // 3. Per-module mod.rs exists for each enabled module
172    for module_name in ctx.enabled_modules.iter() {
173        let mod_path = rust_out.join(module_name).join("mod.rs");
174        if !mod_path.exists() {
175            errors.push(format!("Module mod.rs missing: {}", mod_path.display()));
176        }
177    }
178
179    // 4. Required C++ output files exist and are non-empty
180    let cpp_required = ["UikaFuncIds.h", "UikaFillFuncTable.cpp"];
181    for name in &cpp_required {
182        let path = cpp_out.join(name);
183        match std::fs::metadata(&path) {
184            Ok(m) if m.len() == 0 => errors.push(format!("C++ output empty: {}", path.display())),
185            Err(_) => errors.push(format!("C++ output missing: {}", path.display())),
186            _ => {}
187        }
188    }
189
190    // 5. Summary stats
191    let func_count = ctx.func_table.len();
192    let module_count = ctx.enabled_modules.len();
193    let class_count: usize = ctx.module_classes.values().map(|v| v.len()).sum();
194    let struct_count: usize = ctx.module_structs.values().map(|v| v.len()).sum();
195    let enum_count: usize = ctx.module_enums.values().map(|v| v.len()).sum();
196
197    if errors.is_empty() {
198        eprintln!(
199            "  OK: {} modules, {} classes, {} structs, {} enums, {} functions",
200            module_count, class_count, struct_count, enum_count, func_count
201        );
202    } else {
203        eprintln!("  Verification FAILED:");
204        for e in &errors {
205            eprintln!("    - {e}");
206        }
207        std::process::exit(1);
208    }
209}
210
211/// Assign deterministic FuncIds to all exportable functions.
212fn build_func_table(ctx: &mut context::CodegenContext) {
213    let mut entries = Vec::new();
214
215    for (module_name, classes) in &ctx.module_classes {
216        for class in classes {
217            // Skip UInterface classes — their functions can't be called directly
218            if class.super_class.as_deref() == Some("Interface") {
219                continue;
220            }
221            for func in &class.funcs {
222                // Skip functions with unsupported param types
223                let all_supported = func.params.iter().all(|p| {
224                    type_map::map_property_type(
225                        &p.prop_type,
226                        p.class_name.as_deref(),
227                        p.struct_name.as_deref(),
228                        p.enum_name.as_deref(),
229                        p.enum_underlying_type.as_deref(),
230                        p.meta_class_name.as_deref(),
231                        p.interface_name.as_deref(),
232                    )
233                    .supported
234                });
235                if !all_supported {
236                    continue;
237                }
238                entries.push(context::FuncEntry {
239                    func_id: 0, // assigned below
240                    module_name: module_name.clone(),
241                    class_name: class.name.clone(),
242                    func_name: func.name.clone(),
243                    rust_func_name: naming::to_snake_case(&func.name),
244                    func: func.clone(),
245                    cpp_class_name: class.cpp_name.clone(),
246                    header: class.header.clone(),
247                });
248            }
249        }
250    }
251
252    // Sort by (module, class, func) for deterministic IDs
253    entries.sort_by(|a, b| {
254        a.module_name
255            .cmp(&b.module_name)
256            .then_with(|| a.class_name.cmp(&b.class_name))
257            .then_with(|| a.func_name.cmp(&b.func_name))
258    });
259
260    // Assign sequential IDs
261    for (i, entry) in entries.iter_mut().enumerate() {
262        entry.func_id = i as u32;
263    }
264
265    ctx.func_table = entries;
266}