Skip to main content

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