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 is_protected = func.func_flags & FUNC_PROTECTED != 0;
370    let call_expr = if is_protected && func.interface.is_none() && !is_blueprint_native {
371        // A protected function a Blueprint child may call (FinishTask): a
372        // local type re-exports it with a using-declaration, as below.
373        out.push_str(&format!(
374            "    struct FAccess : {class_cpp} {{ using {class_cpp}::{func_name}; }};\n"
375        ));
376        if is_static {
377            format!("FAccess::{func_name}({args})")
378        } else {
379            format!("(Self->*&FAccess::{func_name})({args})")
380        }
381    } else if is_static {
382        format!("{class_cpp}::{func_name}({args})")
383    } else if let Some(iface) = &func.interface {
384        // Declared on an interface the class implements: call it through the
385        // interface, where the UFUNCTION is public (an override in the class may not be).
386        format!("static_cast<{iface}*>(Self)->{func_name}({args})")
387    } else if is_blueprint_native {
388        // The native implementation of a BlueprintNativeEvent, as a C++ `Super::`
389        // call would reach it. Classes often declare `_Implementation` protected
390        // (UInputModifier::ModifyRaw_Implementation), so a local type re-exports it
391        // with a using-declaration and the call goes through a member pointer,
392        // which keeps virtual dispatch.
393        out.push_str(&format!(
394            "    struct FAccess : {class_cpp} {{ using {class_cpp}::{func_name}_Implementation; }};\n"
395        ));
396        format!("(Self->*&FAccess::{func_name}_Implementation)({args})")
397    } else {
398        format!("Self->{func_name}({args})")
399    };
400
401    // Emit call + return handling
402    if let Some(rp) = return_param {
403        let strategy = return_strategy(rp);
404        emit_return(out, &strategy, &call_expr);
405    } else {
406        out.push_str(&format!("    {call_expr};\n"));
407    }
408
409    // Post-call conversions for complex output params
410    for action in &post_call_actions {
411        emit_post_call(out, action);
412    }
413
414    out.push_str("    return RustealErrorCode_Ok;\n");
415    out.push_str("}\n\n");
416}
417
418// ---------------------------------------------------------------------------
419// Signature expansion helpers
420// ---------------------------------------------------------------------------
421
422fn expand_input_sig(params: &mut Vec<String>, param: &ParamInfo, dir: ParamDirection) {
423    if is_container_param(param) {
424        let name = &param.name;
425        params.push(format!("    void* {name}_Base"));
426        params.push(format!("    void* {name}_Prop"));
427        return;
428    }
429    let mapped = map_param(param);
430    let name = &param.name;
431    match mapped.rust_to_ffi {
432        ConversionKind::StringUtf8 => {
433            params.push(format!("    const char* {name}"));
434            params.push(format!("    uint32_t {name}Len"));
435            // InOut strings also need an output buffer to write the modified value back
436            if dir == ParamDirection::InOut {
437                params.push(format!("    char* Out{name}"));
438                params.push(format!("    uint32_t Out{name}BufLen"));
439                params.push(format!("    uint32_t* Out{name}Len"));
440            }
441        }
442        _ => {
443            let cpp_type = scalar_input_cpp_type(&mapped, param, dir);
444            params.push(format!("    {cpp_type} {name}"));
445        }
446    }
447}
448
449fn expand_output_sig(params: &mut Vec<String>, param: &ParamInfo) {
450    if is_container_param(param) {
451        let name = &param.name;
452        params.push(format!("    void* {name}_Base"));
453        params.push(format!("    void* {name}_Prop"));
454        return;
455    }
456    let mapped = map_param(param);
457    let name = &param.name;
458    match mapped.ffi_to_rust {
459        ConversionKind::StringUtf8 => {
460            params.push(format!("    char* Out{name}"));
461            params.push(format!("    uint32_t Out{name}BufLen"));
462            params.push(format!("    uint32_t* Out{name}Len"));
463        }
464        _ => {
465            let cpp_type = scalar_output_cpp_type(&mapped, param);
466            params.push(format!("    {cpp_type} Out{name}"));
467        }
468    }
469}
470
471fn expand_return_sig(params: &mut Vec<String>, param: &ParamInfo) {
472    if is_container_param(param) {
473        params.push("    void* OutReturnValue_Base".to_string());
474        params.push("    void* OutReturnValue_Prop".to_string());
475        return;
476    }
477    let mapped = map_param(param);
478    match mapped.ffi_to_rust {
479        ConversionKind::StringUtf8 => {
480            params.push("    char* OutBuf".to_string());
481            params.push("    uint32_t BufLen".to_string());
482            params.push("    uint32_t* OutLen".to_string());
483        }
484        _ => {
485            let cpp_type = scalar_output_cpp_type(&mapped, param);
486            params.push(format!("    {cpp_type} OutReturnValue"));
487        }
488    }
489}
490
491// ---------------------------------------------------------------------------
492// Type helpers
493// ---------------------------------------------------------------------------
494
495/// Map a ParamInfo to its MappedType.
496fn map_param(param: &ParamInfo) -> MappedType {
497    type_map::map_property_type(
498        &param.prop_type,
499        param.class_name.as_deref(),
500        param.struct_name.as_deref(),
501        param.enum_name.as_deref(),
502        param.enum_underlying_type.as_deref(),
503        param.meta_class_name.as_deref(),
504        param.interface_name.as_deref(),
505    )
506}
507
508/// Get the C++ type for a single scalar input parameter.
509fn scalar_input_cpp_type(mapped: &MappedType, param: &ParamInfo, dir: ParamDirection) -> String {
510    match mapped.rust_to_ffi {
511        ConversionKind::Identity => mapped.cpp_type.clone(),
512        ConversionKind::ObjectRef => "void*".to_string(),
513        ConversionKind::StringUtf8 => unreachable!("strings handled by expand_input_sig"),
514        ConversionKind::EnumCast => enum_ffi_ctype(param.enum_underlying_type.as_deref()),
515        ConversionKind::IntCast => int_ctype(&mapped.cpp_type),
516        ConversionKind::StructOpaque => {
517            if dir == ParamDirection::InOut {
518                "uint8_t*".to_string() // mutable: data flows both ways
519            } else {
520                "const uint8_t*".to_string()
521            }
522        }
523        ConversionKind::FName => "uint64_t".to_string(),
524        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
525        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
526            unreachable!("container/delegate types are property-only, never function params"),
527    }
528}
529
530/// Get the C++ output pointer type for a single scalar output/return parameter.
531fn scalar_output_cpp_type(mapped: &MappedType, param: &ParamInfo) -> String {
532    match mapped.ffi_to_rust {
533        ConversionKind::Identity => format!("{}*", mapped.cpp_type),
534        ConversionKind::ObjectRef => "void**".to_string(),
535        ConversionKind::StringUtf8 => unreachable!("strings handled by expand_*_sig"),
536        ConversionKind::EnumCast => {
537            format!("{}*", enum_ffi_ctype(param.enum_underlying_type.as_deref()))
538        }
539        ConversionKind::IntCast => format!("{}*", int_ctype(&mapped.cpp_type)),
540        ConversionKind::StructOpaque => "uint8_t*".to_string(),
541        ConversionKind::FName => "uint64_t*".to_string(),
542        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
543        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
544            unreachable!("container/delegate types are property-only, never function params"),
545    }
546}
547
548fn enum_ffi_ctype(underlying: Option<&str>) -> String {
549    match underlying.unwrap_or("uint8") {
550        "uint8" => "uint8_t",
551        "int8" => "int8_t",
552        "uint16" => "uint16_t",
553        "int16" => "int16_t",
554        "uint32" => "uint32_t",
555        "int32" => "int32_t",
556        "uint64" => "uint64_t",
557        "int64" => "int64_t",
558        _ => "uint8_t",
559    }
560    .to_string()
561}
562
563fn int_ctype(cpp_type: &str) -> String {
564    match cpp_type {
565        "uint8" => "uint8_t",
566        "int8" => "int8_t",
567        "uint16" => "uint16_t",
568        "int16" => "int16_t",
569        "uint32" => "uint32_t",
570        "int32" => "int32_t",
571        "uint64" => "uint64_t",
572        "int64" => "int64_t",
573        _ => "int32_t",
574    }
575    .to_string()
576}
577
578/// Resolve the C++ type name for a StructOpaque parameter.
579/// Handles regular structs (F-prefixed), soft object refs, and weak object refs.
580fn resolve_struct_opaque_cpp_type(param: &ParamInfo, ctx: &CodegenContext) -> String {
581    match param.prop_type.as_str() {
582        "SoftObjectProperty" => {
583            let cls_cpp = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
584            format!("TSoftObjectPtr<{cls_cpp}>")
585        }
586        "WeakObjectProperty" => {
587            let cls_cpp = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
588            format!("TWeakObjectPtr<{cls_cpp}>")
589        }
590        _ => {
591            format!("F{}", param.struct_name.as_deref().unwrap_or("Unknown"))
592        }
593    }
594}
595
596/// Get the C++ class name without a trailing * (e.g., "UObject", "AActor").
597fn resolve_object_cpp_type_bare(ctx: &CodegenContext, class_name: Option<&str>) -> String {
598    match class_name {
599        Some(cn) => {
600            if let Some(cls) = ctx.classes.get(cn) {
601                cls.cpp_name.clone()
602            } else {
603                cn.to_string()
604            }
605        }
606        None => "UObject".to_string(),
607    }
608}
609
610/// Resolve an object parameter's full C++ class name (e.g., "AActor*") from the context.
611fn resolve_object_cpp_type(ctx: &CodegenContext, class_name: Option<&str>) -> String {
612    match class_name {
613        Some(cn) => {
614            if let Some(cls) = ctx.classes.get(cn) {
615                format!("{}*", cls.cpp_name)
616            } else {
617                // Class not in context — use stripped name as-is
618                format!("{cn}*")
619            }
620        }
621        None => "UObject*".to_string(),
622    }
623}
624
625// ---------------------------------------------------------------------------
626// Input argument expressions
627// ---------------------------------------------------------------------------
628
629/// Build the C++ expression for passing an input argument.
630fn input_arg_expression(param: &ParamInfo, dir: ParamDirection, ctx: &CodegenContext) -> String {
631    if is_container_param(param) {
632        return format!("__Container_{}", param.name);
633    }
634    let mapped = map_param(param);
635
636    match mapped.rust_to_ffi {
637        ConversionKind::Identity => param.name.clone(),
638        ConversionKind::ObjectRef => {
639            let cpp_type = resolve_object_cpp_type(ctx, param.class_name.as_deref());
640            match param.prop_type.as_str() {
641                // TSoftObjectPtr<T> / TWeakObjectPtr<T> are value types constructed from UObject*.
642                "SoftObjectProperty" => {
643                    let bare_type = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
644                    format!("TSoftObjectPtr<{bare_type}>(static_cast<{cpp_type}>({}))", param.name)
645                }
646                "WeakObjectProperty" => {
647                    let bare_type = resolve_object_cpp_type_bare(ctx, param.class_name.as_deref());
648                    format!("TWeakObjectPtr<{bare_type}>(static_cast<{cpp_type}>({}))", param.name)
649                }
650                _ => format!("static_cast<{cpp_type}>({})", param.name),
651            }
652        }
653        ConversionKind::StringUtf8 => {
654            if dir == ParamDirection::InOut {
655                // InOut: use the pre-declared mutable local
656                format!("__InOut{}", param.name)
657            } else {
658                let fstring_expr = format!(
659                    "FString(UTF8_TO_TCHAR(std::string({name}, {name}Len).c_str()))",
660                    name = param.name
661                );
662                if param.prop_type == "TextProperty" {
663                    format!("FText::FromString({fstring_expr})")
664                } else {
665                    fstring_expr
666                }
667            }
668        }
669        ConversionKind::EnumCast => {
670            let cpp_enum = resolve_enum_cpp_type(param);
671            format!("static_cast<{cpp_enum}>({})", param.name)
672        }
673        ConversionKind::StructOpaque => {
674            if dir == ParamDirection::InOut {
675                // InOut struct: use the pre-declared mutable local
676                format!("__InOut{}", param.name)
677            } else {
678                let struct_cpp = resolve_struct_opaque_cpp_type(param, ctx);
679                format!(
680                    "*reinterpret_cast<const {struct_cpp}*>({})",
681                    param.name
682                )
683            }
684        }
685        ConversionKind::IntCast => param.name.clone(), // C++ native type, no conversion needed
686        ConversionKind::FName => {
687            format!("RustealUnpackFName({})", param.name)
688        }
689        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
690        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
691            unreachable!("container/delegate types are property-only, never function params"),
692    }
693}
694
695// ---------------------------------------------------------------------------
696// Return handling
697// ---------------------------------------------------------------------------
698
699fn return_strategy(param: &ParamInfo) -> ReturnStrategy {
700    if is_container_param(param) {
701        return ReturnStrategy::ContainerReturn;
702    }
703    let mapped = map_param(param);
704
705    match mapped.ffi_to_rust {
706        ConversionKind::Identity | ConversionKind::IntCast => ReturnStrategy::Direct,
707        ConversionKind::ObjectRef => {
708            // TSoftObjectPtr / TWeakObjectPtr return value types, not raw pointers.
709            // Need .Get() to extract the UObject* before casting to void*.
710            match param.prop_type.as_str() {
711                "SoftObjectProperty" | "WeakObjectProperty" => ReturnStrategy::SoftWeakObjectRef,
712                _ => ReturnStrategy::ObjectRef,
713            }
714        }
715        ConversionKind::StringUtf8 => {
716            if param.prop_type == "TextProperty" {
717                ReturnStrategy::TextReturn
718            } else {
719                ReturnStrategy::StringReturn
720            }
721        }
722        ConversionKind::EnumCast => {
723            let ctype = match mapped.rust_ffi_type.as_str() {
724                "u8" => "uint8_t",
725                "i8" => "int8_t",
726                "i32" => "int32_t",
727                _ => "uint8_t",
728            };
729            ReturnStrategy::Cast(ctype.to_string())
730        }
731        ConversionKind::StructOpaque => ReturnStrategy::StructReturn,
732        ConversionKind::FName => ReturnStrategy::FNameReturn,
733        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
734        | ConversionKind::Delegate | ConversionKind::MulticastDelegate =>
735            unreachable!("container/delegate types are property-only, never function return types"),
736    }
737}
738
739fn emit_return(out: &mut String, strategy: &ReturnStrategy, call_expr: &str) {
740    match strategy {
741        ReturnStrategy::Direct => {
742            out.push_str(&format!("    *OutReturnValue = {call_expr};\n"));
743        }
744        ReturnStrategy::ObjectRef => {
745            // const_cast needed for functions returning const T*
746            out.push_str(&format!(
747                "    *OutReturnValue = const_cast<void*>(static_cast<const void*>({call_expr}));\n"
748            ));
749        }
750        ReturnStrategy::SoftWeakObjectRef => {
751            // TSoftObjectPtr<T> / TWeakObjectPtr<T> are value types; call .Get()
752            // to resolve to the underlying UObject* before casting.
753            out.push_str(&format!(
754                "    *OutReturnValue = const_cast<void*>(static_cast<const void*>({call_expr}.Get()));\n"
755            ));
756        }
757        ReturnStrategy::Cast(ctype) => {
758            out.push_str(&format!(
759                "    *OutReturnValue = static_cast<{ctype}>({call_expr});\n"
760            ));
761        }
762        ReturnStrategy::StringReturn => {
763            out.push_str(&format!("    FString __RustealResult = {call_expr};\n"));
764            out.push_str(
765                "    auto __RustealUtf8 = StringCast<ANSICHAR>(*__RustealResult);\n\
766                 \x20   int32 __RustealLen = __RustealUtf8.Length();\n\
767                 \x20   if (__RustealLen > static_cast<int32>(BufLen)) __RustealLen = static_cast<int32>(BufLen);\n\
768                 \x20   FMemory::Memcpy(OutBuf, __RustealUtf8.Get(), __RustealLen);\n\
769                 \x20   *OutLen = static_cast<uint32_t>(__RustealLen);\n",
770            );
771        }
772        ReturnStrategy::TextReturn => {
773            out.push_str(&format!("    FString __RustealResult = ({call_expr}).ToString();\n"));
774            out.push_str(
775                "    auto __RustealUtf8 = StringCast<ANSICHAR>(*__RustealResult);\n\
776                 \x20   int32 __RustealLen = __RustealUtf8.Length();\n\
777                 \x20   if (__RustealLen > static_cast<int32>(BufLen)) __RustealLen = static_cast<int32>(BufLen);\n\
778                 \x20   FMemory::Memcpy(OutBuf, __RustealUtf8.Get(), __RustealLen);\n\
779                 \x20   *OutLen = static_cast<uint32_t>(__RustealLen);\n",
780            );
781        }
782        ReturnStrategy::StructReturn => {
783            out.push_str(&format!("    auto __RustealResult = {call_expr};\n"));
784            out.push_str(
785                "    FMemory::Memcpy(OutReturnValue, &__RustealResult, sizeof(__RustealResult));\n",
786            );
787        }
788        ReturnStrategy::FNameReturn => {
789            out.push_str(&format!("    FName __RustealResult = {call_expr};\n"));
790            out.push_str(
791                "    *OutReturnValue = RustealPackFName(__RustealResult);\n",
792            );
793        }
794        ReturnStrategy::ContainerReturn => {
795            // The reflected element types do not say everything the C++ type does
796            // (`TMap<TObjectPtr<UFoo>, int32>` and `TMap<UFoo*, int32>` reflect the
797            // same), so the returned container is written as the function's own
798            // return type; both share the property's memory layout.
799            out.push_str(&format!(
800                "    using __RustealReturnType = std::decay_t<decltype({call_expr})>;\n\
801                 \x20   *reinterpret_cast<__RustealReturnType*>(\n\
802                 \x20       static_cast<FProperty*>(OutReturnValue_Prop)->ContainerPtrToValuePtr<void>(OutReturnValue_Base))\n\
803                 \x20       = {call_expr};\n"
804            ));
805        }
806    }
807}
808
809/// Wrap an enum type with TEnumAsByte<> for non-enum-class enums.
810/// cpp_form 0 (Regular): `TEnumAsByte<EFoo>`
811/// cpp_form 1 (Namespaced): `TEnumAsByte<EFoo::Type>`
812/// cpp_form 2 (EnumClass): `EFoo` (no wrapping needed)
813fn wrap_enum_as_byte(name: &str, cpp_form: Option<u32>) -> String {
814    let form = cpp_form.unwrap_or(2);
815    match form {
816        0 => format!("TEnumAsByte<{name}>"),
817        1 => format!("TEnumAsByte<{name}::Type>"),
818        _ => name.to_string(),
819    }
820}
821
822/// Resolve the correct C++ enum type name based on cpp_form.
823/// cpp_form 0 (Regular) and 1 (Namespaced): `EFoo::Type`
824/// cpp_form 2 (EnumClass): `EFoo`
825fn resolve_enum_cpp_type(param: &ParamInfo) -> String {
826    let name = param
827        .enum_cpp_name
828        .as_deref()
829        .or(param.enum_name.as_deref())
830        .unwrap_or("uint8");
831    let form = param.enum_cpp_form.unwrap_or(2);
832    if form == 1 {
833        // Namespaced enum: namespace EFoo { enum Type { ... }; }
834        format!("{name}::Type")
835    } else {
836        // Regular (0) or EnumClass (2): use name directly
837        name.to_string()
838    }
839}
840
841fn emit_post_call(out: &mut String, action: &PostCallAction) {
842    match action {
843        PostCallAction::StringOutput(name) => {
844            out.push_str(&format!(
845                "    {{\n\
846                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Out{name});\n\
847                 \x20       int32 __Len = __Utf8.Length();\n\
848                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
849                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
850                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
851                 \x20   }}\n"
852            ));
853        }
854        PostCallAction::TextOutput(name) => {
855            // FText output: convert via .ToString() then UTF-8
856            out.push_str(&format!(
857                "    {{\n\
858                 \x20       FString __Str = __Out{name}.ToString();\n\
859                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Str);\n\
860                 \x20       int32 __Len = __Utf8.Length();\n\
861                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
862                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
863                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
864                 \x20   }}\n"
865            ));
866        }
867        PostCallAction::StructOutput { name, struct_cpp } => {
868            out.push_str(&format!(
869                "    if (Out{name}) {{ FMemory::Memcpy(Out{name}, &__Out{name}, sizeof({struct_cpp})); }}\n"
870            ));
871        }
872        PostCallAction::ObjectOutput(name) => {
873            out.push_str(&format!(
874                "    *Out{name} = static_cast<void*>(__Out{name});\n"
875            ));
876        }
877        PostCallAction::SoftWeakObjectOutput(name) => {
878            out.push_str(&format!(
879                "    *Out{name} = static_cast<void*>(__Out{name}.Get());\n"
880            ));
881        }
882        PostCallAction::FNameOutput(name) => {
883            out.push_str(&format!(
884                "    *Out{name} = RustealPackFName(__Out{name});\n"
885            ));
886        }
887        PostCallAction::EnumOutput { name, ffi_type } => {
888            out.push_str(&format!(
889                "    *Out{name} = static_cast<{ffi_type}>(__Out{name});\n"
890            ));
891        }
892        PostCallAction::InOutStructCopyback { name, struct_cpp } => {
893            out.push_str(&format!(
894                "    if ({name}) {{ FMemory::Memcpy({name}, &__InOut{name}, sizeof({struct_cpp})); }}\n"
895            ));
896        }
897        PostCallAction::InOutStringCopyback(name) => {
898            out.push_str(&format!(
899                "    {{\n\
900                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__InOut{name});\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        PostCallAction::InOutTextCopyback(name) => {
909            out.push_str(&format!(
910                "    {{\n\
911                 \x20       FString __Str = __InOut{name}.ToString();\n\
912                 \x20       auto __Utf8 = StringCast<ANSICHAR>(*__Str);\n\
913                 \x20       int32 __Len = __Utf8.Length();\n\
914                 \x20       if (__Len > static_cast<int32>(Out{name}BufLen)) __Len = static_cast<int32>(Out{name}BufLen);\n\
915                 \x20       FMemory::Memcpy(Out{name}, __Utf8.Get(), __Len);\n\
916                 \x20       *Out{name}Len = static_cast<uint32_t>(__Len);\n\
917                 \x20   }}\n"
918            ));
919        }
920    }
921}
922
923// ---------------------------------------------------------------------------
924// Container param helpers
925// ---------------------------------------------------------------------------
926
927fn is_container_param(param: &ParamInfo) -> bool {
928    matches!(
929        param.prop_type.as_str(),
930        "ArrayProperty" | "MapProperty" | "SetProperty"
931    )
932}
933
934/// Resolve the C++ element type for a container's inner property.
935fn resolve_inner_cpp_type(inner: &PropertyInfo, ctx: &CodegenContext) -> String {
936    match inner.prop_type.as_str() {
937        "BoolProperty" => "bool".to_string(),
938        "Int8Property" => "int8".to_string(),
939        "ByteProperty" => {
940            if let Some(ref en) = inner.enum_name {
941                let name = inner
942                    .enum_cpp_name
943                    .as_deref()
944                    .unwrap_or(en.as_str());
945                wrap_enum_as_byte(name, inner.enum_cpp_form)
946            } else {
947                "uint8".to_string()
948            }
949        }
950        "Int16Property" => "int16".to_string(),
951        "UInt16Property" => "uint16".to_string(),
952        "IntProperty" => "int32".to_string(),
953        "UInt32Property" => "uint32".to_string(),
954        "Int64Property" => "int64".to_string(),
955        "UInt64Property" => "uint64".to_string(),
956        "FloatProperty" => "float".to_string(),
957        "DoubleProperty" => "double".to_string(),
958        "StrProperty" => "FString".to_string(),
959        "TextProperty" => "FText".to_string(),
960        "NameProperty" => "FName".to_string(),
961        "ObjectProperty" => {
962            resolve_object_cpp_type(ctx, inner.class_name.as_deref())
963        }
964        "SoftObjectProperty" => {
965            let cls_cpp = resolve_object_cpp_type_bare(ctx, inner.class_name.as_deref());
966            format!("TSoftObjectPtr<{cls_cpp}>")
967        }
968        "WeakObjectProperty" => {
969            let cls_cpp = resolve_object_cpp_type_bare(ctx, inner.class_name.as_deref());
970            format!("TWeakObjectPtr<{cls_cpp}>")
971        }
972        "ClassProperty" => {
973            // Inside a container the element type must match the engine's exactly: a
974            // `TArray<TSubclassOf<X>>` is not a `TArray<X*>` (a direct parameter gets away
975            // with `UClass*` because TSubclassOf converts implicitly; an element does not).
976            match inner.meta_class_name.as_deref() {
977                Some(meta) => {
978                    let meta_cpp = resolve_object_cpp_type_bare(ctx, Some(meta));
979                    format!("TSubclassOf<{meta_cpp}>")
980                }
981                None => "UClass*".to_string(),
982            }
983        }
984        "InterfaceProperty" => {
985            resolve_object_cpp_type(ctx, inner.interface_name.as_deref())
986        }
987        "EnumProperty" => {
988            let name = inner
989                .enum_cpp_name
990                .as_deref()
991                .or(inner.enum_name.as_deref())
992                .unwrap_or("uint8");
993            wrap_enum_as_byte(name, inner.enum_cpp_form)
994        }
995        "StructProperty" => {
996            if let Some(ref sn) = inner.struct_name {
997                if let Some(si) = ctx.structs.get(sn.as_str()) {
998                    si.cpp_name.clone()
999                } else {
1000                    format!("F{sn}")
1001                }
1002            } else {
1003                "uint8".to_string()
1004            }
1005        }
1006        _ => "uint8".to_string(),
1007    }
1008}
1009
1010/// Resolve the full C++ container type (e.g., "TArray<AActor*>").
1011fn resolve_container_cpp_type(param: &ParamInfo, ctx: &CodegenContext) -> Option<String> {
1012    match param.prop_type.as_str() {
1013        "ArrayProperty" => {
1014            let inner = param.inner_prop.as_ref()?;
1015            let elem = resolve_inner_cpp_type(inner, ctx);
1016            Some(format!("TArray<{elem}>"))
1017        }
1018        "MapProperty" => {
1019            let key = param.key_prop.as_ref()?;
1020            let val = param.value_prop.as_ref()?;
1021            let kt = resolve_inner_cpp_type(key, ctx);
1022            let vt = resolve_inner_cpp_type(val, ctx);
1023            Some(format!("TMap<{kt}, {vt}>"))
1024        }
1025        "SetProperty" => {
1026            let elem = param.element_prop.as_ref()?;
1027            let et = resolve_inner_cpp_type(elem, ctx);
1028            Some(format!("TSet<{et}>"))
1029        }
1030        _ => None,
1031    }
1032}
1033
1034/// Collect headers needed by a parameter's referenced types (class, struct, etc.).
1035fn collect_param_headers(
1036    param: &ParamInfo,
1037    ctx: &CodegenContext,
1038    includes: &mut BTreeSet<String>,
1039) {
1040    // Direct class reference (ObjectProperty, ClassProperty)
1041    for cls_name in [
1042        param.class_name.as_deref(),
1043        param.meta_class_name.as_deref(),
1044    ]
1045    .into_iter()
1046    .flatten()
1047    {
1048        if let Some(cls) = ctx.classes.get(cls_name)
1049            && !cls.header.is_empty()
1050        {
1051            includes.insert(format!("\"{}\"", cls.header));
1052        }
1053    }
1054
1055    // Direct struct reference: a by-value struct or a container of structs needs
1056    // the complete type, which the function's own header may only forward-declare
1057    // (PlayerMappableInputConfig.h and FEnhancedActionKeyMapping).
1058    if let Some(sn) = param.struct_name.as_deref() {
1059        include_struct_header(sn, ctx, includes);
1060    }
1061
1062    // Container inner types
1063    for inner in [
1064        param.inner_prop.as_deref(),
1065        param.key_prop.as_deref(),
1066        param.value_prop.as_deref(),
1067        param.element_prop.as_deref(),
1068    ]
1069    .into_iter()
1070    .flatten()
1071    {
1072        for cls_name in [
1073            inner.class_name.as_deref(),
1074            inner.meta_class_name.as_deref(),
1075        ]
1076        .into_iter()
1077        .flatten()
1078        {
1079            if let Some(cls) = ctx.classes.get(cls_name)
1080                && !cls.header.is_empty()
1081            {
1082                includes.insert(format!("\"{}\"", cls.header));
1083            }
1084        }
1085        if let Some(sn) = inner.struct_name.as_deref() {
1086            include_struct_header(sn, ctx, includes);
1087        }
1088    }
1089}
1090
1091fn include_struct_header(struct_name: &str, ctx: &CodegenContext, includes: &mut BTreeSet<String>) {
1092    if let Some(st) = ctx.structs.get(struct_name)
1093        && !st.header.is_empty()
1094    {
1095        includes.insert(format!("\"{}\"", st.header));
1096    }
1097}