Skip to main content

rusteal_codegen/rust_gen/
classes.rs

1// Rust class generation: marker types, UeClass trait, properties, functions.
2
3use std::collections::HashSet;
4
5use crate::context::{CodegenContext, FuncEntry};
6use crate::defaults;
7use crate::naming::{escape_reserved, to_snake_case};
8use crate::schema::*;
9use crate::type_map::{self, ConversionKind, MappedType, ParamDirection};
10
11use super::delegates;
12use super::param_helpers;
13use super::properties::{self, PropertyContext};
14
15/// Generate Rust code for a single UE class.
16pub fn generate_class(class: &ClassInfo, ctx: &CodegenContext) -> String {
17    let mut out = String::with_capacity(8192);
18
19    // Import traits and types from own module and all other enabled modules
20    out.push_str("use super::*;\n");
21    out.push_str("use rusteal_core::{UeClass, UeStruct, UeEnum, UeHandle, ValidHandle, Pinned, Checked};\n");
22    let current_module = ctx.package_to_module.get(&class.package).map(|s| s.as_str()).unwrap_or("");
23    // Sorted: the generated crate is versioned with the project, so the output
24    // has to be the same on every run (enabled_modules is a HashSet).
25    let mut modules: Vec<&String> = ctx.enabled_modules.iter().collect();
26    modules.sort();
27    for module in modules {
28        if module != current_module {
29            if let Some(feature) = ctx.feature_for_module(module) {
30                out.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
31            }
32            out.push_str(&format!("use crate::{module}::*;\n"));
33        }
34    }
35    out.push('\n');
36
37    let name = &class.name;         // e.g., "Actor"
38    let cpp_name = &class.cpp_name; // e.g., "AActor"
39
40    // Use the JSON `name` as the Rust struct name.
41    // This keeps it consistent with UE naming (Actor, Pawn, etc.)
42    out.push_str(&format!(
43        "/// UE class `{cpp_name}`.\n\
44         pub struct {name};\n\n"
45    ));
46
47    // UeClass trait impl
48    let name_bytes_len = name.len();
49    let byte_lit = format!("b\"{}\\0\"", name);
50    out.push_str(&format!(
51        "impl rusteal_core::UeClass for {name} {{\n\
52         \x20   fn static_class() -> rusteal_core::UClassHandle {{\n\
53         \x20       static CACHE: std::sync::OnceLock<rusteal_core::UClassHandle> = std::sync::OnceLock::new();\n\
54         \x20       *CACHE.get_or_init(|| unsafe {{\n\
55         \x20           rusteal_core::ffi_dispatch::reflection_get_static_class({byte_lit}.as_ptr(), {name_bytes_len})\n\
56         \x20       }})\n\
57         \x20   }}\n\
58         }}\n\n"
59    ));
60
61    // HasParent impl (must come before early-return — a class with no own
62    // members still needs HasParent for the Deref chain)
63    if let Some(parent) = &class.super_class
64        && ctx.classes.contains_key(parent.as_str())
65    {
66        // Cfg-gate if parent is in a different module
67        let parent_class = ctx.classes.get(parent.as_str()).unwrap();
68        let parent_module = ctx.package_to_module.get(&parent_class.package)
69            .map(|s| s.as_str()).unwrap_or("");
70        if parent_module != current_module
71            && let Some(feature) = ctx.feature_for_module(parent_module) {
72                out.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
73            }
74        out.push_str(&format!(
75            "impl rusteal_core::HasParent for {name} {{\n\
76             \x20   type Parent = {parent};\n\
77             }}\n\n"
78        ));
79    }
80
81    // Inherits: the class is itself and each of its ancestors, for
82    // `UObjectRef::upcast_to`.
83    let mut ancestor = Some(name.as_str());
84    while let Some(class_name) = ancestor {
85        let Some(ancestor_class) = ctx.classes.get(class_name) else {
86            break;
87        };
88        let ancestor_module = ctx
89            .package_to_module
90            .get(&ancestor_class.package)
91            .map(|s| s.as_str())
92            .unwrap_or("");
93        if ancestor_module != current_module
94            && let Some(feature) = ctx.feature_for_module(ancestor_module)
95        {
96            out.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
97        }
98        out.push_str(&format!("impl rusteal_core::Inherits<{class_name}> for {name} {{}}\n"));
99        ancestor = ancestor_class.super_class.as_deref();
100    }
101    out.push('\n');
102
103    // Collect own functions only (inherited methods are accessed via Deref chain)
104    let mut seen_func_names: HashSet<String> = HashSet::new();
105    let mut class_funcs: Vec<&FuncEntry> = Vec::new();
106    for entry in ctx.func_table.iter().filter(|e| e.class_name == *name) {
107        if seen_func_names.insert(entry.rust_func_name.clone()) {
108            class_funcs.push(entry);
109        }
110    }
111
112    // Collect own property accessor names, deduplicating
113    let (mut prop_names, deduped_props) = properties::collect_deduped_properties(&class.props, Some(ctx));
114
115    // Collect own delegate properties
116    let own_delegate_infos = delegates::collect_delegate_props(&class.props, name, ctx);
117
118    // Only generate extension trait if there are own properties, functions, or delegates
119    if deduped_props.is_empty() && class_funcs.is_empty()
120        && own_delegate_infos.is_empty()
121    {
122        return out;
123    }
124
125    // Detect setter-function collisions: when a UFUNCTION matches a property setter name,
126    // keep the UFUNCTION and suppress the setter (Option B from TODO_IMPROVEMENTS)
127    let func_names: HashSet<String> = class_funcs
128        .iter()
129        .map(|e| escape_reserved(&e.rust_func_name))
130        .collect();
131
132    let mut suppress_setters: HashSet<String> = prop_names
133        .iter()
134        .filter(|n| n.starts_with("set_") && func_names.contains(n.as_str()))
135        .cloned()
136        .collect();
137
138    // A private or protected property Blueprint may only read (a component such
139    // as ACharacter's CharacterMovement) gets no setter either.
140    suppress_setters.extend(
141        deduped_props
142            .iter()
143            .filter(|p| properties::is_read_only_non_public(p))
144            .map(|p| format!("set_{}", properties::rust_property_name(p))),
145    );
146
147    // Remove suppressed setters from prop_names so they don't block UFUNCTIONs
148    for setter in &suppress_setters {
149        prop_names.remove(setter);
150    }
151
152    // Filter out functions whose names still collide with remaining property accessors
153    let class_funcs: Vec<&FuncEntry> = class_funcs
154        .into_iter()
155        .filter(|e| !prop_names.contains(&escape_reserved(&e.rust_func_name)))
156        .collect();
157
158    // PropertyContext for class properties
159    let pctx = PropertyContext {
160        find_prop_fn: "find_property".to_string(),
161        handle_expr: format!("{name}::static_class()"),
162        pre_access: "let h = self.handle();".to_string(),
163        container_expr: "h".to_string(),
164        is_class: true,
165    };
166
167    // Generate delegate wrapper structs (own only)
168    delegates::generate_delegate_structs(&mut out, &own_delegate_infos, name);
169
170    // Extension trait with ValidHandle supertrait — default impls work for both
171    // Checked<T> and Pinned<T> (dispatch via handle()).
172    let trait_name = format!("{name}Ext");
173    out.push_str(&format!(
174        "pub trait {trait_name}: rusteal_core::ValidHandle {{\n"
175    ));
176
177    // Property getters/setters as default impls
178    for prop in &deduped_props {
179        properties::generate_property(&mut out, prop, &pctx, ctx, &suppress_setters);
180    }
181
182    // Delegate accessor default impls (own only)
183    delegates::generate_delegate_impls(&mut out, &own_delegate_infos);
184
185    // Function wrapper default impls (instance functions; the static ones
186    // go in the inherent impl below)
187    let (static_funcs, instance_funcs): (Vec<&FuncEntry>, Vec<&FuncEntry>) =
188        class_funcs.iter().partition(|e| is_static_fn(e));
189    for entry in &instance_funcs {
190        generate_function(&mut out, entry, &entry.class_name, ctx);
191    }
192
193    out.push_str("}\n\n");
194
195    // Static functions (function libraries, `UGameplayStatics::...`) are
196    // called on the class, as in C++: `GameplayStatics::get_player_controller(..)`.
197    if !static_funcs.is_empty() {
198        out.push_str(&format!("impl {name} {{\n"));
199        for entry in &static_funcs {
200            generate_function(&mut out, entry, &entry.class_name, ctx);
201        }
202        out.push_str("}\n\n");
203    }
204
205    // Empty impls — Checked and Pinned both satisfy ValidHandle
206    out.push_str(&format!(
207        "impl {trait_name} for rusteal_core::Checked<{name}> {{}}\n"
208    ));
209    out.push_str(&format!(
210        "impl {trait_name} for rusteal_core::Pinned<{name}> {{}}\n"
211    ));
212
213    out
214}
215
216fn is_static_fn(entry: &FuncEntry) -> bool {
217    entry.func.is_static || (entry.func.func_flags & FUNC_STATIC != 0)
218}
219
220// ---------------------------------------------------------------------------
221// Container param helpers (delegated to type_map)
222// ---------------------------------------------------------------------------
223
224pub(super) use type_map::is_container_param;
225pub(super) use type_map::container_param_input_type;
226pub(super) use type_map::container_param_output_type;
227pub(super) use type_map::container_elem_type_str;
228
229/// Build the composite return type from all output components.
230fn build_return_type(output_types: &[String]) -> String {
231    match output_types.len() {
232        0 => "()".to_string(),
233        1 => output_types[0].clone(),
234        _ => format!("({})", output_types.join(", ")),
235    }
236}
237
238/// Get the Rust type for a scalar Out/InOut param or ReturnValue in a return tuple.
239/// StructOpaque returns `OwnedStruct<FStructName>` when the struct has UeStruct,
240/// otherwise falls back to the raw pointer type.
241fn scalar_out_rust_type_ctx(mapped: &MappedType, struct_name: Option<&str>, ctx: &CodegenContext) -> String {
242    match mapped.ffi_to_rust {
243        ConversionKind::StructOpaque => {
244            if let Some(sn) = struct_name
245                && let Some(si) = ctx.structs.get(sn)
246                    && si.has_static_struct
247            {
248                    return format!("rusteal_core::OwnedStruct<{}>", si.cpp_name);
249            }
250            // Struct not available or no static_struct — use raw pointer
251            mapped.rust_type.clone()
252        }
253        _ => mapped.rust_type.clone(),
254    }
255}
256
257/// Check if a StructOpaque return/out can use OwnedStruct (has valid UeStruct impl).
258pub(super) fn is_struct_owned(struct_name: Option<&str>, ctx: &CodegenContext) -> bool {
259    struct_name.is_some_and(|sn| ctx.structs.get(sn).is_some_and(|si| si.has_static_struct))
260}
261
262/// Check if a scalar Out/InOut param should be included in the return tuple.
263/// InOut StructOpaque params write back through the mutable pointer, so they
264/// are NOT included in the return tuple.
265pub(super) fn is_scalar_output_returnable(dir: ParamDirection, mapped: &MappedType) -> bool {
266    if dir == ParamDirection::InOut && mapped.ffi_to_rust == ConversionKind::StructOpaque {
267        return false;
268    }
269    dir == ParamDirection::Out || dir == ParamDirection::InOut
270}
271
272// ---------------------------------------------------------------------------
273// Function implementation (dispatch)
274// ---------------------------------------------------------------------------
275
276/// Generate a function wrapper (direct call via func_table).
277fn generate_function(out: &mut String, entry: &FuncEntry, class_name: &str, ctx: &CodegenContext) {
278    let has_container = entry.func.params.iter().any(is_container_param);
279    if has_container {
280        generate_container_function(out, entry, class_name, ctx);
281    } else {
282        generate_scalar_function(out, entry, class_name, ctx);
283    }
284}
285
286// ---------------------------------------------------------------------------
287// Scalar function implementation (no container params — original path)
288// ---------------------------------------------------------------------------
289
290fn generate_scalar_function(out: &mut String, entry: &FuncEntry, class_name: &str, ctx: &CodegenContext) {
291    let func = &entry.func;
292    let rust_fn_name = escape_reserved(&entry.rust_func_name);
293    let func_id = entry.func_id;
294
295    // Classify params
296    let mut return_param: Option<&ParamInfo> = None;
297
298    for param in &func.params {
299        let dir = type_map::param_direction(param);
300        if dir == ParamDirection::Return {
301            return_param = Some(param);
302        }
303    }
304
305    // Map types for all params
306    let mut all_mapped: Vec<(&ParamInfo, ParamDirection, MappedType)> = Vec::new();
307    let mut all_supported = true;
308
309    for param in &func.params {
310        let dir = type_map::param_direction(param);
311        let mapped = type_map::map_property_type(
312            &param.prop_type,
313            param.class_name.as_deref(),
314            param.struct_name.as_deref(),
315            param.enum_name.as_deref(),
316            param.enum_underlying_type.as_deref(),
317            param.meta_class_name.as_deref(),
318            param.interface_name.as_deref(),
319        );
320        if !mapped.supported {
321            all_supported = false;
322            break;
323        }
324        all_mapped.push((param, dir, mapped));
325    }
326
327    if !all_supported {
328        out.push_str(&format!(
329            "    // Skipped: {}.{} (unsupported param type)\n\n",
330            class_name, func.name
331        ));
332        return;
333    }
334
335    // Determine return type
336    let ret_mapped = return_param.map(|rp| {
337        type_map::map_property_type(
338            &rp.prop_type,
339            rp.class_name.as_deref(),
340            rp.struct_name.as_deref(),
341            rp.enum_name.as_deref(),
342            rp.enum_underlying_type.as_deref(),
343            rp.meta_class_name.as_deref(),
344            rp.interface_name.as_deref(),
345        )
346    });
347
348    // Build return type: ReturnValue + all Out/InOut scalar params
349    let return_rust_type = {
350        let mut output_types = Vec::new();
351        if let Some(m) = &ret_mapped {
352            let rp_struct = return_param.and_then(|rp| rp.struct_name.as_deref());
353            output_types.push(scalar_out_rust_type_ctx(m, rp_struct, ctx));
354        }
355        for (param, dir, mapped) in &all_mapped {
356            if is_scalar_output_returnable(*dir, mapped) {
357                output_types.push(scalar_out_rust_type_ctx(mapped, param.struct_name.as_deref(), ctx));
358            }
359        }
360        build_return_type(&output_types)
361    };
362
363    // Build FFI type signature
364    let is_static = func.is_static || (func.func_flags & FUNC_STATIC != 0);
365
366    // Build Rust function signature
367    let mut sig = String::new();
368    if is_static {
369        sig.push_str(&format!(
370            "    pub fn {rust_fn_name}("
371        ));
372    } else {
373        sig.push_str(&format!(
374            "    fn {rust_fn_name}(&self, "
375        ));
376    }
377
378    // Input params
379    let mut param_names = Vec::new();
380    let mut default_unwraps: Vec<(String, String)> = Vec::new(); // (pname, default_expr)
381    for (param, dir, mapped) in &all_mapped {
382        if *dir == ParamDirection::Return {
383            continue;
384        }
385        let pname = escape_reserved(&to_snake_case(&param.name));
386        let has_default = *dir == ParamDirection::In
387            && defaults::parse_default_literal(param, mapped, ctx).is_some();
388        if has_default {
389            let default_expr = defaults::parse_default_literal(param, mapped, ctx)
390                .expect("default literal must be parseable (has_default was true)");
391            default_unwraps.push((pname.clone(), default_expr));
392        }
393        match dir {
394            ParamDirection::In | ParamDirection::InOut => {
395                match mapped.rust_to_ffi {
396                    ConversionKind::StringUtf8 => {
397                        if has_default {
398                            sig.push_str(&format!("{pname}: Option<&str>, "));
399                        } else {
400                            sig.push_str(&format!("{pname}: &str, "));
401                        }
402                    }
403                    ConversionKind::StructOpaque if *dir == ParamDirection::In
404                        && is_struct_owned(param.struct_name.as_deref(), ctx) =>
405                    {
406                        let si = ctx.structs.get(param.struct_name.as_deref().expect("StructOpaque param must have struct_name"))
407                            .expect("struct must exist in context");
408                        sig.push_str(&format!(
409                            "{pname}: &rusteal_core::OwnedStruct<{}>, ", si.cpp_name
410                        ));
411                    }
412                    ConversionKind::StructOpaque if *dir == ParamDirection::InOut => {
413                        sig.push_str(&format!("{pname}: *mut u8, "));
414                    }
415                    _ => {
416                        if has_default {
417                            sig.push_str(&format!("{pname}: Option<{}>, ", mapped.rust_type));
418                        } else {
419                            sig.push_str(&format!("{pname}: {}, ", mapped.rust_type));
420                        }
421                    }
422                }
423            }
424            ParamDirection::Out => {
425                // Output params are returned as additional outputs — skip from signature for now
426            }
427            ParamDirection::Return => {}
428        }
429        param_names.push((pname, param, *dir, mapped));
430    }
431
432    // Remove trailing comma+space
433    if sig.ends_with(", ") {
434        sig.truncate(sig.len() - 2);
435    }
436
437    if return_rust_type == "()" {
438        sig.push(')');
439    } else {
440        sig.push_str(&format!(") -> {return_rust_type}"));
441    }
442
443    out.push_str(&sig);
444    out.push_str(" {\n");
445
446    // Unwrap defaulted params before any FFI conversion
447    for (pname, default_expr) in &default_unwraps {
448        out.push_str(&format!("        let {pname} = {pname}.unwrap_or({default_expr});\n"));
449    }
450
451    // FFI dispatch: load wrapper pointer from func_table and transmute to typed fn.
452    out.push_str("        {\n");
453
454    // Build FFI fn type signature
455    let mut ffi_params = String::new();
456    if !is_static {
457        ffi_params.push_str("rusteal_core::UObjectHandle, ");
458    }
459    for (_param, dir, mapped) in &all_mapped {
460        match dir {
461            ParamDirection::In | ParamDirection::InOut => {
462                match mapped.rust_to_ffi {
463                    ConversionKind::StringUtf8 => {
464                        ffi_params.push_str("*const u8, u32, ");
465                        // InOut strings also have output buffer params
466                        if *dir == ParamDirection::InOut {
467                            ffi_params.push_str("*mut u8, u32, *mut u32, ");
468                        }
469                    }
470                    ConversionKind::ObjectRef => {
471                        ffi_params.push_str("rusteal_core::UObjectHandle, ");
472                    }
473                    ConversionKind::EnumCast => {
474                        ffi_params.push_str(&format!("{}, ", mapped.rust_ffi_type));
475                    }
476                    ConversionKind::StructOpaque => {
477                        if *dir == ParamDirection::InOut {
478                            ffi_params.push_str("*mut u8, "); // mutable: data flows both ways
479                        } else {
480                            ffi_params.push_str("*const u8, ");
481                        }
482                    }
483                    _ => {
484                        ffi_params.push_str(&format!("{}, ", mapped.rust_ffi_type));
485                    }
486                }
487            }
488            ParamDirection::Out | ParamDirection::Return => {
489                match mapped.ffi_to_rust {
490                    ConversionKind::StringUtf8 => {
491                        ffi_params.push_str("*mut u8, u32, *mut u32, ");
492                    }
493                    ConversionKind::ObjectRef => {
494                        ffi_params.push_str("*mut rusteal_core::UObjectHandle, ");
495                    }
496                    ConversionKind::StructOpaque => {
497                        ffi_params.push_str("*mut u8, ");
498                    }
499                    ConversionKind::EnumCast => {
500                        ffi_params.push_str(&format!("*mut {}, ", mapped.rust_ffi_type));
501                    }
502                    _ => {
503                        ffi_params.push_str(&format!("*mut {}, ", mapped.rust_ffi_type));
504                    }
505                }
506            }
507        }
508    }
509    // Remove trailing comma+space
510    if ffi_params.ends_with(", ") {
511        ffi_params.truncate(ffi_params.len() - 2);
512    }
513
514    out.push_str(&format!(
515        "        const FN_ID: u32 = {func_id};\n\
516         \x20       type Fn = unsafe extern \"C\" fn({ffi_params}) -> rusteal_core::RustealErrorCode;\n\
517         \x20       let __rusteal_fn: Fn = unsafe {{ std::mem::transmute(*(rusteal_core::api().func_table.add(FN_ID as usize))) }};\n"
518    ));
519
520    // Get handle for instance methods (pre-validated via ValidHandle)
521    if !is_static {
522        out.push_str("        let h = self.handle();\n");
523    }
524
525    // Declare output variables
526    if let Some(_rp) = return_param {
527        let rm = ret_mapped.as_ref().expect("return param must have mapped type");
528        match rm.ffi_to_rust {
529            ConversionKind::ObjectRef => {
530                out.push_str("        let mut _ret = rusteal_core::UObjectHandle::null();\n");
531            }
532            ConversionKind::StringUtf8 => {
533                out.push_str("        let mut _ret_buf = vec![0u8; 512];\n");
534                out.push_str("        let mut _ret_len: u32 = 0;\n");
535            }
536            ConversionKind::EnumCast => {
537                out.push_str(&format!("        let mut _ret: {} = 0;\n", rm.rust_ffi_type));
538            }
539            ConversionKind::StructOpaque => {
540                out.push_str("        let mut _ret_struct_buf = vec![0u8; 256];\n");
541            }
542            _ => {
543                let default = properties::default_value_for(&rm.rust_ffi_type);
544                out.push_str(&format!("        let mut _ret = {default};\n"));
545            }
546        }
547    }
548
549    for (param, dir, mapped) in &all_mapped {
550        if *dir == ParamDirection::Out {
551            param_helpers::emit_out_param_var_decl(out, param, mapped);
552        }
553        if *dir == ParamDirection::InOut {
554            param_helpers::emit_inout_string_buf_decl(out, param, mapped);
555        }
556    }
557
558    // Build the FFI call (infallible after pre-validation)
559    out.push_str("        rusteal_core::ffi_infallible(unsafe { __rusteal_fn(");
560    if !is_static {
561        out.push_str("h, ");
562    }
563    for (param, dir, mapped) in &all_mapped {
564        let pname = escape_reserved(&to_snake_case(&param.name));
565        match dir {
566            ParamDirection::In | ParamDirection::InOut => {
567                match mapped.rust_to_ffi {
568                    ConversionKind::StringUtf8 => {
569                        out.push_str(&format!("{pname}.as_ptr(), {pname}.len() as u32, "));
570                        // InOut strings also pass output buffer params
571                        if *dir == ParamDirection::InOut {
572                            out.push_str(&format!("{pname}_buf.as_mut_ptr(), {pname}_buf.len() as u32, &mut {pname}_len, "));
573                        }
574                    }
575                    ConversionKind::ObjectRef => {
576                        out.push_str(&format!("rusteal_core::ObjectPointer::object_handle(&{pname}), "));
577                    }
578                    ConversionKind::EnumCast => {
579                        out.push_str(&format!("{pname} as {}, ", mapped.rust_ffi_type));
580                    }
581                    ConversionKind::StructOpaque if *dir == ParamDirection::In
582                        && is_struct_owned(param.struct_name.as_deref(), ctx) =>
583                    {
584                        out.push_str(&format!("{pname}.as_bytes().as_ptr(), "));
585                    }
586                    _ => {
587                        out.push_str(&format!("{pname}, "));
588                    }
589                }
590            }
591            ParamDirection::Out => {
592                match mapped.ffi_to_rust {
593                    ConversionKind::StructOpaque => {
594                        out.push_str(&format!("{pname}_buf.as_mut_ptr(), "));
595                    }
596                    ConversionKind::StringUtf8 => {
597                        out.push_str(&format!("{pname}_buf.as_mut_ptr(), {pname}_buf.len() as u32, &mut {pname}_len, "));
598                    }
599                    _ => {
600                        out.push_str(&format!("&mut {pname}, "));
601                    }
602                }
603            }
604            ParamDirection::Return => {
605                let rm = ret_mapped.as_ref().expect("return param must have mapped type");
606                match rm.ffi_to_rust {
607                    ConversionKind::StringUtf8 => {
608                        out.push_str("_ret_buf.as_mut_ptr(), _ret_buf.len() as u32, &mut _ret_len, ");
609                    }
610                    ConversionKind::ObjectRef => {
611                        out.push_str("&mut _ret, ");
612                    }
613                    ConversionKind::StructOpaque => {
614                        out.push_str("_ret_struct_buf.as_mut_ptr(), ");
615                    }
616                    _ => {
617                        out.push_str("&mut _ret, ");
618                    }
619                }
620            }
621        }
622    }
623    // Remove trailing comma+space in the call args
624    let out_len = out.len();
625    if out.ends_with(", ") {
626        out.truncate(out_len - 2);
627    }
628    out.push_str(") });\n");
629
630    // Return conversion: assemble ReturnValue + Out/InOut params (infallible)
631    {
632        let mut return_parts = Vec::new();
633
634        // ReturnValue
635        if let Some(rp) = return_param {
636            let rm = ret_mapped.as_ref().expect("return param must have mapped type");
637            match rm.ffi_to_rust {
638                ConversionKind::ObjectRef => {
639                    return_parts.push("unsafe { rusteal_core::ObjectPointer::from_object_handle(_ret) }".to_string());
640                }
641                ConversionKind::StringUtf8 => {
642                    out.push_str("        _ret_buf.truncate(_ret_len as usize);\n");
643                    out.push_str("        let _ret_str = String::from_utf8_lossy(&_ret_buf).into_owned();\n");
644                    return_parts.push("_ret_str".to_string());
645                }
646                ConversionKind::EnumCast => {
647                    let rt = &rm.rust_type;
648                    let actual_repr = rp.enum_name.as_deref()
649                        .and_then(|en| ctx.enum_actual_repr(en))
650                        .unwrap_or(&rm.rust_ffi_type);
651                    out.push_str(&format!("        let _ret_enum = {rt}::from_value(_ret as {actual_repr}).expect(\"unknown enum value\");\n"));
652                    return_parts.push("_ret_enum".to_string());
653                }
654                ConversionKind::StructOpaque => {
655                    if is_struct_owned(rp.struct_name.as_deref(), ctx) {
656                        out.push_str("        let _ret_owned = rusteal_core::OwnedStruct::from_bytes(_ret_struct_buf);\n");
657                        return_parts.push("_ret_owned".to_string());
658                    } else {
659                        out.push_str("        let _ret_ptr = _ret_struct_buf.as_ptr();\n");
660                        out.push_str("        std::mem::forget(_ret_struct_buf);\n");
661                        return_parts.push("_ret_ptr".to_string());
662                    }
663                }
664                _ => {
665                    return_parts.push("_ret".to_string());
666                }
667            }
668        }
669
670        // Out/InOut params (skip InOut StructOpaque — data written back in-place)
671        for (param, dir, mapped) in &all_mapped {
672            if !is_scalar_output_returnable(*dir, mapped) {
673                continue;
674            }
675            return_parts.push(param_helpers::emit_out_param_conversion(
676                out, param, mapped, ctx,
677            ));
678        }
679
680        param_helpers::emit_return_expr(out, &return_parts);
681    }
682
683    out.push_str("        }\n");
684    out.push_str("    }\n\n");
685}
686
687// ---------------------------------------------------------------------------
688// Container function implementation
689// ---------------------------------------------------------------------------
690
691/// Metadata about a container parameter tracked during code generation.
692struct ContainerParamMeta<'a> {
693    param: &'a ParamInfo,
694    dir: ParamDirection,
695    /// Index in the CPROPS array.
696    index: usize,
697}
698
699/// Generate a function wrapper for functions that have container parameters.
700/// Uses alloc_temp/free_temp for temp container lifecycle management.
701fn generate_container_function(out: &mut String, entry: &FuncEntry, class_name: &str, ctx: &CodegenContext) {
702    let func = &entry.func;
703    let rust_fn_name = escape_reserved(&entry.rust_func_name);
704    let func_id = entry.func_id;
705    let is_static = func.is_static || (func.func_flags & FUNC_STATIC != 0);
706    let ue_name = if func.ue_name.is_empty() { &entry.func_name } else { &func.ue_name };
707
708    // Collect container params with their indices
709    let mut container_params: Vec<ContainerParamMeta> = Vec::new();
710    for param in &func.params {
711        if is_container_param(param) {
712            let dir = type_map::param_direction(param);
713            let index = container_params.len();
714            container_params.push(ContainerParamMeta { param, dir, index });
715        }
716    }
717    let n_containers = container_params.len();
718
719    // Classify all params and check support
720    let mut return_param: Option<&ParamInfo> = None;
721    let mut all_supported = true;
722
723    for param in &func.params {
724        let dir = type_map::param_direction(param);
725        if dir == ParamDirection::Return {
726            return_param = Some(param);
727        }
728        if is_container_param(param) {
729            if (dir == ParamDirection::In || dir == ParamDirection::InOut)
730                && container_param_input_type(param, ctx).is_none()
731            {
732                all_supported = false;
733                break;
734            }
735            if (dir == ParamDirection::Out || dir == ParamDirection::Return || dir == ParamDirection::InOut)
736                && container_param_output_type(param, ctx).is_none()
737            {
738                all_supported = false;
739                break;
740            }
741        } else {
742            let mapped = type_map::map_property_type(
743                &param.prop_type, param.class_name.as_deref(),
744                param.struct_name.as_deref(), param.enum_name.as_deref(),
745                param.enum_underlying_type.as_deref(),
746                param.meta_class_name.as_deref(),
747                param.interface_name.as_deref(),
748            );
749            if !mapped.supported {
750                all_supported = false;
751                break;
752            }
753        }
754    }
755
756    if !all_supported {
757        out.push_str(&format!(
758            "    // Skipped: {}.{} (unsupported container inner type)\n\n",
759            class_name, func.name
760        ));
761        return;
762    }
763
764    // Build return type
765    let mut output_types = Vec::new();
766    let mut scalar_return_mapped: Option<MappedType> = None;
767
768    if let Some(rp) = return_param {
769        if is_container_param(rp) {
770            output_types.push(container_param_output_type(rp, ctx)
771                    .expect("container return type should be resolvable"));
772        } else {
773            let rm = type_map::map_property_type(
774                &rp.prop_type, rp.class_name.as_deref(),
775                rp.struct_name.as_deref(), rp.enum_name.as_deref(),
776                rp.enum_underlying_type.as_deref(),
777                rp.meta_class_name.as_deref(),
778                rp.interface_name.as_deref(),
779            );
780            output_types.push(scalar_out_rust_type_ctx(&rm, rp.struct_name.as_deref(), ctx));
781            scalar_return_mapped = Some(rm);
782        }
783    }
784    for param in &func.params {
785        let dir = type_map::param_direction(param);
786        if dir == ParamDirection::Out || dir == ParamDirection::InOut {
787            if is_container_param(param) {
788                output_types.push(container_param_output_type(param, ctx)
789                        .expect("container out-param type should be resolvable"));
790            } else {
791                let rm = type_map::map_property_type(
792                    &param.prop_type, param.class_name.as_deref(),
793                    param.struct_name.as_deref(), param.enum_name.as_deref(),
794                    param.enum_underlying_type.as_deref(),
795                    param.meta_class_name.as_deref(),
796                    param.interface_name.as_deref(),
797                );
798                if is_scalar_output_returnable(dir, &rm) {
799                    output_types.push(scalar_out_rust_type_ctx(&rm, param.struct_name.as_deref(), ctx));
800                }
801            }
802        }
803    }
804    let return_rust_type = build_return_type(&output_types);
805
806    // === Emit Rust function signature ===
807    let mut sig = String::new();
808    if is_static {
809        sig.push_str(&format!("    pub fn {rust_fn_name}("));
810    } else {
811        sig.push_str(&format!("    fn {rust_fn_name}(&self, "));
812    }
813
814    let mut default_unwraps: Vec<(String, String)> = Vec::new();
815    for param in &func.params {
816        let dir = type_map::param_direction(param);
817        if dir == ParamDirection::Return || dir == ParamDirection::Out {
818            continue;
819        }
820        let pname = escape_reserved(&to_snake_case(&param.name));
821        if is_container_param(param) {
822            let input_type = container_param_input_type(param, ctx)
823                .expect("container input type should be resolvable");
824            sig.push_str(&format!("{pname}: {input_type}, "));
825        } else {
826            let mapped = map_param(param);
827            let has_default = dir == ParamDirection::In
828                && defaults::parse_default_literal(param, &mapped, ctx).is_some();
829            if has_default {
830                let default_expr = defaults::parse_default_literal(param, &mapped, ctx)
831                    .expect("default literal must be parseable (has_default was true)");
832                default_unwraps.push((pname.clone(), default_expr));
833            }
834            match mapped.rust_to_ffi {
835                ConversionKind::StringUtf8 => {
836                    if has_default {
837                        sig.push_str(&format!("{pname}: Option<&str>, "));
838                    } else {
839                        sig.push_str(&format!("{pname}: &str, "));
840                    }
841                }
842                ConversionKind::StructOpaque if dir == ParamDirection::In
843                    && is_struct_owned(param.struct_name.as_deref(), ctx) =>
844                {
845                    let si = ctx.structs.get(param.struct_name.as_deref().expect("StructOpaque param must have struct_name"))
846                            .expect("struct must exist in context");
847                    sig.push_str(&format!(
848                        "{pname}: &rusteal_core::OwnedStruct<{}>, ", si.cpp_name
849                    ));
850                }
851                ConversionKind::StructOpaque if dir == ParamDirection::InOut => {
852                    sig.push_str(&format!("{pname}: *mut u8, "));
853                }
854                _ => {
855                    if has_default {
856                        sig.push_str(&format!("{pname}: Option<{}>, ", mapped.rust_type));
857                    } else {
858                        sig.push_str(&format!("{pname}: {}, ", mapped.rust_type));
859                    }
860                }
861            }
862        }
863    }
864    if sig.ends_with(", ") {
865        sig.truncate(sig.len() - 2);
866    }
867    if return_rust_type == "()" {
868        sig.push(')');
869    } else {
870        sig.push_str(&format!(") -> {return_rust_type}"));
871    }
872    out.push_str(&sig);
873    out.push_str(" {\n");
874
875    // Unwrap defaulted params before any FFI conversion
876    for (pname, default_expr) in &default_unwraps {
877        out.push_str(&format!("        let {pname} = {pname}.unwrap_or({default_expr});\n"));
878    }
879
880    out.push_str("        {\n");
881
882    // === OnceLock for container FPropertyHandles ===
883    let ue_name_len = ue_name.len();
884    let ue_name_byte_lit = format!("b\"{}\\0\"", ue_name);
885
886    out.push_str(&format!(
887        "        const FN_ID: u32 = {func_id};\n\
888         \x20       static CPROPS: std::sync::OnceLock<[rusteal_core::FPropertyHandle; {n_containers}]> = std::sync::OnceLock::new();\n\
889         \x20       let __cprops = CPROPS.get_or_init(|| unsafe {{\n\
890         \x20           let __ufunc = ((*rusteal_core::api().reflection).find_function_by_class)(\n\
891         \x20               {class_name}::static_class(),\n\
892         \x20               {ue_name_byte_lit}.as_ptr(), {ue_name_len});\n\
893         \x20           [\n"
894    ));
895    for cp in &container_params {
896        let param_name = &cp.param.name;
897        let param_name_len = param_name.len();
898        let param_byte_lit = format!("b\"{}\\0\"", param_name);
899        out.push_str(&format!(
900            "                ((*rusteal_core::api().reflection).get_function_param)(\n\
901             \x20                   __ufunc, {param_byte_lit}.as_ptr(), {param_name_len}),\n"
902        ));
903    }
904    out.push_str(
905        "            ]\n\
906         \x20       });\n"
907    );
908
909    // === FFI type signature ===
910    let mut ffi_params = String::new();
911    if !is_static {
912        ffi_params.push_str("rusteal_core::UObjectHandle, ");
913    }
914    for param in &func.params {
915        let dir = type_map::param_direction(param);
916        if is_container_param(param) {
917            ffi_params.push_str("*mut u8, *mut u8, "); // base, prop
918        } else {
919            let mapped = map_param(param);
920            match dir {
921                ParamDirection::In | ParamDirection::InOut => {
922                    match mapped.rust_to_ffi {
923                        ConversionKind::StringUtf8 => {
924                            ffi_params.push_str("*const u8, u32, ");
925                            if dir == ParamDirection::InOut {
926                                ffi_params.push_str("*mut u8, u32, *mut u32, ");
927                            }
928                        }
929                        ConversionKind::ObjectRef => ffi_params.push_str("rusteal_core::UObjectHandle, "),
930                        ConversionKind::EnumCast => ffi_params.push_str(&format!("{}, ", mapped.rust_ffi_type)),
931                        ConversionKind::StructOpaque => {
932                            if dir == ParamDirection::InOut {
933                                ffi_params.push_str("*mut u8, ");
934                            } else {
935                                ffi_params.push_str("*const u8, ");
936                            }
937                        }
938                        _ => ffi_params.push_str(&format!("{}, ", mapped.rust_ffi_type)),
939                    }
940                }
941                ParamDirection::Out | ParamDirection::Return => {
942                    match mapped.ffi_to_rust {
943                        ConversionKind::StringUtf8 => ffi_params.push_str("*mut u8, u32, *mut u32, "),
944                        ConversionKind::ObjectRef => ffi_params.push_str("*mut rusteal_core::UObjectHandle, "),
945                        ConversionKind::StructOpaque => ffi_params.push_str("*mut u8, "),
946                        ConversionKind::EnumCast => ffi_params.push_str(&format!("*mut {}, ", mapped.rust_ffi_type)),
947                        _ => ffi_params.push_str(&format!("*mut {}, ", mapped.rust_ffi_type)),
948                    }
949                }
950            }
951        }
952    }
953    if ffi_params.ends_with(", ") {
954        ffi_params.truncate(ffi_params.len() - 2);
955    }
956
957    out.push_str(&format!(
958        "        type Fn = unsafe extern \"C\" fn({ffi_params}) -> rusteal_core::RustealErrorCode;\n\
959         \x20       let __rusteal_fn: Fn = unsafe {{ std::mem::transmute(*(rusteal_core::api().func_table.add(FN_ID as usize))) }};\n"
960    ));
961
962    // === Get handle (pre-validated via ValidHandle) ===
963    if !is_static {
964        out.push_str("        let h = self.handle();\n");
965    }
966
967    // === Alloc temps for all container params ===
968    for cp in &container_params {
969        let idx = cp.index;
970        out.push_str(&format!(
971            "        let __temp_{idx} = unsafe {{ ((*rusteal_core::api().container).alloc_temp)(__cprops[{idx}]) }};\n"
972        ));
973    }
974
975    // === Populate input containers ===
976    for cp in &container_params {
977        if cp.dir != ParamDirection::In && cp.dir != ParamDirection::InOut {
978            continue;
979        }
980        let idx = cp.index;
981        let pname = escape_reserved(&to_snake_case(&cp.param.name));
982        emit_container_populate(out, cp.param, idx, &pname, ctx);
983    }
984
985    // === Declare scalar output variables ===
986    let ret_mapped = scalar_return_mapped.as_ref();
987    if let Some(rm) = ret_mapped {
988        match rm.ffi_to_rust {
989            ConversionKind::ObjectRef => {
990                out.push_str("        let mut __scalar_ret = rusteal_core::UObjectHandle::null();\n");
991            }
992            ConversionKind::StringUtf8 => {
993                out.push_str("        let mut __scalar_ret_buf = vec![0u8; 512];\n");
994                out.push_str("        let mut __scalar_ret_len: u32 = 0;\n");
995            }
996            ConversionKind::EnumCast => {
997                out.push_str(&format!("        let mut __scalar_ret: {} = 0;\n", rm.rust_ffi_type));
998            }
999            ConversionKind::StructOpaque => {
1000                out.push_str("        let mut __scalar_ret_buf = vec![0u8; 256];\n");
1001            }
1002            _ => {
1003                let default = properties::default_value_for(&rm.rust_ffi_type);
1004                out.push_str(&format!("        let mut __scalar_ret = {default};\n"));
1005            }
1006        }
1007    }
1008
1009    // Scalar Out params (non-container)
1010    for param in &func.params {
1011        let dir = type_map::param_direction(param);
1012        if dir == ParamDirection::Out && !is_container_param(param) {
1013            let mapped = map_param(param);
1014            param_helpers::emit_out_param_var_decl(out, param, &mapped);
1015        }
1016        if dir == ParamDirection::InOut && !is_container_param(param) {
1017            let mapped = map_param(param);
1018            param_helpers::emit_inout_string_buf_decl(out, param, &mapped);
1019        }
1020    }
1021
1022    // === FFI call (deferred error check) ===
1023    out.push_str("        let __result = unsafe { __rusteal_fn(");
1024    if !is_static {
1025        out.push_str("h, ");
1026    }
1027    for param in &func.params {
1028        let dir = type_map::param_direction(param);
1029        if is_container_param(param) {
1030            let cp = container_params.iter().find(|c| std::ptr::eq(c.param, param))
1031                .expect("container param must have matching metadata");
1032            let idx = cp.index;
1033            out.push_str(&format!("__temp_{idx}, __cprops[{idx}].0 as *mut u8, "));
1034        } else {
1035            let pname = escape_reserved(&to_snake_case(&param.name));
1036            let mapped = map_param(param);
1037            match dir {
1038                ParamDirection::In | ParamDirection::InOut => {
1039                    match mapped.rust_to_ffi {
1040                        ConversionKind::StringUtf8 => {
1041                            out.push_str(&format!("{pname}.as_ptr(), {pname}.len() as u32, "));
1042                            if dir == ParamDirection::InOut {
1043                                out.push_str(&format!("{pname}_buf.as_mut_ptr(), {pname}_buf.len() as u32, &mut {pname}_len, "));
1044                            }
1045                        }
1046                        ConversionKind::ObjectRef => {
1047                            out.push_str(&format!("rusteal_core::ObjectPointer::object_handle(&{pname}), "));
1048                        }
1049                        ConversionKind::EnumCast => {
1050                            out.push_str(&format!("{pname} as {}, ", mapped.rust_ffi_type));
1051                        }
1052                        ConversionKind::StructOpaque if dir == ParamDirection::In
1053                            && is_struct_owned(param.struct_name.as_deref(), ctx) =>
1054                        {
1055                            out.push_str(&format!("{pname}.as_bytes().as_ptr(), "));
1056                        }
1057                        _ => {
1058                            out.push_str(&format!("{pname}, "));
1059                        }
1060                    }
1061                }
1062                ParamDirection::Out => {
1063                    match mapped.ffi_to_rust {
1064                        ConversionKind::StructOpaque => {
1065                            out.push_str(&format!("{pname}_buf.as_mut_ptr(), "));
1066                        }
1067                        ConversionKind::StringUtf8 => {
1068                            out.push_str(&format!("{pname}_buf.as_mut_ptr(), {pname}_buf.len() as u32, &mut {pname}_len, "));
1069                        }
1070                        _ => {
1071                            out.push_str(&format!("&mut {pname}, "));
1072                        }
1073                    }
1074                }
1075                ParamDirection::Return => {
1076                    let rm = ret_mapped.expect("return param must have mapped type");
1077                    match rm.ffi_to_rust {
1078                        ConversionKind::StringUtf8 => {
1079                            out.push_str("__scalar_ret_buf.as_mut_ptr(), __scalar_ret_buf.len() as u32, &mut __scalar_ret_len, ");
1080                        }
1081                        ConversionKind::ObjectRef => {
1082                            out.push_str("&mut __scalar_ret, ");
1083                        }
1084                        ConversionKind::StructOpaque => {
1085                            out.push_str("__scalar_ret_buf.as_mut_ptr(), ");
1086                        }
1087                        _ => {
1088                            out.push_str("&mut __scalar_ret, ");
1089                        }
1090                    }
1091                }
1092            }
1093        }
1094    }
1095    // Remove trailing comma+space
1096    let out_len = out.len();
1097    if out.ends_with(", ") {
1098        out.truncate(out_len - 2);
1099    }
1100    out.push_str(") };\n");
1101
1102    // === Read output containers (only on success) ===
1103    for cp in &container_params {
1104        if cp.dir != ParamDirection::Out && cp.dir != ParamDirection::Return && cp.dir != ParamDirection::InOut {
1105            continue;
1106        }
1107        let idx = cp.index;
1108        emit_container_read(out, cp.param, idx, ctx);
1109    }
1110
1111    // === Free ALL temps ===
1112    out.push_str("        unsafe {\n");
1113    for cp in &container_params {
1114        let idx = cp.index;
1115        out.push_str(&format!(
1116            "            ((*rusteal_core::api().container).free_temp)(__cprops[{idx}], __temp_{idx});\n"
1117        ));
1118    }
1119    out.push_str("        }\n");
1120
1121    // === Assert success (infallible after pre-validation) ===
1122    out.push_str("        rusteal_core::ffi_infallible(__result);\n");
1123
1124    // === Return ===
1125    emit_container_return(out, return_param, ret_mapped, &container_params, &func.params, ctx);
1126
1127    out.push_str("        }\n");
1128    out.push_str("    }\n\n");
1129}
1130
1131/// Emit code to populate an input container from a Rust slice.
1132fn emit_container_populate(out: &mut String, param: &ParamInfo, idx: usize, pname: &str, ctx: &CodegenContext) {
1133    let elem_type = container_elem_type_str(param, ctx)
1134        .expect("container element type must be resolvable");
1135    match param.prop_type.as_str() {
1136        "ArrayProperty" => {
1137            out.push_str(&format!(
1138                "        {{\n\
1139                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1140                 \x20           let __arr = rusteal_core::UeArray::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1141                 \x20           for __elem in {pname} {{\n\
1142                 \x20               let _ = __arr.push(__elem);\n\
1143                 \x20           }}\n\
1144                 \x20       }}\n"
1145            ));
1146        }
1147        "SetProperty" => {
1148            out.push_str(&format!(
1149                "        {{\n\
1150                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1151                 \x20           let __set = rusteal_core::UeSet::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1152                 \x20           for __elem in {pname} {{\n\
1153                 \x20               let _ = __set.add(__elem);\n\
1154                 \x20           }}\n\
1155                 \x20       }}\n"
1156            ));
1157        }
1158        "MapProperty" => {
1159            out.push_str(&format!(
1160                "        {{\n\
1161                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1162                 \x20           let __map = rusteal_core::UeMap::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1163                 \x20           for (__k, __v) in {pname} {{\n\
1164                 \x20               let _ = __map.add(__k, __v);\n\
1165                 \x20           }}\n\
1166                 \x20       }}\n"
1167            ));
1168        }
1169        _ => {}
1170    }
1171}
1172
1173/// Emit code to read an output container into a Vec.
1174fn emit_container_read(out: &mut String, param: &ParamInfo, idx: usize, ctx: &CodegenContext) {
1175    let elem_type = container_elem_type_str(param, ctx)
1176        .expect("container element type must be resolvable");
1177    match param.prop_type.as_str() {
1178        "ArrayProperty" => {
1179            out.push_str(&format!(
1180                "        let __out_{idx} = if __result == rusteal_core::RustealErrorCode::Ok {{\n\
1181                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1182                 \x20           let __arr = rusteal_core::UeArray::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1183                 \x20           let __len = __arr.len().unwrap_or(0);\n\
1184                 \x20           let mut __v = Vec::with_capacity(__len);\n\
1185                 \x20           for __i in 0..__len {{\n\
1186                 \x20               if let Ok(__val) = __arr.get(__i) {{ __v.push(__val); }}\n\
1187                 \x20           }}\n\
1188                 \x20           __v\n\
1189                 \x20       }} else {{ Vec::new() }};\n"
1190            ));
1191        }
1192        "SetProperty" => {
1193            out.push_str(&format!(
1194                "        let __out_{idx} = if __result == rusteal_core::RustealErrorCode::Ok {{\n\
1195                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1196                 \x20           let __set = rusteal_core::UeSet::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1197                 \x20           let __len = __set.len().unwrap_or(0);\n\
1198                 \x20           let mut __v = Vec::with_capacity(__len);\n\
1199                 \x20           for __i in 0..__len {{\n\
1200                 \x20               if let Ok(__val) = __set.get_element(__i) {{ __v.push(__val); }}\n\
1201                 \x20           }}\n\
1202                 \x20           __v\n\
1203                 \x20       }} else {{ Vec::new() }};\n"
1204            ));
1205        }
1206        "MapProperty" => {
1207            out.push_str(&format!(
1208                "        let __out_{idx} = if __result == rusteal_core::RustealErrorCode::Ok {{\n\
1209                 \x20           let __h = rusteal_core::UObjectHandle(__temp_{idx} as *mut std::ffi::c_void);\n\
1210                 \x20           let __map = rusteal_core::UeMap::<{elem_type}>::new(__h, __cprops[{idx}]);\n\
1211                 \x20           let __len = __map.len().unwrap_or(0);\n\
1212                 \x20           let mut __v = Vec::with_capacity(__len);\n\
1213                 \x20           for __i in 0..__len {{\n\
1214                 \x20               if let Ok(__pair) = __map.get_pair(__i) {{ __v.push(__pair); }}\n\
1215                 \x20           }}\n\
1216                 \x20           __v\n\
1217                 \x20       }} else {{ Vec::new() }};\n"
1218            ));
1219        }
1220        _ => {}
1221    }
1222}
1223
1224/// Emit the final return expression, assembling scalar returns and container outputs (infallible).
1225fn emit_container_return(
1226    out: &mut String,
1227    return_param: Option<&ParamInfo>,
1228    ret_mapped: Option<&MappedType>,
1229    container_params: &[ContainerParamMeta],
1230    func_params: &[ParamInfo],
1231    ctx: &CodegenContext,
1232) {
1233    let mut return_parts = Vec::new();
1234
1235    // Scalar or container return value
1236    if let Some(rp) = return_param {
1237        if is_container_param(rp) {
1238            let cp = container_params.iter().find(|c| c.dir == ParamDirection::Return)
1239                .expect("container return param must exist");
1240            return_parts.push(format!("__out_{}", cp.index));
1241        } else if let Some(rm) = ret_mapped {
1242            match rm.ffi_to_rust {
1243                ConversionKind::ObjectRef => {
1244                    return_parts.push("unsafe { rusteal_core::ObjectPointer::from_object_handle(__scalar_ret) }".to_string());
1245                }
1246                ConversionKind::StringUtf8 => {
1247                    out.push_str("        __scalar_ret_buf.truncate(__scalar_ret_len as usize);\n");
1248                    out.push_str("        let __scalar_str = String::from_utf8_lossy(&__scalar_ret_buf).into_owned();\n");
1249                    return_parts.push("__scalar_str".to_string());
1250                }
1251                ConversionKind::EnumCast => {
1252                    let rt = &rm.rust_type;
1253                    let rp_ref = return_param.expect("return_param must be Some in return conversion");
1254                    let actual_repr = rp_ref.enum_name.as_deref()
1255                        .and_then(|en| ctx.enum_actual_repr(en))
1256                        .unwrap_or(&rm.rust_ffi_type);
1257                    out.push_str(&format!(
1258                        "        let __scalar_enum = {rt}::from_value(__scalar_ret as {actual_repr}).expect(\"unknown enum value\");\n"
1259                    ));
1260                    return_parts.push("__scalar_enum".to_string());
1261                }
1262                ConversionKind::StructOpaque => {
1263                    let rp_ref = return_param.expect("return_param must be Some in return conversion");
1264                    if is_struct_owned(rp_ref.struct_name.as_deref(), ctx) {
1265                        out.push_str("        let __scalar_owned = rusteal_core::OwnedStruct::from_bytes(__scalar_ret_buf);\n");
1266                        return_parts.push("__scalar_owned".to_string());
1267                    } else {
1268                        out.push_str("        let __scalar_ptr = __scalar_ret_buf.as_ptr();\n");
1269                        out.push_str("        std::mem::forget(__scalar_ret_buf);\n");
1270                        return_parts.push("__scalar_ptr".to_string());
1271                    }
1272                }
1273                _ => {
1274                    return_parts.push("__scalar_ret".to_string());
1275                }
1276            }
1277        }
1278    }
1279
1280    // Out/InOut params in original parameter order (must match return type construction)
1281    for param in func_params {
1282        let dir = type_map::param_direction(param);
1283        if dir != ParamDirection::Out && dir != ParamDirection::InOut {
1284            continue;
1285        }
1286
1287        if is_container_param(param) {
1288            let cp = container_params.iter().find(|c| std::ptr::eq(c.param, param))
1289                .expect("container param must have matching metadata");
1290            return_parts.push(format!("__out_{}", cp.index));
1291        } else {
1292            let mapped = map_param(param);
1293            if !is_scalar_output_returnable(dir, &mapped) {
1294                continue;
1295            }
1296            return_parts.push(param_helpers::emit_out_param_conversion(
1297                out, param, &mapped, ctx,
1298            ));
1299        }
1300    }
1301
1302    param_helpers::emit_return_expr(out, &return_parts);
1303}
1304
1305/// Map a ParamInfo to its MappedType (convenience helper).
1306pub(super) fn map_param(param: &ParamInfo) -> MappedType {
1307    type_map::map_property_type(
1308        &param.prop_type,
1309        param.class_name.as_deref(),
1310        param.struct_name.as_deref(),
1311        param.enum_name.as_deref(),
1312        param.enum_underlying_type.as_deref(),
1313        param.meta_class_name.as_deref(),
1314        param.interface_name.as_deref(),
1315    )
1316}