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