Skip to main content

uika_codegen/rust_gen/
properties.rs

1// Shared property codegen for both classes and structs.
2//
3// Property generation (getters/setters via the reflection PropertyApi) is
4// identical for UClass and UScriptStruct except for:
5// - The reflection lookup function (find_property vs find_struct_property)
6// - The handle expression (static_class() vs static_struct())
7// - The handle access (classes do self.handle(), structs don't)
8// - The container expression passed to the PropertyApi (h vs self.as_ptr())
9//
10// `PropertyContext` captures these differences so all the codegen helpers
11// can be shared.
12
13use crate::context::CodegenContext;
14use crate::naming::{strip_bool_prefix, to_snake_case};
15use crate::schema::PropertyInfo;
16use crate::type_map::{self, ConversionKind, MappedType};
17
18/// Context that parameterizes property codegen for classes vs structs.
19pub struct PropertyContext {
20    /// Reflection API function name: "find_property" or "find_struct_property".
21    pub find_prop_fn: String,
22    /// Expression to get the type handle for property lookup.
23    /// e.g., "Actor::static_class()" or "FVector::static_struct()".
24    pub handle_expr: String,
25    /// Validity check statement (including semicolon). Empty string for structs.
26    /// e.g., "let h = self.handle();"
27    pub pre_access: String,
28    /// Container expression for property API calls: "h" or "self.as_ptr()".
29    pub container_expr: String,
30    /// Whether this is a UClass context (true) or struct context (false).
31    /// Container properties are only valid in class contexts.
32    pub is_class: bool,
33}
34
35/// Collect supported, deduplicated properties, returning their getter name set and the property list.
36/// Properties referencing types not in the context (e.g., enums/classes from non-enabled modules)
37/// are filtered out.
38pub fn collect_deduped_properties<'a>(
39    props: &'a [PropertyInfo],
40    ctx: Option<&CodegenContext>,
41) -> (std::collections::HashSet<String>, Vec<&'a PropertyInfo>) {
42    let mut prop_names = std::collections::HashSet::new();
43    let mut deduped = Vec::new();
44
45    for prop in props {
46        // Skip BlueprintGetter/Setter properties (accessed via function calls)
47        if prop.getter.is_some() || prop.setter.is_some() {
48            continue;
49        }
50
51        let mapped = type_map::map_property_type(
52            &prop.prop_type,
53            prop.class_name.as_deref(),
54            prop.struct_name.as_deref(),
55            prop.enum_name.as_deref(),
56            prop.enum_underlying_type.as_deref(),
57            prop.meta_class_name.as_deref(),
58            prop.interface_name.as_deref(),
59        );
60        if !mapped.supported {
61            continue;
62        }
63
64        // ObjectRef: check that the referenced class/interface is available
65        if matches!(mapped.rust_to_ffi, ConversionKind::ObjectRef) {
66            if let Some(ctx) = ctx {
67                let type_available = match prop.prop_type.as_str() {
68                    "ClassProperty" => {
69                        let effective = prop.meta_class_name.as_deref().or(prop.class_name.as_deref());
70                        effective.map_or(true, |c| ctx.classes.contains_key(c))
71                    }
72                    "InterfaceProperty" => {
73                        prop.interface_name.as_deref().map_or(false, |i| ctx.classes.contains_key(i))
74                    }
75                    _ => prop.class_name.as_deref().map_or(true, |c| ctx.classes.contains_key(c)),
76                };
77                if !type_available {
78                    continue;
79                }
80            }
81        }
82
83        // StructOpaque: only allow if the struct is in enabled modules with static_struct
84        if matches!(mapped.rust_to_ffi, ConversionKind::StructOpaque) {
85            let valid = if let Some(ctx) = ctx {
86                prop.struct_name.as_deref().map_or(false, |sn| {
87                    ctx.structs.get(sn).map_or(false, |si| si.has_static_struct)
88                })
89            } else {
90                prop.struct_name.is_some()
91            };
92            if !valid {
93                continue;
94            }
95        }
96
97        // Container types: skip if inner types can't be resolved
98        if matches!(
99            mapped.rust_to_ffi,
100            ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
101        ) {
102            if type_map::resolve_container_rust_type(prop, ctx).is_none() {
103                continue;
104            }
105            // Container properties only work on UClass contexts (need UObject owner),
106            // not on struct contexts. `ctx` is None only for non-class callers.
107            // More precisely: skip containers in struct property contexts.
108        }
109
110        // Skip properties that reference types not in enabled modules
111        if let Some(ctx) = ctx {
112            match mapped.rust_to_ffi {
113                ConversionKind::EnumCast => {
114                    if let Some(en) = &prop.enum_name {
115                        if !ctx.enums.contains_key(en.as_str()) {
116                            continue;
117                        }
118                    }
119                }
120                ConversionKind::ObjectRef => {
121                    if let Some(cn) = &prop.class_name {
122                        if !ctx.classes.contains_key(cn.as_str()) {
123                            continue;
124                        }
125                    }
126                }
127                _ => {}
128            }
129        }
130
131        let rust_name = if prop.prop_type == "BoolProperty" {
132            strip_bool_prefix(&prop.name)
133        } else {
134            to_snake_case(&prop.name)
135        };
136
137        // Container and delegate properties use bare name (no get_/set_ prefix)
138        let is_container = matches!(
139            mapped.rust_to_ffi,
140            ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
141        );
142        let is_delegate = matches!(
143            mapped.rust_to_ffi,
144            ConversionKind::Delegate | ConversionKind::MulticastDelegate
145        );
146        let getter_name = if is_container || is_delegate {
147            rust_name.clone()
148        } else {
149            format!("get_{rust_name}")
150        };
151        if prop_names.contains(&getter_name) {
152            continue;
153        }
154        prop_names.insert(getter_name);
155        if !is_container && !is_delegate {
156            prop_names.insert(format!("set_{rust_name}"));
157        }
158        deduped.push(prop);
159    }
160
161    (prop_names, deduped)
162}
163
164/// Generate a getter and setter for a single property (used as default impls in Ext trait).
165///
166/// Setters whose name appears in `suppress_setters` are skipped.
167pub fn generate_property(
168    out: &mut String,
169    prop: &PropertyInfo,
170    pctx: &PropertyContext,
171    ctx: &CodegenContext,
172    suppress_setters: &std::collections::HashSet<String>,
173) {
174    let mapped = type_map::map_property_type(
175        &prop.prop_type,
176        prop.class_name.as_deref(),
177        prop.struct_name.as_deref(),
178        prop.enum_name.as_deref(),
179        prop.enum_underlying_type.as_deref(),
180        prop.meta_class_name.as_deref(),
181        prop.interface_name.as_deref(),
182    );
183    if !mapped.supported {
184        return;
185    }
186
187    // Delegate properties: handled by delegates.rs, skip here.
188    if matches!(
189        mapped.rust_to_ffi,
190        ConversionKind::Delegate | ConversionKind::MulticastDelegate
191    ) {
192        return;
193    }
194
195    let prop_name = &prop.name;
196    let rust_name = if prop.prop_type == "BoolProperty" {
197        strip_bool_prefix(prop_name)
198    } else {
199        to_snake_case(prop_name)
200    };
201    let prop_name_len = prop_name.len();
202    let byte_lit = format!("b\"{}\\0\"", prop_name);
203
204    // Fixed array properties: use indexed access via get_property_at/set_property_at
205    if prop.array_dim > 1 {
206        generate_fixed_array_property(out, prop, &rust_name, &byte_lit, prop_name_len, pctx, ctx, &mapped);
207        return;
208    }
209
210    // Container types: generate handle-returning getter (no setter)
211    // Only valid in class contexts (containers need a UObject owner)
212    if matches!(
213        mapped.rust_to_ffi,
214        ConversionKind::ContainerArray | ConversionKind::ContainerMap | ConversionKind::ContainerSet
215    ) {
216        if pctx.is_class {
217            if let Some(container_type) = type_map::resolve_container_rust_type(prop, Some(ctx)) {
218                generate_container_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &container_type);
219            }
220        }
221        return;
222    }
223
224    // Getter
225    match mapped.rust_to_ffi {
226        ConversionKind::StringUtf8 => {
227            generate_string_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
228        }
229        ConversionKind::StructOpaque => {
230            let struct_cpp = prop.struct_name.as_deref()
231                .and_then(|sn| ctx.structs.get(sn))
232                .map(|si| si.cpp_name.clone())
233                .unwrap_or_else(|| format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown")));
234            generate_struct_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &struct_cpp);
235            let setter_name = format!("set_{rust_name}");
236            if !suppress_setters.contains(&setter_name) {
237                generate_struct_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &struct_cpp);
238            }
239            return;
240        }
241        ConversionKind::ObjectRef => {
242            generate_object_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
243        }
244        ConversionKind::EnumCast => {
245            let actual_repr = prop
246                .enum_name
247                .as_deref()
248                .and_then(|en| ctx.enum_actual_repr(en))
249                .unwrap_or(&mapped.rust_ffi_type);
250            generate_enum_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped, actual_repr);
251        }
252        ConversionKind::FName => {
253            generate_fname_getter(out, &rust_name, &byte_lit, prop_name_len, pctx);
254        }
255        ConversionKind::IntCast => {
256            generate_int_cast_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
257        }
258        _ => {
259            generate_primitive_getter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
260        }
261    }
262
263    // Setter (suppressed when a UFUNCTION with the same name takes priority)
264    let setter_name = format!("set_{rust_name}");
265    if suppress_setters.contains(&setter_name) {
266        return;
267    }
268    match mapped.rust_to_ffi {
269        ConversionKind::StringUtf8 => {
270            generate_string_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
271        }
272        ConversionKind::StructOpaque => { /* handled in getter branch with early return */ }
273        ConversionKind::ObjectRef => {
274            generate_object_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
275        }
276        ConversionKind::EnumCast => {
277            generate_enum_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
278        }
279        ConversionKind::FName => {
280            generate_fname_setter(out, &rust_name, &byte_lit, prop_name_len, pctx);
281        }
282        ConversionKind::IntCast => {
283            generate_int_cast_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
284        }
285        _ => {
286            generate_primitive_setter(out, &rust_name, &byte_lit, prop_name_len, pctx, &mapped);
287        }
288    }
289}
290
291/// Get a default value literal for a Rust type.
292pub fn default_value_for(rust_type: &str) -> &'static str {
293    match rust_type {
294        "bool" => "false",
295        "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" => "0",
296        "f32" => "0.0f32",
297        "f64" => "0.0f64",
298        _ => "Default::default()",
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Prop lookup boilerplate
304// ---------------------------------------------------------------------------
305
306/// Generates the property OnceLock lookup. Returns after the closing `});`.
307fn emit_prop_lookup(out: &mut String, byte_lit: &str, prop_name_len: usize, pctx: &PropertyContext) {
308    let find = &pctx.find_prop_fn;
309    let handle = &pctx.handle_expr;
310    out.push_str(&format!(
311        "        static PROP: std::sync::OnceLock<uika_runtime::FPropertyHandle> = std::sync::OnceLock::new();\n\
312         \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
313         \x20           ((*uika_runtime::api().reflection).{find})(\n\
314         \x20               {handle}, {byte_lit}.as_ptr(), {prop_name_len}\n\
315         \x20           )\n\
316         \x20       }});\n"
317    ));
318}
319
320/// Emit pre-access (validity check) if needed.
321fn emit_pre_access(out: &mut String, pctx: &PropertyContext) {
322    if !pctx.pre_access.is_empty() {
323        out.push_str(&format!("        {}\n", pctx.pre_access));
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Getters
329// ---------------------------------------------------------------------------
330
331fn generate_primitive_getter(
332    out: &mut String,
333    rust_name: &str,
334    byte_lit: &str,
335    prop_name_len: usize,
336    pctx: &PropertyContext,
337    mapped: &MappedType,
338) {
339    let rust_type = &mapped.rust_type;
340    let getter = &mapped.property_getter;
341    let default = default_value_for(rust_type);
342    let c = &pctx.container_expr;
343
344    out.push_str(&format!(
345        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
346    ));
347    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
348    emit_pre_access(out, pctx);
349    out.push_str(&format!(
350        "        let mut out = {default};\n\
351         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).{getter})({c}, prop, &mut out) }}, \"{rust_name}\");\n\
352         \x20       out\n\
353         \x20   }}\n\n"
354    ));
355}
356
357fn generate_int_cast_getter(
358    out: &mut String,
359    rust_name: &str,
360    byte_lit: &str,
361    prop_name_len: usize,
362    pctx: &PropertyContext,
363    mapped: &MappedType,
364) {
365    let rust_type = &mapped.rust_type;
366    let ffi_type = &mapped.rust_ffi_type;
367    let getter = &mapped.property_getter;
368    let default = default_value_for(ffi_type);
369    let c = &pctx.container_expr;
370
371    out.push_str(&format!(
372        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
373    ));
374    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
375    emit_pre_access(out, pctx);
376    out.push_str(&format!(
377        "        let mut out = {default};\n\
378         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).{getter})({c}, prop, &mut out) }}, \"{rust_name}\");\n\
379         \x20       out as {rust_type}\n\
380         \x20   }}\n\n"
381    ));
382}
383
384fn generate_string_getter(
385    out: &mut String,
386    rust_name: &str,
387    byte_lit: &str,
388    prop_name_len: usize,
389    pctx: &PropertyContext,
390) {
391    let c = &pctx.container_expr;
392
393    out.push_str(&format!(
394        "    fn get_{rust_name}(&self) -> String {{\n"
395    ));
396    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
397    emit_pre_access(out, pctx);
398    out.push_str(&format!(
399        "        let mut buf = vec![0u8; 512];\n\
400         \x20       let mut out_len: u32 = 0;\n\
401         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{\n\
402         \x20           ((*uika_runtime::api().property).get_string)({c}, prop, buf.as_mut_ptr(), buf.len() as u32, &mut out_len)\n\
403         \x20       }}, \"{rust_name}\");\n\
404         \x20       buf.truncate(out_len as usize);\n\
405         \x20       String::from_utf8_lossy(&buf).into_owned()\n\
406         \x20   }}\n\n"
407    ));
408}
409
410fn generate_object_getter(
411    out: &mut String,
412    rust_name: &str,
413    byte_lit: &str,
414    prop_name_len: usize,
415    pctx: &PropertyContext,
416    mapped: &MappedType,
417) {
418    let rust_type = &mapped.rust_type;
419    let c = &pctx.container_expr;
420
421    out.push_str(&format!(
422        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
423    ));
424    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
425    emit_pre_access(out, pctx);
426    out.push_str(&format!(
427        "        let mut raw = uika_runtime::UObjectHandle(std::ptr::null_mut());\n\
428         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).get_object)({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
429         \x20       unsafe {{ uika_runtime::UObjectRef::from_raw(raw) }}\n\
430         \x20   }}\n\n"
431    ));
432}
433
434fn generate_enum_getter(
435    out: &mut String,
436    rust_name: &str,
437    byte_lit: &str,
438    prop_name_len: usize,
439    pctx: &PropertyContext,
440    mapped: &MappedType,
441    actual_repr: &str,
442) {
443    let rust_type = &mapped.rust_type;
444    let c = &pctx.container_expr;
445
446    out.push_str(&format!(
447        "    fn get_{rust_name}(&self) -> {rust_type} {{\n"
448    ));
449    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
450    emit_pre_access(out, pctx);
451    out.push_str(&format!(
452        "        let mut raw: i64 = 0;\n\
453         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).get_enum)({c}, prop, &mut raw) }}, \"{rust_name}\");\n\
454         \x20       {rust_type}::from_value(raw as {actual_repr}).expect(\"unknown enum value\")\n\
455         \x20   }}\n\n"
456    ));
457}
458
459fn generate_fname_getter(
460    out: &mut String,
461    rust_name: &str,
462    byte_lit: &str,
463    prop_name_len: usize,
464    pctx: &PropertyContext,
465) {
466    let c = &pctx.container_expr;
467
468    out.push_str(&format!(
469        "    fn get_{rust_name}(&self) -> uika_runtime::FNameHandle {{\n"
470    ));
471    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
472    emit_pre_access(out, pctx);
473    out.push_str(&format!(
474        "        let mut out = uika_runtime::FNameHandle(0);\n\
475         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).get_fname)({c}, prop, &mut out) }}, \"{rust_name}\");\n\
476         \x20       out\n\
477         \x20   }}\n\n"
478    ));
479}
480
481// ---------------------------------------------------------------------------
482// Setters
483// ---------------------------------------------------------------------------
484
485fn generate_primitive_setter(
486    out: &mut String,
487    rust_name: &str,
488    byte_lit: &str,
489    prop_name_len: usize,
490    pctx: &PropertyContext,
491    mapped: &MappedType,
492) {
493    let rust_type = &mapped.rust_type;
494    let setter = &mapped.property_setter;
495    let c = &pctx.container_expr;
496
497    out.push_str(&format!(
498        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
499    ));
500    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
501    emit_pre_access(out, pctx);
502    out.push_str(&format!(
503        "        uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).{setter})({c}, prop, val) }}, \"{rust_name}\");\n\
504         \x20   }}\n\n"
505    ));
506}
507
508fn generate_int_cast_setter(
509    out: &mut String,
510    rust_name: &str,
511    byte_lit: &str,
512    prop_name_len: usize,
513    pctx: &PropertyContext,
514    mapped: &MappedType,
515) {
516    let rust_type = &mapped.rust_type;
517    let ffi_type = &mapped.rust_ffi_type;
518    let setter = &mapped.property_setter;
519    let c = &pctx.container_expr;
520
521    out.push_str(&format!(
522        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
523    ));
524    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
525    emit_pre_access(out, pctx);
526    out.push_str(&format!(
527        "        uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).{setter})({c}, prop, val as {ffi_type}) }}, \"{rust_name}\");\n\
528         \x20   }}\n\n"
529    ));
530}
531
532fn generate_string_setter(
533    out: &mut String,
534    rust_name: &str,
535    byte_lit: &str,
536    prop_name_len: usize,
537    pctx: &PropertyContext,
538) {
539    let c = &pctx.container_expr;
540
541    out.push_str(&format!(
542        "    fn set_{rust_name}(&self, val: &str) {{\n"
543    ));
544    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
545    emit_pre_access(out, pctx);
546    out.push_str(&format!(
547        "        uika_runtime::ffi_infallible_ctx(unsafe {{\n\
548         \x20           ((*uika_runtime::api().property).set_string)({c}, prop, val.as_ptr(), val.len() as u32)\n\
549         \x20       }}, \"{rust_name}\");\n\
550         \x20   }}\n\n"
551    ));
552}
553
554fn generate_object_setter(
555    out: &mut String,
556    rust_name: &str,
557    byte_lit: &str,
558    prop_name_len: usize,
559    pctx: &PropertyContext,
560    mapped: &MappedType,
561) {
562    let rust_type = &mapped.rust_type;
563    let c = &pctx.container_expr;
564
565    out.push_str(&format!(
566        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
567    ));
568    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
569    emit_pre_access(out, pctx);
570    out.push_str(&format!(
571        "        uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).set_object)({c}, prop, val.raw()) }}, \"{rust_name}\");\n\
572         \x20   }}\n\n"
573    ));
574}
575
576fn generate_enum_setter(
577    out: &mut String,
578    rust_name: &str,
579    byte_lit: &str,
580    prop_name_len: usize,
581    pctx: &PropertyContext,
582    mapped: &MappedType,
583) {
584    let rust_type = &mapped.rust_type;
585    let c = &pctx.container_expr;
586
587    out.push_str(&format!(
588        "    fn set_{rust_name}(&self, val: {rust_type}) {{\n"
589    ));
590    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
591    emit_pre_access(out, pctx);
592    out.push_str(&format!(
593        "        uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).set_enum)({c}, prop, val as i64) }}, \"{rust_name}\");\n\
594         \x20   }}\n\n"
595    ));
596}
597
598fn generate_fname_setter(
599    out: &mut String,
600    rust_name: &str,
601    byte_lit: &str,
602    prop_name_len: usize,
603    pctx: &PropertyContext,
604) {
605    let c = &pctx.container_expr;
606
607    out.push_str(&format!(
608        "    fn set_{rust_name}(&self, val: uika_runtime::FNameHandle) {{\n"
609    ));
610    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
611    emit_pre_access(out, pctx);
612    out.push_str(&format!(
613        "        uika_runtime::ffi_infallible_ctx(unsafe {{ ((*uika_runtime::api().property).set_fname)({c}, prop, val) }}, \"{rust_name}\");\n\
614         \x20   }}\n\n"
615    ));
616}
617
618// ---------------------------------------------------------------------------
619// Struct getters/setters (OwnedStruct<T>)
620// ---------------------------------------------------------------------------
621
622fn generate_struct_getter(
623    out: &mut String,
624    rust_name: &str,
625    byte_lit: &str,
626    prop_name_len: usize,
627    pctx: &PropertyContext,
628    struct_cpp: &str,
629) {
630    let c = &pctx.container_expr;
631
632    out.push_str(&format!(
633        "    fn get_{rust_name}(&self) -> uika_runtime::OwnedStruct<{struct_cpp}> {{\n"
634    ));
635    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
636    emit_pre_access(out, pctx);
637    out.push_str(&format!(
638        "        let size = unsafe {{ ((*uika_runtime::api().reflection).get_property_size)(prop) }} as usize;\n\
639         \x20       let mut buf = vec![0u8; size];\n\
640         \x20       uika_runtime::ffi_infallible_ctx(unsafe {{\n\
641         \x20           ((*uika_runtime::api().property).get_struct)({c}, prop, buf.as_mut_ptr(), size as u32)\n\
642         \x20       }}, \"{rust_name}\");\n\
643         \x20       uika_runtime::OwnedStruct::from_bytes(buf)\n\
644         \x20   }}\n\n"
645    ));
646}
647
648fn generate_struct_setter(
649    out: &mut String,
650    rust_name: &str,
651    byte_lit: &str,
652    prop_name_len: usize,
653    pctx: &PropertyContext,
654    struct_cpp: &str,
655) {
656    let c = &pctx.container_expr;
657
658    out.push_str(&format!(
659        "    fn set_{rust_name}(&self, val: &uika_runtime::OwnedStruct<{struct_cpp}>) {{\n"
660    ));
661    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
662    emit_pre_access(out, pctx);
663    out.push_str(&format!(
664        "        uika_runtime::ffi_infallible_ctx(unsafe {{\n\
665         \x20           ((*uika_runtime::api().property).set_struct)({c}, prop, val.as_bytes().as_ptr(), val.as_bytes().len() as u32)\n\
666         \x20       }}, \"{rust_name}\");\n\
667         \x20   }}\n\n"
668    ));
669}
670
671// ---------------------------------------------------------------------------
672// Fixed array getter/setter (array_dim > 1)
673// ---------------------------------------------------------------------------
674
675fn generate_fixed_array_property(
676    out: &mut String,
677    prop: &PropertyInfo,
678    rust_name: &str,
679    byte_lit: &str,
680    prop_name_len: usize,
681    pctx: &PropertyContext,
682    ctx: &CodegenContext,
683    mapped: &MappedType,
684) {
685    let array_dim = prop.array_dim;
686    let c = &pctx.container_expr;
687
688    // Determine the Rust return type and conversion for the getter
689    let (getter_ret_type, getter_conversion, setter_param_type, setter_conversion) = match mapped.rust_to_ffi {
690        ConversionKind::Identity if mapped.rust_type == "bool" => (
691            "bool".to_string(),
692            "Ok(buf[0] != 0)".to_string(),
693            "bool".to_string(),
694            format!(
695                "let mut buf = vec![0u8; elem_size];\n\
696                 \x20       if val {{ buf[0] = 1; }}"
697            ),
698        ),
699        ConversionKind::Identity | ConversionKind::IntCast => {
700            let rust_type = &mapped.rust_type;
701            let byte_count = rust_type_byte_size(rust_type);
702            (
703                rust_type.clone(),
704                format!("Ok({rust_type}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap()))"),
705                rust_type.clone(),
706                format!("let buf = val.to_ne_bytes().to_vec();"),
707            )
708        }
709        ConversionKind::ObjectRef => {
710            let rust_type = &mapped.rust_type;
711            (
712                rust_type.clone(),
713                "let handle = uika_runtime::UObjectHandle(usize::from_ne_bytes(buf[..std::mem::size_of::<usize>()].try_into().unwrap()) as *mut std::ffi::c_void);\n\
714                 \x20       Ok(unsafe { uika_runtime::UObjectRef::from_raw(handle) })".to_string(),
715                rust_type.clone(),
716                "let buf = (val.raw().0 as usize).to_ne_bytes().to_vec();".to_string(),
717            )
718        }
719        ConversionKind::EnumCast => {
720            let rust_type = &mapped.rust_type;
721            let actual_repr = prop
722                .enum_name
723                .as_deref()
724                .and_then(|en| ctx.enum_actual_repr(en))
725                .unwrap_or(&mapped.rust_ffi_type);
726            let byte_count = rust_type_byte_size(actual_repr);
727            (
728                rust_type.clone(),
729                format!(
730                    "let raw = {actual_repr}::from_ne_bytes(buf[..{byte_count}].try_into().unwrap());\n\
731                     \x20       {rust_type}::from_value(raw).ok_or(uika_runtime::UikaError::TypeMismatch)"
732                ),
733                rust_type.clone(),
734                format!("let buf = (val as {actual_repr}).to_ne_bytes().to_vec();"),
735            )
736        }
737        ConversionKind::StructOpaque => {
738            let struct_cpp = prop.struct_name.as_deref()
739                .and_then(|sn| ctx.structs.get(sn))
740                .map(|si| si.cpp_name.clone())
741                .unwrap_or_else(|| format!("F{}", prop.struct_name.as_deref().unwrap_or("Unknown")));
742            (
743                format!("uika_runtime::OwnedStruct<{struct_cpp}>"),
744                "Ok(uika_runtime::OwnedStruct::from_bytes(buf))".to_string(),
745                format!("&uika_runtime::OwnedStruct<{struct_cpp}>"),
746                "let buf = val.as_bytes().to_vec();".to_string(),
747            )
748        }
749        _ => return, // Unsupported conversion kind for fixed arrays
750    };
751
752    // Getter
753    out.push_str(&format!(
754        "    fn get_{rust_name}(&self, index: u32) -> uika_runtime::UikaResult<{getter_ret_type}> {{\n"
755    ));
756    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
757    emit_pre_access(out, pctx);
758    out.push_str(&format!(
759        "        if index >= {array_dim} {{ return Err(uika_runtime::UikaError::IndexOutOfRange); }}\n\
760         \x20       let elem_size = unsafe {{ ((*uika_runtime::api().reflection).get_element_size)(prop) }} as usize;\n\
761         \x20       let mut buf = vec![0u8; elem_size];\n\
762         \x20       uika_runtime::check_ffi_ctx(unsafe {{\n\
763         \x20           ((*uika_runtime::api().property).get_property_at)({c}, prop, index, buf.as_mut_ptr(), elem_size as u32)\n\
764         \x20       }}, \"{rust_name}\")?;\n\
765         \x20       {getter_conversion}\n\
766         \x20   }}\n\n"
767    ));
768
769    // Setter
770    out.push_str(&format!(
771        "    fn set_{rust_name}(&self, index: u32, val: {setter_param_type}) -> uika_runtime::UikaResult<()> {{\n"
772    ));
773    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
774    emit_pre_access(out, pctx);
775    out.push_str(&format!(
776        "        if index >= {array_dim} {{ return Err(uika_runtime::UikaError::IndexOutOfRange); }}\n\
777         \x20       let elem_size = unsafe {{ ((*uika_runtime::api().reflection).get_element_size)(prop) }} as usize;\n\
778         \x20       {setter_conversion}\n\
779         \x20       uika_runtime::check_ffi_ctx(unsafe {{\n\
780         \x20           ((*uika_runtime::api().property).set_property_at)({c}, prop, index, buf.as_ptr(), elem_size as u32)\n\
781         \x20       }}, \"{rust_name}\")?;\n\
782         \x20       Ok(())\n\
783         \x20   }}\n\n"
784    ));
785}
786
787/// Get the byte size of a Rust primitive type for from_ne_bytes conversions.
788fn rust_type_byte_size(rust_type: &str) -> usize {
789    match rust_type {
790        "bool" | "i8" | "u8" => 1,
791        "i16" | "u16" => 2,
792        "i32" | "u32" | "f32" => 4,
793        "i64" | "u64" | "f64" => 8,
794        _ => 8, // fallback for pointer-sized types
795    }
796}
797
798// ---------------------------------------------------------------------------
799// Container getter
800// ---------------------------------------------------------------------------
801
802fn generate_container_getter(
803    out: &mut String,
804    rust_name: &str,
805    byte_lit: &str,
806    prop_name_len: usize,
807    pctx: &PropertyContext,
808    container_type: &str,
809) {
810    out.push_str(&format!(
811        "    fn {rust_name}(&self) -> {container_type} {{\n"
812    ));
813    emit_prop_lookup(out, byte_lit, prop_name_len, pctx);
814    emit_pre_access(out, pctx);
815    // Container getter returns a handle (not data), constructed from the owner + prop handles.
816    // Use turbofish syntax (::< >) because the type is in expression position.
817    let turbofish_type = container_type.replace('<', "::<");
818    out.push_str(&format!(
819        "        {turbofish_type}::new(h, prop)\n\
820         \x20   }}\n\n"
821    ));
822}