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