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