Skip to main content

rusteal_codegen/cpp_gen/
wrapper.rs

1// C++ extern "C" wrapper function generation.
2
3use std::collections::BTreeSet;
4
5use crate::context::{CodegenContext, FuncEntry};
6use crate::schema::*;
7use crate::type_map::{self, ConversionKind, MappedType, ParamDirection};
8
9/// Strategy for generating return value handling in C++ wrappers.
10enum ReturnStrategy {
11    /// `*OutReturnValue = {call};`
12    Direct,
13    /// `*OutReturnValue = static_cast<void*>({call});`
14    ObjectRef,
15    /// `*OutReturnValue = static_cast<void*>({call}.Get());`
16    /// For TSoftObjectPtr / TWeakObjectPtr returns that resolve to UObject*.
17    SoftWeakObjectRef,
18    /// `*OutReturnValue = static_cast<{0}>({call});`
19    Cast(String),
20    /// FString result -> UTF-8 buffer conversion.
21    StringReturn,
22    /// FText result -> .ToString() then UTF-8 buffer conversion.
23    TextReturn,
24    /// Struct result -> memcpy to output buffer.
25    StructReturn,
26    /// FName result -> pack to u64.
27    FNameReturn,
28    /// Container return -> move-assign into temp container.
29    ContainerReturn,
30}
31
32/// Post-call action for complex output parameters.
33enum PostCallAction {
34    StringOutput(String),
35    /// FText output param: call .ToString() before UTF-8 conversion.
36    TextOutput(String),
37    StructOutput { name: String, struct_cpp: String },
38    ObjectOutput(String),
39    /// TSoftObjectPtr / TWeakObjectPtr output: call .Get() to extract raw pointer.
40    SoftWeakObjectOutput(String),
41    FNameOutput(String),
42    EnumOutput { name: String, ffi_type: String },
43    /// InOut struct copyback: copy local back to mutable buffer.
44    InOutStructCopyback { name: String, struct_cpp: String },
45    /// InOut FString copyback: convert __InOut{name} back to UTF-8.
46    InOutStringCopyback(String),
47    /// InOut FText copyback: convert __InOut{name}.ToString() back to UTF-8.
48    InOutTextCopyback(String),
49}
50
51/// Generate a complete .cpp file with all wrapper functions for a class.
52pub fn generate_wrapper_file(entries: &[&FuncEntry], ctx: &CodegenContext) -> String {
53    let mut out = String::with_capacity(entries.len() * 512 + 256);
54
55    out.push_str("// Auto-generated by rusteal-codegen. Do not edit.\n\n");
56
57    // Collect all required includes
58    let mut includes = BTreeSet::new();
59    includes.insert("\"RustealApiTable.h\"".to_string());
60
61    let has_container_params = entries.iter().any(|e| {
62        e.func.params.iter().any(is_container_param)
63    });
64
65    for entry in entries {
66        if !entry.header.is_empty() {
67            includes.insert(format!("\"{}\"", entry.header));
68        }
69        // Include headers for all classes referenced by params/return types.
70        // Required for TSubclassOf<T> which needs the full type definition.
71        for param in &entry.func.params {
72            collect_param_headers(param, ctx, &mut includes);
73        }
74    }
75
76    if has_container_params {
77        includes.insert("\"UObject/UnrealType.h\"".to_string());
78    }
79
80    // Check if any function uses FName params or returns
81    let has_fname = entries.iter().any(|e| {
82        e.func.params.iter().any(|p| {
83            matches!(map_param(p).rust_to_ffi, ConversionKind::FName)
84        })
85    });
86    if has_fname {
87        includes.insert("\"RustealFNameHelper.h\"".to_string());
88    }
89
90    for inc in &includes {
91        out.push_str(&format!("#include {inc}\n"));
92    }
93    out.push_str("#include <string>\n");
94    out.push('\n');
95
96    // Suppress C4996 (deprecated API) warnings in generated wrappers.
97    // We bind all UHT-exported functions unconditionally; users can choose
98    // not to call the deprecated ones from Rust.
99    out.push_str("#ifdef _MSC_VER\n");
100    out.push_str("#pragma warning(push)\n");
101    out.push_str("#pragma warning(disable: 4996)\n");
102    out.push_str("#endif\n\n");
103
104    // Forward declare the error code (guarded for Unity builds)
105    out.push_str("#ifndef RUSTEAL_ERROR_CODES_DEFINED\n");
106    out.push_str("#define RUSTEAL_ERROR_CODES_DEFINED\n");
107    out.push_str("static constexpr uint32_t RustealErrorCode_Ok = 0;\n");
108    out.push_str("static constexpr uint32_t RustealErrorCode_ObjectDestroyed = 1;\n");
109    out.push_str("#endif\n\n");
110
111    // Generate each wrapper
112    for entry in entries {
113        generate_wrapper_function(&mut out, entry, ctx);
114    }
115
116    out.push_str("#ifdef _MSC_VER\n");
117    out.push_str("#pragma warning(pop)\n");
118    out.push_str("#endif\n");
119
120    out
121}
122
123/// Build the C wrapper function name: Rusteal_ClassName_FuncName
124pub fn cpp_wrapper_name(class_name: &str, func_name: &str) -> String {
125    format!("Rusteal_{class_name}_{func_name}")
126}
127
128/// Generate a single extern "C" wrapper function.
129fn generate_wrapper_function(out: &mut String, entry: &FuncEntry, ctx: &CodegenContext) {
130    let func = &entry.func;
131    let class_cpp = &entry.cpp_class_name;
132    let func_name = &entry.func_name;
133
134    let is_static = func.is_static || (func.func_flags & FUNC_STATIC != 0);
135    let is_blueprint_native = (func.func_flags & FUNC_BLUEPRINT_EVENT != 0)
136        && (func.func_flags & FUNC_NATIVE != 0);
137
138    let c_func_name = cpp_wrapper_name(&entry.class_name, func_name);
139
140    // Classify params
141    let mut inputs = Vec::new();
142    let mut outputs = Vec::new();
143    let mut return_param: Option<&ParamInfo> = None;
144
145    for param in &func.params {
146        let dir = type_map::param_direction(param);
147        match dir {
148            ParamDirection::Return => return_param = Some(param),
149            ParamDirection::In | ParamDirection::InOut => inputs.push((param, dir)),
150            ParamDirection::Out => outputs.push(param),
151        }
152    }
153
154    // === Build function signature ===
155    out.push_str(&format!("extern \"C\" uint32_t {c_func_name}(\n"));
156
157    let mut params_str = Vec::new();
158    if !is_static {
159        params_str.push("    void* Obj".to_string());
160    }
161
162    // Generate signature entries in original parameter order
163    for param in &func.params {
164        let dir = type_map::param_direction(param);
165        match dir {
166            ParamDirection::In | ParamDirection::InOut => {
167                expand_input_sig(&mut params_str, param, dir);
168            }
169            ParamDirection::Out => {
170                expand_output_sig(&mut params_str, param);
171            }
172            ParamDirection::Return => {
173                expand_return_sig(&mut params_str, param);
174            }
175        }
176    }
177
178    out.push_str(&params_str.join(",\n"));
179    out.push_str("\n) {\n");
180
181    // === Function body ===
182
183    // Cast to the concrete class. Validity is already checked on the Rust side
184    // (ValidHandle::handle) before the FFI call. Debug builds keep a
185    // redundant check as defense-in-depth; shipping builds skip it.
186    if !is_static {
187        out.push_str(&format!(
188            "    {class_cpp}* Self = static_cast<{class_cpp}*>(Obj);\n\
189             #if UE_BUILD_DEBUG || UE_BUILD_DEVELOPMENT\n\
190             \x20   if (!IsValid(Self)) return RustealErrorCode_ObjectDestroyed;\n\
191             #endif\n"
192        ));
193    }
194
195    // Extract container references from temp buffers
196    for param in &func.params {
197        if !is_container_param(param) {
198            continue;
199        }
200        let dir = type_map::param_direction(param);
201        let name = &param.name;
202        let cpp_type = resolve_container_cpp_type(param, ctx)
203            .unwrap_or_else(|| "void /* ERROR */".to_string());
204
205        let (var_name, base_name, prop_name) = if dir == ParamDirection::Return {
206            (
207                "ReturnValue".to_string(),
208                "OutReturnValue_Base".to_string(),
209                "OutReturnValue_Prop".to_string(),
210            )
211        } else {
212            (
213                name.clone(),
214                format!("{name}_Base"),
215                format!("{name}_Prop"),
216            )
217        };
218
219        out.push_str(&format!(
220            "    auto& __Container_{var_name} = *reinterpret_cast<{cpp_type}*>(\n\
221             \x20       static_cast<FProperty*>({prop_name})->ContainerPtrToValuePtr<void>({base_name}));\n"
222        ));
223    }
224
225    // Declare locals for complex output params
226    let mut post_call_actions = Vec::new();
227    for param in &outputs {
228        if is_container_param(param) {
229            continue; // containers handled by extraction above
230        }
231        let mapped = map_param(param);
232        let name = &param.name;
233        match mapped.ffi_to_rust {
234            ConversionKind::StringUtf8 => {
235                if param.prop_type == "TextProperty" {
236                    out.push_str(&format!("    FText __Out{name};\n"));
237                    post_call_actions.push(PostCallAction::TextOutput(name.clone()));
238                } else {
239                    out.push_str(&format!("    FString __Out{name};\n"));
240                    post_call_actions.push(PostCallAction::StringOutput(name.clone()));
241                }
242            }
243            ConversionKind::StructOpaque => {
244                let struct_cpp = resolve_struct_opaque_cpp_type(param, ctx);
245                out.push_str(&format!("    {struct_cpp} __Out{name};\n"));
246                post_call_actions.push(PostCallAction::StructOutput {
247                    name: name.clone(),
248                    struct_cpp,
249                });
250            }
251            ConversionKind::ObjectRef => {
252                match param.prop_type.as_str() {
253                    // TSoftObjectPtr / TWeakObjectPtr are value types, not raw pointers.
254                    "SoftObjectProperty" => {
255                        let bare = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
256                        out.push_str(&format!("    TSoftObjectPtr<{bare}> __Out{name};\n"));
257                        post_call_actions.push(PostCallAction::SoftWeakObjectOutput(name.clone()));
258                    }
259                    "WeakObjectProperty" => {
260                        let bare = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
261                        out.push_str(&format!("    TWeakObjectPtr<{bare}> __Out{name};\n"));
262                        post_call_actions.push(PostCallAction::SoftWeakObjectOutput(name.clone()));
263                    }
264                    _ => {
265                        let obj_cpp = resolve_object_cpp_type(ctx, param.class_name.as_deref());
266                        out.push_str(&format!("    {obj_cpp} __Out{name} = nullptr;\n"));
267                        post_call_actions.push(PostCallAction::ObjectOutput(name.clone()));
268                    }
269                }
270            }
271            ConversionKind::FName => {
272                out.push_str(&format!("    FName __Out{name};\n"));
273                post_call_actions.push(PostCallAction::FNameOutput(name.clone()));
274            }
275            ConversionKind::EnumCast => {
276                let cpp_enum = resolve_enum_cpp_type(param);
277                let form = param.enum_cpp_form.unwrap_or(2);
278                let local_type = match form {
279                    0 | 1 => format!("TEnumAsByte<{cpp_enum}>"),
280                    _ => cpp_enum,
281                };
282                out.push_str(&format!("    {local_type} __Out{name};\n"));
283                post_call_actions.push(PostCallAction::EnumOutput {
284                    name: name.clone(),
285                    ffi_type: enum_ffi_ctype(param.enum_underlying_type.as_deref()),
286                });
287            }
288            _ => {} // Primitive outputs use direct pass-through
289        }
290    }
291
292    // Declare locals for InOut struct params (non-const reference)
293    for param in &func.params {
294        let dir = type_map::param_direction(param);
295        if dir == ParamDirection::InOut {
296            let mapped = map_param(param);
297            if mapped.rust_to_ffi == ConversionKind::StructOpaque {
298                let name = &param.name;
299                let struct_cpp = resolve_struct_opaque_cpp_type(param, ctx);
300                out.push_str(&format!("    {struct_cpp} __InOut{name};\n"));
301                out.push_str(&format!("    if ({name}) {{ FMemory::Memcpy(&__InOut{name}, {name}, sizeof({struct_cpp})); }}\n"));
302                post_call_actions.push(PostCallAction::InOutStructCopyback {
303                    name: name.clone(),
304                    struct_cpp,
305                });
306            }
307        }
308    }
309
310    // Declare locals for InOut string/text params (need lvalue for non-const reference)
311    for param in &func.params {
312        let dir = type_map::param_direction(param);
313        if dir == ParamDirection::InOut {
314            let mapped = map_param(param);
315            if mapped.rust_to_ffi == ConversionKind::StringUtf8 {
316                let name = &param.name;
317                if param.prop_type == "TextProperty" {
318                    out.push_str(&format!(
319                        "    FText __InOut{name} = FText::FromString(FString(UTF8_TO_TCHAR(std::string({name}, {name}Len).c_str())));\n"
320                    ));
321                    post_call_actions.push(PostCallAction::InOutTextCopyback(name.clone()));
322                } else {
323                    out.push_str(&format!(
324                        "    FString __InOut{name} = FString(UTF8_TO_TCHAR(std::string({name}, {name}Len).c_str()));\n"
325                    ));
326                    post_call_actions.push(PostCallAction::InOutStringCopyback(name.clone()));
327                }
328            }
329        }
330    }
331
332    // Build call arguments in original parameter order (must match UE method signature)
333    let mut call_args = Vec::new();
334    for param in &func.params {
335        let dir = type_map::param_direction(param);
336        match dir {
337            ParamDirection::Return => {} // handled by return strategy, not a call arg
338            ParamDirection::In | ParamDirection::InOut => {
339                call_args.push(input_arg_expression(param, dir, ctx));
340            }
341            ParamDirection::Out => {
342                if is_container_param(param) {
343                    call_args.push(format!("__Container_{}", param.name));
344                } else {
345                    let mapped = map_param(param);
346                    match mapped.ffi_to_rust {
347                        ConversionKind::StringUtf8
348                        | ConversionKind::StructOpaque
349                        | ConversionKind::ObjectRef
350                        | ConversionKind::FName
351                        | ConversionKind::EnumCast => {
352                            call_args.push(format!("__Out{}", param.name));
353                        }
354                        _ => {
355                            call_args.push(format!("*Out{}", param.name));
356                        }
357                    }
358                }
359            }
360        }
361    }
362
363    let call_expr = if is_static {
364        format!("{}::{}({})", class_cpp, func_name, call_args.join(", "))
365    } else if is_blueprint_native {
366        format!(
367            "Self->{}_Implementation({})",
368            func_name,
369            call_args.join(", ")
370        )
371    } else {
372        format!("Self->{}({})", func_name, call_args.join(", "))
373    };
374
375    // Emit call + return handling
376    if let Some(rp) = return_param {
377        let strategy = return_strategy(rp);
378        emit_return(out, &strategy, &call_expr);
379    } else {
380        out.push_str(&format!("    {call_expr};\n"));
381    }
382
383    // Post-call conversions for complex output params
384    for action in &post_call_actions {
385        emit_post_call(out, action);
386    }
387
388    out.push_str("    return RustealErrorCode_Ok;\n");
389    out.push_str("}\n\n");
390}
391
392// ---------------------------------------------------------------------------
393// Signature expansion helpers
394// ---------------------------------------------------------------------------
395
396fn expand_input_sig(params: &mut Vec<String>, param: &ParamInfo, dir: ParamDirection) {
397    if is_container_param(param) {
398        let name = &param.name;
399        params.push(format!("    void* {name}_Base"));
400        params.push(format!("    void* {name}_Prop"));
401        return;
402    }
403    let mapped = map_param(param);
404    let name = &param.name;
405    match mapped.rust_to_ffi {
406        ConversionKind::StringUtf8 => {
407            params.push(format!("    const char* {name}"));
408            params.push(format!("    uint32_t {name}Len"));
409            // InOut strings also need an output buffer to write the modified value back
410            if dir == ParamDirection::InOut {
411                params.push(format!("    char* Out{name}"));
412                params.push(format!("    uint32_t Out{name}BufLen"));
413                params.push(format!("    uint32_t* Out{name}Len"));
414            }
415        }
416        _ => {
417            let cpp_type = scalar_input_cpp_type(&mapped, param, dir);
418            params.push(format!("    {cpp_type} {name}"));
419        }
420    }
421}
422
423fn expand_output_sig(params: &mut Vec<String>, param: &ParamInfo) {
424    if is_container_param(param) {
425        let name = &param.name;
426        params.push(format!("    void* {name}_Base"));
427        params.push(format!("    void* {name}_Prop"));
428        return;
429    }
430    let mapped = map_param(param);
431    let name = &param.name;
432    match mapped.ffi_to_rust {
433        ConversionKind::StringUtf8 => {
434            params.push(format!("    char* Out{name}"));
435            params.push(format!("    uint32_t Out{name}BufLen"));
436            params.push(format!("    uint32_t* Out{name}Len"));
437        }
438        _ => {
439            let cpp_type = scalar_output_cpp_type(&mapped, param);
440            params.push(format!("    {cpp_type} Out{name}"));
441        }
442    }
443}
444
445fn expand_return_sig(params: &mut Vec<String>, param: &ParamInfo) {
446    if is_container_param(param) {
447        params.push("    void* OutReturnValue_Base".to_string());
448        params.push("    void* OutReturnValue_Prop".to_string());
449        return;
450    }
451    let mapped = map_param(param);
452    match mapped.ffi_to_rust {
453        ConversionKind::StringUtf8 => {
454            params.push("    char* OutBuf".to_string());
455            params.push("    uint32_t BufLen".to_string());
456            params.push("    uint32_t* OutLen".to_string());
457        }
458        _ => {
459            let cpp_type = scalar_output_cpp_type(&mapped, param);
460            params.push(format!("    {cpp_type} OutReturnValue"));
461        }
462    }
463}
464
465// ---------------------------------------------------------------------------
466// Type helpers
467// ---------------------------------------------------------------------------
468
469/// Map a ParamInfo to its MappedType.
470fn map_param(param: &ParamInfo) -> MappedType {
471    type_map::map_property_type(
472        &param.prop_type,
473        param.class_name.as_deref(),
474        param.struct_name.as_deref(),
475        param.enum_name.as_deref(),
476        param.enum_underlying_type.as_deref(),
477        param.meta_class_name.as_deref(),
478        param.interface_name.as_deref(),
479    )
480}
481
482/// Get the C++ type for a single scalar input parameter.
483fn scalar_input_cpp_type(mapped: &MappedType, param: &ParamInfo, dir: ParamDirection) -> String {
484    match mapped.rust_to_ffi {
485        ConversionKind::Identity => mapped.cpp_type.clone(),
486        ConversionKind::ObjectRef => "void*".to_string(),
487        ConversionKind::StringUtf8 => unreachable!("strings handled by expand_input_sig"),
488        ConversionKind::EnumCast => enum_ffi_ctype(param.enum_underlying_type.as_deref()),
489        ConversionKind::IntCast => int_ctype(&mapped.cpp_type),
490        ConversionKind::StructOpaque => {
491            if dir == ParamDirection::InOut {
492                "uint8_t*".to_string() // mutable: data flows both ways
493            } else {
494                "const uint8_t*".to_string()
495            }
496        }
497        ConversionKind::FName => "uint64_t".to_string(),
498        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
499        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
500            unreachable!("container/delegate types are property-only, never function params"),
501    }
502}
503
504/// Get the C++ output pointer type for a single scalar output/return parameter.
505fn scalar_output_cpp_type(mapped: &MappedType, param: &ParamInfo) -> String {
506    match mapped.ffi_to_rust {
507        ConversionKind::Identity => format!("{}*", mapped.cpp_type),
508        ConversionKind::ObjectRef => "void**".to_string(),
509        ConversionKind::StringUtf8 => unreachable!("strings handled by expand_*_sig"),
510        ConversionKind::EnumCast => {
511            format!("{}*", enum_ffi_ctype(param.enum_underlying_type.as_deref()))
512        }
513        ConversionKind::IntCast => format!("{}*", int_ctype(&mapped.cpp_type)),
514        ConversionKind::StructOpaque => "uint8_t*".to_string(),
515        ConversionKind::FName => "uint64_t*".to_string(),
516        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
517        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
518            unreachable!("container/delegate types are property-only, never function params"),
519    }
520}
521
522fn enum_ffi_ctype(underlying: Option<&str>) -> String {
523    match underlying.unwrap_or("uint8") {
524        "uint8" => "uint8_t",
525        "int8" => "int8_t",
526        "uint16" => "uint16_t",
527        "int16" => "int16_t",
528        "uint32" => "uint32_t",
529        "int32" => "int32_t",
530        "uint64" => "uint64_t",
531        "int64" => "int64_t",
532        _ => "uint8_t",
533    }
534    .to_string()
535}
536
537fn int_ctype(cpp_type: &str) -> String {
538    match cpp_type {
539        "uint8" => "uint8_t",
540        "int8" => "int8_t",
541        "uint16" => "uint16_t",
542        "int16" => "int16_t",
543        "uint32" => "uint32_t",
544        "int32" => "int32_t",
545        "uint64" => "uint64_t",
546        "int64" => "int64_t",
547        _ => "int32_t",
548    }
549    .to_string()
550}
551
552/// Resolve the C++ type name for a StructOpaque parameter.
553/// Handles regular structs (F-prefixed), soft object refs, and weak object refs.
554fn resolve_struct_opaque_cpp_type(param: &ParamInfo, ctx: &CodegenContext) -> String {
555    match param.prop_type.as_str() {
556        "SoftObjectProperty" => {
557            let cls_cpp = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
558            format!("TSoftObjectPtr<{cls_cpp}>")
559        }
560        "WeakObjectProperty" => {
561            let cls_cpp = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
562            format!("TWeakObjectPtr<{cls_cpp}>")
563        }
564        _ => {
565            format!("F{}", param.struct_name.as_deref().unwrap_or("Unknown"))
566        }
567    }
568}
569
570/// Get the C++ class name without a trailing * (e.g., "UObject", "AActor").
571fn resolve_object_cpp_type_bare(ctx: &CodegenContext, class_name: Option<&str>) -> String {
572    match class_name {
573        Some(cn) => {
574            if let Some(cls) = ctx.classes.get(cn) {
575                cls.cpp_name.clone()
576            } else {
577                cn.to_string()
578            }
579        }
580        None => "UObject".to_string(),
581    }
582}
583
584/// Resolve an object parameter's full C++ class name (e.g., "AActor*") from the context.
585fn resolve_object_cpp_type(ctx: &CodegenContext, class_name: Option<&str>) -> String {
586    match class_name {
587        Some(cn) => {
588            if let Some(cls) = ctx.classes.get(cn) {
589                format!("{}*", cls.cpp_name)
590            } else {
591                // Class not in context — use stripped name as-is
592                format!("{cn}*")
593            }
594        }
595        None => "UObject*".to_string(),
596    }
597}
598
599// ---------------------------------------------------------------------------
600// Input argument expressions
601// ---------------------------------------------------------------------------
602
603/// Build the C++ expression for passing an input argument.
604fn input_arg_expression(param: &ParamInfo, dir: ParamDirection, ctx: &CodegenContext) -> String {
605    if is_container_param(param) {
606        return format!("__Container_{}", param.name);
607    }
608    let mapped = map_param(param);
609
610    match mapped.rust_to_ffi {
611        ConversionKind::Identity => param.name.clone(),
612        ConversionKind::ObjectRef => {
613            let cpp_type = resolve_object_cpp_type(ctx, param.class_name.as_deref());
614            match param.prop_type.as_str() {
615                // TSoftObjectPtr<T> / TWeakObjectPtr<T> are value types constructed from UObject*.
616                "SoftObjectProperty" => {
617                    let bare_type = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
618                    format!("TSoftObjectPtr<{bare_type}>(static_cast<{cpp_type}>({}))", param.name)
619                }
620                "WeakObjectProperty" => {
621                    let bare_type = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
622                    format!("TWeakObjectPtr<{bare_type}>(static_cast<{cpp_type}>({}))", param.name)
623                }
624                _ => format!("static_cast<{cpp_type}>({})", param.name),
625            }
626        }
627        ConversionKind::StringUtf8 => {
628            if dir == ParamDirection::InOut {
629                // InOut: use the pre-declared mutable local
630                format!("__InOut{}", param.name)
631            } else {
632                let fstring_expr = format!(
633                    "FString(UTF8_TO_TCHAR(std::string({name}, {name}Len).c_str()))",
634                    name = param.name
635                );
636                if param.prop_type == "TextProperty" {
637                    format!("FText::FromString({fstring_expr})")
638                } else {
639                    fstring_expr
640                }
641            }
642        }
643        ConversionKind::EnumCast => {
644            let cpp_enum = resolve_enum_cpp_type(param);
645            format!("static_cast<{cpp_enum}>({})", param.name)
646        }
647        ConversionKind::StructOpaque => {
648            if dir == ParamDirection::InOut {
649                // InOut struct: use the pre-declared mutable local
650                format!("__InOut{}", param.name)
651            } else {
652                let struct_cpp = resolve_struct_opaque_cpp_type(param, ctx);
653                format!(
654                    "*reinterpret_cast<const {struct_cpp}*>({})",
655                    param.name
656                )
657            }
658        }
659        ConversionKind::IntCast => param.name.clone(), // C++ native type, no conversion needed
660        ConversionKind::FName => {
661            format!("RustealUnpackFName({})", param.name)
662        }
663        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
664        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
665            unreachable!("container/delegate types are property-only, never function params"),
666    }
667}
668
669// ---------------------------------------------------------------------------
670// Return handling
671// ---------------------------------------------------------------------------
672
673fn return_strategy(param: &ParamInfo) -> ReturnStrategy {
674    if is_container_param(param) {
675        return ReturnStrategy::ContainerReturn;
676    }
677    let mapped = map_param(param);
678
679    match mapped.ffi_to_rust {
680        ConversionKind::Identity | ConversionKind::IntCast => ReturnStrategy::Direct,
681        ConversionKind::ObjectRef => {
682            // TSoftObjectPtr / TWeakObjectPtr return value types, not raw pointers.
683            // Need .Get() to extract the UObject* before casting to void*.
684            match param.prop_type.as_str() {
685                "SoftObjectProperty" | "WeakObjectProperty" => ReturnStrategy::SoftWeakObjectRef,
686                _ => ReturnStrategy::ObjectRef,
687            }
688        }
689        ConversionKind::StringUtf8 => {
690            if param.prop_type == "TextProperty" {
691                ReturnStrategy::TextReturn
692            } else {
693                ReturnStrategy::StringReturn
694            }
695        }
696        ConversionKind::EnumCast => {
697            let ctype = match mapped.rust_ffi_type.as_str() {
698                "u8" => "uint8_t",
699                "i8" => "int8_t",
700                "i32" => "int32_t",
701                _ => "uint8_t",
702            };
703            ReturnStrategy::Cast(ctype.to_string())
704        }
705        ConversionKind::StructOpaque => ReturnStrategy::StructReturn,
706        ConversionKind::FName => ReturnStrategy::FNameReturn,
707        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
708        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
709            unreachable!("container/delegate types are property-only, never function return types"),
710    }
711}
712
713fn emit_return(out: &mut String, strategy: &ReturnStrategy, call_expr: &str) {
714    match strategy {
715        ReturnStrategy::Direct => {
716            out.push_str(&format!("    *OutReturnValue = {call_expr};\n"));
717        }
718        ReturnStrategy::ObjectRef => {
719            // const_cast needed for functions returning const T*
720            out.push_str(&format!(
721                "    *OutReturnValue = const_cast<void*>(static_cast<const void*>({call_expr}));\n"
722            ));
723        }
724        ReturnStrategy::SoftWeakObjectRef => {
725            // TSoftObjectPtr<T> / TWeakObjectPtr<T> are value types; call .Get()
726            // to resolve to the underlying UObject* before casting.
727            out.push_str(&format!(
728                "    *OutReturnValue = const_cast<void*>(static_cast<const void*>({call_expr}.Get()));\n"
729            ));
730        }
731        ReturnStrategy::Cast(ctype) => {
732            out.push_str(&format!(
733                "    *OutReturnValue = static_cast<{ctype}>({call_expr});\n"
734            ));
735        }
736        ReturnStrategy::StringReturn => {
737            out.push_str(&format!("    FString __RustealResult = {call_expr};\n"));
738            out.push_str(
739                "    auto __RustealUtf8 = StringCast<ANSICHAR>(*__RustealResult);\n\
740                 \x20   int32 __RustealLen = __RustealUtf8.Length();\n\
741                 \x20   if (__RustealLen > static_cast<int32>(BufLen)) __RustealLen = static_cast<int32>(BufLen);\n\
742                 \x20   FMemory::Memcpy(OutBuf, __RustealUtf8.Get(), __RustealLen);\n\
743                 \x20   *OutLen = static_cast<uint32_t>(__RustealLen);\n",
744            );
745        }
746        ReturnStrategy::TextReturn => {
747            out.push_str(&format!("    FString __RustealResult = ({call_expr}).ToString();\n"));
748            out.push_str(
749                "    auto __RustealUtf8 = StringCast<ANSICHAR>(*__RustealResult);\n\
750                 \x20   int32 __RustealLen = __RustealUtf8.Length();\n\
751                 \x20   if (__RustealLen > static_cast<int32>(BufLen)) __RustealLen = static_cast<int32>(BufLen);\n\
752                 \x20   FMemory::Memcpy(OutBuf, __RustealUtf8.Get(), __RustealLen);\n\
753                 \x20   *OutLen = static_cast<uint32_t>(__RustealLen);\n",
754            );
755        }
756        ReturnStrategy::StructReturn => {
757            out.push_str(&format!("    auto __RustealResult = {call_expr};\n"));
758            out.push_str(
759                "    FMemory::Memcpy(OutReturnValue, &__RustealResult, sizeof(__RustealResult));\n",
760            );
761        }
762        ReturnStrategy::FNameReturn => {
763            out.push_str(&format!("    FName __RustealResult = {call_expr};\n"));
764            out.push_str(
765                "    *OutReturnValue = RustealPackFName(__RustealResult);\n",
766            );
767        }
768        ReturnStrategy::ContainerReturn => {
769            out.push_str(&format!("    __Container_ReturnValue = {call_expr};\n"));
770        }
771    }
772}
773
774/// Wrap an enum type with TEnumAsByte<> for non-enum-class enums.
775/// cpp_form 0 (Regular): `TEnumAsByte<EFoo>`
776/// cpp_form 1 (Namespaced): `TEnumAsByte<EFoo::Type>`
777/// cpp_form 2 (EnumClass): `EFoo` (no wrapping needed)
778fn wrap_enum_as_byte(name: &str, cpp_form: Option<u32>) -> String {
779    let form = cpp_form.unwrap_or(2);
780    match form {
781        0 => format!("TEnumAsByte<{name}>"),
782        1 => format!("TEnumAsByte<{name}::Type>"),
783        _ => name.to_string(),
784    }
785}
786
787/// Resolve the correct C++ enum type name based on cpp_form.
788/// cpp_form 0 (Regular) and 1 (Namespaced): `EFoo::Type`
789/// cpp_form 2 (EnumClass): `EFoo`
790fn resolve_enum_cpp_type(param: &ParamInfo) -> String {
791    let name = param
792        .enum_cpp_name
793        .as_deref()
794        .or(param.enum_name.as_deref())
795        .unwrap_or("uint8");
796    let form = param.enum_cpp_form.unwrap_or(2);
797    if form == 1 {
798        // Namespaced enum: namespace EFoo { enum Type { ... }; }
799        format!("{name}::Type")
800    } else {
801        // Regular (0) or EnumClass (2): use name directly
802        name.to_string()
803    }
804}
805
806fn emit_post_call(out: &mut String, action: &PostCallAction) {
807    match action {
808        PostCallAction::StringOutput(name) => {
809            out.push_str(&format!(
810                "    {{\n\
811                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Out{name});\n\
812                 \x20       int32 __Len = __Utf8.Length();\n\
813                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
814                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
815                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
816                 \x20   }}\n"
817            ));
818        }
819        PostCallAction::TextOutput(name) => {
820            // FText output: convert via .ToString() then UTF-8
821            out.push_str(&format!(
822                "    {{\n\
823                 \x20       FString __Str = __Out{name}.ToString();\n\
824                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Str);\n\
825                 \x20       int32 __Len = __Utf8.Length();\n\
826                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
827                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
828                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
829                 \x20   }}\n"
830            ));
831        }
832        PostCallAction::StructOutput { name, struct_cpp } => {
833            out.push_str(&format!(
834                "    if (Out{name}) {{ FMemory::Memcpy(Out{name}, &__Out{name}, sizeof({struct_cpp})); }}\n"
835            ));
836        }
837        PostCallAction::ObjectOutput(name) => {
838            out.push_str(&format!(
839                "    *Out{name} = static_cast<void*>(__Out{name});\n"
840            ));
841        }
842        PostCallAction::SoftWeakObjectOutput(name) => {
843            out.push_str(&format!(
844                "    *Out{name} = static_cast<void*>(__Out{name}.Get());\n"
845            ));
846        }
847        PostCallAction::FNameOutput(name) => {
848            out.push_str(&format!(
849                "    *Out{name} = RustealPackFName(__Out{name});\n"
850            ));
851        }
852        PostCallAction::EnumOutput { name, ffi_type } => {
853            out.push_str(&format!(
854                "    *Out{name} = static_cast<{ffi_type}>(__Out{name});\n"
855            ));
856        }
857        PostCallAction::InOutStructCopyback { name, struct_cpp } => {
858            out.push_str(&format!(
859                "    if ({name}) {{ FMemory::Memcpy({name}, &__InOut{name}, sizeof({struct_cpp})); }}\n"
860            ));
861        }
862        PostCallAction::InOutStringCopyback(name) => {
863            out.push_str(&format!(
864                "    {{\n\
865                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__InOut{name});\n\
866                 \x20       int32 __Len = __Utf8.Length();\n\
867                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
868                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
869                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
870                 \x20   }}\n"
871            ));
872        }
873        PostCallAction::InOutTextCopyback(name) => {
874            out.push_str(&format!(
875                "    {{\n\
876                 \x20       FString __Str = __InOut{name}.ToString();\n\
877                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Str);\n\
878                 \x20       int32 __Len = __Utf8.Length();\n\
879                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
880                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
881                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
882                 \x20   }}\n"
883            ));
884        }
885    }
886}
887
888// ---------------------------------------------------------------------------
889// Container param helpers
890// ---------------------------------------------------------------------------
891
892fn is_container_param(param: &ParamInfo) -> bool {
893    matches!(
894        param.prop_type.as_str(),
895        "ArrayProperty" | "MapProperty" | "SetProperty"
896    )
897}
898
899/// Resolve the C++ element type for a container's inner property.
900fn resolve_inner_cpp_type(inner: &PropertyInfo, ctx: &CodegenContext) -> String {
901    match inner.prop_type.as_str() {
902        "BoolProperty" => "bool".to_string(),
903        "Int8Property" => "int8".to_string(),
904        "ByteProperty" => {
905            if let Some(ref en) = inner.enum_name {
906                let name = inner
907                    .enum_cpp_name
908                    .as_deref()
909                    .unwrap_or(en.as_str());
910                wrap_enum_as_byte(name, inner.enum_cpp_form)
911            } else {
912                "uint8".to_string()
913            }
914        }
915        "Int16Property" => "int16".to_string(),
916        "UInt16Property" => "uint16".to_string(),
917        "IntProperty" => "int32".to_string(),
918        "UInt32Property" => "uint32".to_string(),
919        "Int64Property" => "int64".to_string(),
920        "UInt64Property" => "uint64".to_string(),
921        "FloatProperty" => "float".to_string(),
922        "DoubleProperty" => "double".to_string(),
923        "StrProperty" => "FString".to_string(),
924        "TextProperty" => "FText".to_string(),
925        "NameProperty" => "FName".to_string(),
926        "ObjectProperty" => {
927            resolve_object_cpp_type(ctx, inner.class_name.as_deref())
928        }
929        "SoftObjectProperty" => {
930            let cls_cpp = resolve_object_cpp_type_bare(ctx, inner.class_name.as_deref());
931            format!("TSoftObjectPtr<{cls_cpp}>")
932        }
933        "WeakObjectProperty" => {
934            let cls_cpp = resolve_object_cpp_type_bare(ctx, inner.class_name.as_deref());
935            format!("TWeakObjectPtr<{cls_cpp}>")
936        }
937        "ClassProperty" => {
938            // Inside a container the element type must match the engine's exactly: a
939            // `TArray<TSubclassOf<X>>` is not a `TArray<X*>` (a direct parameter gets away
940            // with `UClass*` because TSubclassOf converts implicitly; an element does not).
941            match inner.meta_class_name.as_deref() {
942                Some(meta) => {
943                    let meta_cpp = resolve_object_cpp_type_bare(ctx, Some(meta));
944                    format!("TSubclassOf<{meta_cpp}>")
945                }
946                None => "UClass*".to_string(),
947            }
948        }
949        "InterfaceProperty" => {
950            resolve_object_cpp_type(ctx, inner.interface_name.as_deref())
951        }
952        "EnumProperty" => {
953            let name = inner
954                .enum_cpp_name
955                .as_deref()
956                .or(inner.enum_name.as_deref())
957                .unwrap_or("uint8");
958            wrap_enum_as_byte(name, inner.enum_cpp_form)
959        }
960        "StructProperty" => {
961            if let Some(ref sn) = inner.struct_name {
962                if let Some(si) = ctx.structs.get(sn.as_str()) {
963                    si.cpp_name.clone()
964                } else {
965                    format!("F{sn}")
966                }
967            } else {
968                "uint8".to_string()
969            }
970        }
971        _ => "uint8".to_string(),
972    }
973}
974
975/// Resolve the full C++ container type (e.g., "TArray<AActor*>").
976fn resolve_container_cpp_type(param: &ParamInfo, ctx: &CodegenContext) -> Option<String> {
977    match param.prop_type.as_str() {
978        "ArrayProperty" => {
979            let inner = param.inner_prop.as_ref()?;
980            let elem = resolve_inner_cpp_type(inner, ctx);
981            Some(format!("TArray<{elem}>"))
982        }
983        "MapProperty" => {
984            let key = param.key_prop.as_ref()?;
985            let val = param.value_prop.as_ref()?;
986            let kt = resolve_inner_cpp_type(key, ctx);
987            let vt = resolve_inner_cpp_type(val, ctx);
988            Some(format!("TMap<{kt}, {vt}>"))
989        }
990        "SetProperty" => {
991            let elem = param.element_prop.as_ref()?;
992            let et = resolve_inner_cpp_type(elem, ctx);
993            Some(format!("TSet<{et}>"))
994        }
995        _ => None,
996    }
997}
998
999/// Collect headers needed by a parameter's referenced types (class, struct, etc.).
1000fn collect_param_headers(
1001    param: &ParamInfo,
1002    ctx: &CodegenContext,
1003    includes: &mut BTreeSet<String>,
1004) {
1005    // Direct class reference (ObjectProperty, ClassProperty)
1006    for cls_name in [
1007        param.class_name.as_deref(),
1008        param.meta_class_name.as_deref(),
1009    ]
1010    .into_iter()
1011    .flatten()
1012    {
1013        if let Some(cls) = ctx.classes.get(cls_name)
1014            && !cls.header.is_empty()
1015        {
1016            includes.insert(format!("\"{}\"", cls.header));
1017        }
1018    }
1019
1020    // Container inner types
1021    for inner in [
1022        param.inner_prop.as_deref(),
1023        param.key_prop.as_deref(),
1024        param.value_prop.as_deref(),
1025        param.element_prop.as_deref(),
1026    ]
1027    .into_iter()
1028    .flatten()
1029    {
1030        for cls_name in [
1031            inner.class_name.as_deref(),
1032            inner.meta_class_name.as_deref(),
1033        ]
1034        .into_iter()
1035        .flatten()
1036        {
1037            if let Some(cls) = ctx.classes.get(cls_name)
1038                && !cls.header.is_empty()
1039            {
1040                includes.insert(format!("\"{}\"", cls.header));
1041            }
1042        }
1043    }
1044}