Skip to main content

uika_codegen/rust_gen/
delegates.rs

1// Delegate codegen: generates typed delegate accessor methods and wrapper structs.
2//
3// For each delegate property on a class, we generate:
4// 1. A trait method returning a typed delegate handle struct
5// 2. The delegate handle struct with bind()/add() methods that accept typed closures
6//
7// The handle struct wraps the UObject owner + FPropertyHandle, and the bind/add
8// methods register a closure in the Rust delegate_registry, then call the C++ API.
9
10use crate::context::CodegenContext;
11use crate::naming::to_snake_case;
12use crate::schema::PropertyInfo;
13use crate::type_map::{self, ConversionKind};
14
15/// Information about a delegate property to generate code for.
16pub struct DelegateInfo<'a> {
17    pub prop: &'a PropertyInfo,
18    pub class_name: &'a str,
19    /// Rust name for the accessor method (snake_case).
20    pub rust_name: String,
21    /// Struct name for the delegate wrapper (PascalCase).
22    pub struct_name: String,
23    /// Whether this is a multicast delegate.
24    pub is_multicast: bool,
25    /// Parsed delegate parameters.
26    pub params: Vec<DelegateParam>,
27}
28
29/// A single parameter in a delegate signature.
30pub struct DelegateParam {
31    pub name: String,
32    pub rust_type: String,
33    pub conversion: ParamConversion,
34}
35
36/// How to read a delegate param from the raw params buffer.
37pub enum ParamConversion {
38    /// Primitive: read directly as the type (bool, i32, f32, etc.)
39    Primitive(String),
40    /// Object reference: read UObjectHandle, wrap in UObjectRef<T>.
41    ObjectRef(String),
42    /// Enum: read underlying repr, convert via from_value.
43    Enum { rust_type: String, repr: String },
44    /// FName: read FNameHandle directly.
45    FName,
46    /// String: cannot be read from raw params easily — skip for now.
47    String,
48}
49
50/// Collect delegate properties from a class and resolve their param types.
51pub fn collect_delegate_props<'a>(
52    props: &'a [PropertyInfo],
53    class_name: &'a str,
54    ctx: &CodegenContext,
55) -> Vec<DelegateInfo<'a>> {
56    let mut result = Vec::new();
57
58    for prop in props {
59        let mapped = type_map::map_property_type(
60            &prop.prop_type,
61            prop.class_name.as_deref(),
62            prop.struct_name.as_deref(),
63            prop.enum_name.as_deref(),
64            prop.enum_underlying_type.as_deref(),
65            prop.meta_class_name.as_deref(),
66            prop.interface_name.as_deref(),
67        );
68        if !matches!(
69            mapped.rust_to_ffi,
70            ConversionKind::Delegate | ConversionKind::MulticastDelegate
71        ) {
72            continue;
73        }
74
75        let is_multicast = matches!(mapped.rust_to_ffi, ConversionKind::MulticastDelegate);
76
77        // Parse func_info params
78        let func_info = match &prop.func_info {
79            Some(fi) => fi,
80            None => continue,
81        };
82        let params_json = match func_info.get("params").and_then(|p| p.as_array()) {
83            Some(params) => params,
84            None => &Vec::new() as &Vec<serde_json::Value>,
85        };
86
87        let mut params = Vec::new();
88        let mut all_supported = true;
89
90        for param_value in params_json {
91            let param_name = param_value
92                .get("name")
93                .and_then(|n| n.as_str())
94                .unwrap_or("unknown");
95            let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
96                Some(t) => t,
97                None => {
98                    all_supported = false;
99                    break;
100                }
101            };
102
103            match resolve_delegate_param(param_name, param_type, param_value, ctx) {
104                Some(dp) => params.push(dp),
105                None => {
106                    all_supported = false;
107                    break;
108                }
109            }
110        }
111
112        if !all_supported {
113            continue;
114        }
115
116        let rust_name = to_snake_case(&prop.name);
117        let struct_name = format!("{}{}Delegate", class_name, prop.name);
118
119        result.push(DelegateInfo {
120            prop,
121            class_name,
122            rust_name,
123            struct_name,
124            is_multicast,
125            params,
126        });
127    }
128
129    result
130}
131
132fn resolve_delegate_param(
133    name: &str,
134    prop_type: &str,
135    value: &serde_json::Value,
136    ctx: &CodegenContext,
137) -> Option<DelegateParam> {
138    let param_name = to_snake_case(name);
139
140    match prop_type {
141        "BoolProperty" => Some(DelegateParam {
142            name: param_name,
143            rust_type: "bool".into(),
144            conversion: ParamConversion::Primitive("bool".into()),
145        }),
146        "Int8Property" => Some(DelegateParam {
147            name: param_name,
148            rust_type: "i8".into(),
149            conversion: ParamConversion::Primitive("i8".into()),
150        }),
151        "ByteProperty" => {
152            if let Some(en) = value.get("enum_name").and_then(|v| v.as_str()) {
153                if ctx.enums.contains_key(en) {
154                    Some(DelegateParam {
155                        name: param_name,
156                        rust_type: en.to_string(),
157                        conversion: ParamConversion::Enum {
158                            rust_type: en.to_string(),
159                            repr: ctx
160                                .enum_actual_repr(en)
161                                .unwrap_or("u8")
162                                .to_string(),
163                        },
164                    })
165                } else {
166                    None
167                }
168            } else {
169                Some(DelegateParam {
170                    name: param_name,
171                    rust_type: "u8".into(),
172                    conversion: ParamConversion::Primitive("u8".into()),
173                })
174            }
175        }
176        "Int16Property" => prim_param(param_name, "i16"),
177        "UInt16Property" => prim_param(param_name, "u16"),
178        "IntProperty" => prim_param(param_name, "i32"),
179        "UInt32Property" => prim_param(param_name, "u32"),
180        "Int64Property" => prim_param(param_name, "i64"),
181        "UInt64Property" => prim_param(param_name, "u64"),
182        "FloatProperty" => prim_param(param_name, "f32"),
183        "DoubleProperty" => prim_param(param_name, "f64"),
184        "NameProperty" => Some(DelegateParam {
185            name: param_name,
186            rust_type: "uika_runtime::FNameHandle".into(),
187            conversion: ParamConversion::FName,
188        }),
189        "StrProperty" | "TextProperty" => Some(DelegateParam {
190            name: param_name,
191            rust_type: "String".into(),
192            conversion: ParamConversion::String,
193        }),
194        "ObjectProperty" | "ClassProperty" => {
195            let cls = value.get("class_name").and_then(|v| v.as_str());
196            if let Some(cls) = cls {
197                if ctx.classes.contains_key(cls) {
198                    Some(DelegateParam {
199                        name: param_name,
200                        rust_type: format!("uika_runtime::UObjectRef<{cls}>"),
201                        conversion: ParamConversion::ObjectRef(cls.to_string()),
202                    })
203                } else {
204                    None
205                }
206            } else {
207                Some(DelegateParam {
208                    name: param_name,
209                    rust_type: "uika_runtime::UObjectHandle".into(),
210                    conversion: ParamConversion::Primitive("uika_runtime::UObjectHandle".into()),
211                })
212            }
213        }
214        "EnumProperty" => {
215            let en = value.get("enum_name").and_then(|v| v.as_str())?;
216            if !ctx.enums.contains_key(en) {
217                return None;
218            }
219            Some(DelegateParam {
220                name: param_name,
221                rust_type: en.to_string(),
222                conversion: ParamConversion::Enum {
223                    rust_type: en.to_string(),
224                    repr: ctx.enum_actual_repr(en).unwrap_or("u8").to_string(),
225                },
226            })
227        }
228        "StructProperty" => {
229            // Struct params in delegates require reading from raw memory.
230            // For now, skip struct params — they require special handling.
231            None
232        }
233        _ => None,
234    }
235}
236
237fn prim_param(name: String, ty: &str) -> Option<DelegateParam> {
238    Some(DelegateParam {
239        name,
240        rust_type: ty.into(),
241        conversion: ParamConversion::Primitive(ty.into()),
242    })
243}
244
245// ---------------------------------------------------------------------------
246// Code generation
247// ---------------------------------------------------------------------------
248
249/// Generate trait method implementations for delegate properties (used as default impls).
250pub fn generate_delegate_impls(
251    out: &mut String,
252    delegates: &[DelegateInfo],
253) {
254    for d in delegates {
255        let rust_name = &d.rust_name;
256        let struct_name = &d.struct_name;
257        let class_name = d.class_name;
258        let prop_name = &d.prop.name;
259        let prop_name_len = prop_name.len();
260        let byte_lit = format!("b\"{}\\0\"", prop_name);
261
262        out.push_str(&format!(
263            "    fn {rust_name}(&self) -> {struct_name} {{\n\
264             \x20       static PROP: std::sync::OnceLock<uika_runtime::FPropertyHandle> = std::sync::OnceLock::new();\n\
265             \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
266             \x20           ((*uika_runtime::api().reflection).find_property)(\n\
267             \x20               {class_name}::static_class(), {byte_lit}.as_ptr(), {prop_name_len}\n\
268             \x20           )\n\
269             \x20       }});\n\
270             \x20       {struct_name} {{ owner: self.handle(), prop }}\n\
271             \x20   }}\n\n"
272        ));
273    }
274}
275
276/// Generate delegate wrapper structs with typed bind/add methods.
277/// These are emitted at the top of the class file (before the trait).
278pub fn generate_delegate_structs(
279    out: &mut String,
280    delegates: &[DelegateInfo],
281    class_name: &str,
282) {
283    for d in delegates {
284        let struct_name = &d.struct_name;
285        let is_multicast = d.is_multicast;
286
287        out.push_str(&format!(
288            "pub struct {struct_name} {{\n\
289             \x20   pub owner: uika_runtime::UObjectHandle,\n\
290             \x20   pub prop: uika_runtime::FPropertyHandle,\n\
291             }}\n\n"
292        ));
293
294        // Build the closure parameter types and extraction code
295        let sig_name = d.prop.func_info.as_ref()
296            .and_then(|fi| fi.get("name"))
297            .and_then(|n| n.as_str())
298            .unwrap_or(&d.prop.name);
299        let sig_name_len = sig_name.len();
300        let sig_byte_lit = format!("b\"{}\\0\"", sig_name);
301
302        // Build closure param types for the user-facing closure signature
303        let callback_params: Vec<String> = d.params.iter().map(|p| p.rust_type.clone()).collect();
304        let callback_sig = callback_params.join(", ");
305
306        let method_name = if is_multicast { "add" } else { "bind" };
307        let api_fn = if is_multicast { "bind_multicast" } else { "bind_unicast" };
308
309        out.push_str(&format!(
310            "impl {struct_name} {{\n"
311        ));
312
313        // Generate the bind/add method
314        out.push_str(&format!(
315            "    pub fn {method_name}(&self, mut callback: impl FnMut({callback_sig}) + Send + 'static) -> uika_runtime::UikaResult<uika_runtime::DelegateBinding> {{\n"
316        ));
317
318        // If there are params, we need to resolve offsets via OnceLock
319        if !d.params.is_empty() {
320            let n_params = d.params.len();
321            out.push_str(&format!(
322                "        static OFFSETS: std::sync::OnceLock<[u32; {n_params}]> = std::sync::OnceLock::new();\n\
323                 \x20       let offsets = OFFSETS.get_or_init(|| unsafe {{\n\
324                 \x20           let sig_func = ((*uika_runtime::api().reflection).find_function_by_class)(\n\
325                 \x20               {class_name}::static_class(),\n\
326                 \x20               {sig_byte_lit}.as_ptr(), {sig_name_len});\n\
327                 \x20           [\n"
328            ));
329
330            for p in &d.params {
331                let param_ue_name = &d.prop.func_info.as_ref()
332                    .and_then(|fi| fi.get("params"))
333                    .and_then(|ps| ps.as_array())
334                    .and_then(|arr| arr.iter().find(|v| {
335                        v.get("name").and_then(|n| n.as_str()).map(|n| to_snake_case(n)) == Some(p.name.clone())
336                    }))
337                    .and_then(|v| v.get("name"))
338                    .and_then(|n| n.as_str())
339                    .unwrap_or(&p.name);
340                let pname_len = param_ue_name.len();
341                let pname_lit = format!("b\"{}\\0\"", param_ue_name);
342                out.push_str(&format!(
343                    "                {{\n\
344                     \x20                   let param_prop = ((*uika_runtime::api().reflection).get_function_param)(\n\
345                     \x20                       sig_func, {pname_lit}.as_ptr(), {pname_len});\n\
346                     \x20                   ((*uika_runtime::api().reflection).get_property_offset)(param_prop)\n\
347                     \x20               }},\n"
348                ));
349            }
350
351            out.push_str(
352                "            ]\n\
353                 \x20       });\n\
354                 \x20       #[allow(unused_variables)] let offsets = offsets;\n"
355            );
356        }
357
358        // Build the closure wrapper that extracts typed params from raw *mut u8
359        if d.params.is_empty() {
360            out.push_str(&format!(
361                "        let owner = self.owner;\n\
362                 \x20       let prop = self.prop;\n\
363                 \x20       uika_runtime::delegate_registry::{api_fn}(owner, prop, move |_params: *mut u8| {{\n\
364                 \x20           callback();\n\
365                 \x20       }})\n\
366                 \x20   }}\n"
367            ));
368            out.push_str("}\n\n");
369            continue;
370        }
371
372        // Check if any param actually reads from the raw buffer
373        let needs_unsafe = d.params.iter().any(|p| !matches!(p.conversion, ParamConversion::String));
374        let params_var = if needs_unsafe { "params" } else { "_params" };
375
376        out.push_str(&format!(
377            "        let owner = self.owner;\n\
378             \x20       let prop = self.prop;\n\
379             \x20       uika_runtime::delegate_registry::{api_fn}(owner, prop, move |{params_var}: *mut u8| {{\n"
380        ));
381
382        if needs_unsafe {
383            out.push_str("            unsafe {\n");
384        }
385
386        // Extract each parameter
387        for (i, p) in d.params.iter().enumerate() {
388            let var_name = &p.name;
389            match &p.conversion {
390                ParamConversion::Primitive(ty) => {
391                    out.push_str(&format!(
392                        "                let {var_name} = *(params.add(offsets[{i}] as usize) as *const {ty});\n"
393                    ));
394                }
395                ParamConversion::ObjectRef(_cls) => {
396                    out.push_str(&format!(
397                        "                let {var_name} = uika_runtime::UObjectRef::from_raw(\n\
398                         \x20                   *(params.add(offsets[{i}] as usize) as *const uika_runtime::UObjectHandle)\n\
399                         \x20               );\n"
400                    ));
401                }
402                ParamConversion::Enum { rust_type, repr } => {
403                    // SAFETY: the raw value came from UE and must be a valid repr bit pattern.
404                    // If from_value doesn't recognize it (e.g. unlisted variant), transmute is safe
405                    // because the enum is #[repr(integer)].
406                    out.push_str(&format!(
407                        "                let __raw_{var_name} = *(params.add(offsets[{i}] as usize) as *const {repr});\n\
408                         \x20               let {var_name} = {rust_type}::from_value(__raw_{var_name}).unwrap_or_else(|| std::mem::transmute(__raw_{var_name}));\n"
409                    ));
410                }
411                ParamConversion::FName => {
412                    out.push_str(&format!(
413                        "                let {var_name} = *(params.add(offsets[{i}] as usize) as *const uika_runtime::FNameHandle);\n"
414                    ));
415                }
416                ParamConversion::String => {
417                    // Strings in delegate params are FString in UE memory.
418                    // Reading them requires calling the property API. For now,
419                    // pass an empty string — full support needs ProcessEvent param extraction.
420                    out.push_str(&format!(
421                        "                let {var_name} = String::new(); // TODO: string param extraction\n"
422                    ));
423                }
424            }
425        }
426
427        // Call the user's callback with extracted params
428        let param_names: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
429        let call_args = param_names.join(", ");
430        if needs_unsafe {
431            out.push_str(&format!(
432                "                callback({call_args});\n\
433                 \x20           }}\n\
434                 \x20       }})\n\
435                 \x20   }}\n"
436            ));
437        } else {
438            out.push_str(&format!(
439                "            callback({call_args});\n\
440                 \x20       }})\n\
441                 \x20   }}\n"
442            ));
443        }
444
445        out.push_str("}\n\n");
446    }
447}