Skip to main content

rusteal_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: use read_param FFI to properly pack FName (Editor-safe).
45    FName,
46    /// String/FText: use read_param FFI to read UTF-8 string.
47    String,
48    /// Struct: use read_param FFI to read struct bytes, wrap in OwnedStruct<T>.
49    Struct { struct_name: String, cpp_name: String },
50}
51
52/// Collect delegate properties from a class and resolve their param types.
53pub fn collect_delegate_props<'a>(
54    props: &'a [PropertyInfo],
55    class_name: &'a str,
56    ctx: &CodegenContext,
57) -> Vec<DelegateInfo<'a>> {
58    let mut result = Vec::new();
59
60    for prop in props {
61        let mapped = type_map::map_property_type(
62            &prop.prop_type,
63            prop.class_name.as_deref(),
64            prop.struct_name.as_deref(),
65            prop.enum_name.as_deref(),
66            prop.enum_underlying_type.as_deref(),
67            prop.meta_class_name.as_deref(),
68            prop.interface_name.as_deref(),
69        );
70        if !matches!(
71            mapped.rust_to_ffi,
72            ConversionKind::Delegate | ConversionKind::MulticastDelegate
73        ) {
74            continue;
75        }
76
77        let is_multicast = matches!(mapped.rust_to_ffi, ConversionKind::MulticastDelegate);
78
79        // Parse func_info params
80        let func_info = match &prop.func_info {
81            Some(fi) => fi,
82            None => continue,
83        };
84        let params_json = match func_info.get("params").and_then(|p| p.as_array()) {
85            Some(params) => params,
86            None => &Vec::new() as &Vec<serde_json::Value>,
87        };
88
89        let mut params = Vec::new();
90        let mut all_supported = true;
91
92        for param_value in params_json {
93            let param_name = param_value
94                .get("name")
95                .and_then(|n| n.as_str())
96                .unwrap_or("unknown");
97            let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
98                Some(t) => t,
99                None => {
100                    all_supported = false;
101                    break;
102                }
103            };
104
105            match resolve_delegate_param(param_name, param_type, param_value, ctx) {
106                Some(dp) => params.push(dp),
107                None => {
108                    all_supported = false;
109                    break;
110                }
111            }
112        }
113
114        if !all_supported {
115            continue;
116        }
117
118        let rust_name = to_snake_case(&prop.name);
119        let struct_name = format!("{}{}Delegate", class_name, prop.name);
120
121        result.push(DelegateInfo {
122            prop,
123            class_name,
124            rust_name,
125            struct_name,
126            is_multicast,
127            params,
128        });
129    }
130
131    result
132}
133
134fn resolve_delegate_param(
135    name: &str,
136    prop_type: &str,
137    value: &serde_json::Value,
138    ctx: &CodegenContext,
139) -> Option<DelegateParam> {
140    let param_name = to_snake_case(name);
141
142    match prop_type {
143        "BoolProperty" => Some(DelegateParam {
144            name: param_name,
145            rust_type: "bool".into(),
146            conversion: ParamConversion::Primitive("bool".into()),
147        }),
148        "Int8Property" => Some(DelegateParam {
149            name: param_name,
150            rust_type: "i8".into(),
151            conversion: ParamConversion::Primitive("i8".into()),
152        }),
153        "ByteProperty" => {
154            if let Some(en) = value.get("enum_name").and_then(|v| v.as_str()) {
155                if ctx.enums.contains_key(en) {
156                    Some(DelegateParam {
157                        name: param_name,
158                        rust_type: en.to_string(),
159                        conversion: ParamConversion::Enum {
160                            rust_type: en.to_string(),
161                            repr: ctx
162                                .enum_actual_repr(en)
163                                .unwrap_or("u8")
164                                .to_string(),
165                        },
166                    })
167                } else {
168                    None
169                }
170            } else {
171                Some(DelegateParam {
172                    name: param_name,
173                    rust_type: "u8".into(),
174                    conversion: ParamConversion::Primitive("u8".into()),
175                })
176            }
177        }
178        "Int16Property" => prim_param(param_name, "i16"),
179        "UInt16Property" => prim_param(param_name, "u16"),
180        "IntProperty" => prim_param(param_name, "i32"),
181        "UInt32Property" => prim_param(param_name, "u32"),
182        "Int64Property" => prim_param(param_name, "i64"),
183        "UInt64Property" => prim_param(param_name, "u64"),
184        "FloatProperty" => prim_param(param_name, "f32"),
185        "DoubleProperty" => prim_param(param_name, "f64"),
186        "NameProperty" => Some(DelegateParam {
187            name: param_name,
188            rust_type: "rusteal_core::FNameHandle".into(),
189            conversion: ParamConversion::FName,
190        }),
191        "StrProperty" | "TextProperty" => Some(DelegateParam {
192            name: param_name,
193            rust_type: "String".into(),
194            conversion: ParamConversion::String,
195        }),
196        "ObjectProperty" | "ClassProperty" => {
197            let cls = value.get("class_name").and_then(|v| v.as_str());
198            if let Some(cls) = cls {
199                if ctx.classes.contains_key(cls) {
200                    Some(DelegateParam {
201                        name: param_name,
202                        rust_type: format!("rusteal_core::UObjectRef<{cls}>"),
203                        conversion: ParamConversion::ObjectRef(cls.to_string()),
204                    })
205                } else {
206                    None
207                }
208            } else {
209                Some(DelegateParam {
210                    name: param_name,
211                    rust_type: "rusteal_core::UObjectHandle".into(),
212                    conversion: ParamConversion::Primitive("rusteal_core::UObjectHandle".into()),
213                })
214            }
215        }
216        "EnumProperty" => {
217            let en = value.get("enum_name").and_then(|v| v.as_str())?;
218            if !ctx.enums.contains_key(en) {
219                return None;
220            }
221            Some(DelegateParam {
222                name: param_name,
223                rust_type: en.to_string(),
224                conversion: ParamConversion::Enum {
225                    rust_type: en.to_string(),
226                    repr: ctx.enum_actual_repr(en).unwrap_or("u8").to_string(),
227                },
228            })
229        }
230        "StructProperty" => {
231            let sn = value.get("struct_name").and_then(|v| v.as_str())?;
232            let si = ctx.structs.get(sn)?;
233            if !si.has_static_struct {
234                return None;
235            }
236            Some(DelegateParam {
237                name: param_name,
238                rust_type: format!("rusteal_core::OwnedStruct<{}>", si.cpp_name),
239                conversion: ParamConversion::Struct {
240                    struct_name: sn.to_string(),
241                    cpp_name: si.cpp_name.clone(),
242                },
243            })
244        }
245        _ => None,
246    }
247}
248
249fn prim_param(name: String, ty: &str) -> Option<DelegateParam> {
250    Some(DelegateParam {
251        name,
252        rust_type: ty.into(),
253        conversion: ParamConversion::Primitive(ty.into()),
254    })
255}
256
257// ---------------------------------------------------------------------------
258// Code generation
259// ---------------------------------------------------------------------------
260
261/// Generate trait method implementations for delegate properties (used as default impls).
262pub fn generate_delegate_impls(
263    out: &mut String,
264    delegates: &[DelegateInfo],
265) {
266    for d in delegates {
267        let rust_name = &d.rust_name;
268        let struct_name = &d.struct_name;
269        let class_name = d.class_name;
270        let prop_name = &d.prop.name;
271        let prop_name_len = prop_name.len();
272        let byte_lit = format!("b\"{}\\0\"", prop_name);
273
274        out.push_str(&format!(
275            "    fn {rust_name}(&self) -> {struct_name} {{\n\
276             \x20       static PROP: std::sync::OnceLock<rusteal_core::FPropertyHandle> = std::sync::OnceLock::new();\n\
277             \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
278             \x20           rusteal_core::ffi_dispatch::reflection_find_property(\n\
279             \x20               {class_name}::static_class(), {byte_lit}.as_ptr(), {prop_name_len}\n\
280             \x20           )\n\
281             \x20       }});\n\
282             \x20       {struct_name} {{ owner: self.handle(), prop }}\n\
283             \x20   }}\n\n"
284        ));
285    }
286}
287
288/// Generate delegate wrapper structs with typed bind/add methods.
289/// These are emitted at the top of the class file (before the trait).
290pub fn generate_delegate_structs(
291    out: &mut String,
292    delegates: &[DelegateInfo],
293    class_name: &str,
294) {
295    for d in delegates {
296        let struct_name = &d.struct_name;
297        let is_multicast = d.is_multicast;
298
299        out.push_str(&format!(
300            "pub struct {struct_name} {{\n\
301             \x20   pub owner: rusteal_core::UObjectHandle,\n\
302             \x20   pub prop: rusteal_core::FPropertyHandle,\n\
303             }}\n\n"
304        ));
305
306        // Build the closure parameter types and extraction code.
307        // UHT exports the stripped function name (e.g., "OnEditableTextBoxCommittedEvent"),
308        // but UE stores the signature UFunction with "__DelegateSignature" suffix.
309        let sig_name_base = d.prop.func_info.as_ref()
310            .and_then(|fi| fi.get("name"))
311            .and_then(|n| n.as_str())
312            .unwrap_or(&d.prop.name);
313        let sig_name = format!("{sig_name_base}__DelegateSignature");
314        let sig_name_len = sig_name.len();
315        let sig_byte_lit = format!("b\"{}\\0\"", sig_name);
316
317        // Build closure param types for the user-facing closure signature
318        let callback_params: Vec<String> = d.params.iter().map(|p| p.rust_type.clone()).collect();
319        let callback_sig = callback_params.join(", ");
320
321        let method_name = if is_multicast { "add" } else { "bind" };
322        let api_fn = if is_multicast { "bind_multicast" } else { "bind_unicast" };
323
324        out.push_str(&format!(
325            "impl {struct_name} {{\n"
326        ));
327
328        // Generate the bind/add method
329        out.push_str(&format!(
330            "    pub fn {method_name}(&self, mut callback: impl FnMut({callback_sig}) + Send + 'static) -> rusteal_core::RustealResult<rusteal_core::DelegateBinding> {{\n"
331        ));
332
333        // If there are params, we need to resolve offsets + property handles via OnceLock
334        if !d.params.is_empty() {
335            let n_params = d.params.len();
336            out.push_str(&format!(
337                "        static PARAM_INFO: std::sync::OnceLock<[(u32, rusteal_core::FPropertyHandle); {n_params}]> = std::sync::OnceLock::new();\n\
338                 \x20       let param_info = PARAM_INFO.get_or_init(|| unsafe {{\n\
339                 \x20           let sig_func = rusteal_core::ffi_dispatch::reflection_find_function_by_class(\n\
340                 \x20               {class_name}::static_class(),\n\
341                 \x20               {sig_byte_lit}.as_ptr(), {sig_name_len});\n\
342                 \x20           [\n"
343            ));
344
345            for p in &d.params {
346                let param_ue_name = &d.prop.func_info.as_ref()
347                    .and_then(|fi| fi.get("params"))
348                    .and_then(|ps| ps.as_array())
349                    .and_then(|arr| arr.iter().find(|v| {
350                        v.get("name").and_then(|n| n.as_str()).map(to_snake_case) == Some(p.name.clone())
351                    }))
352                    .and_then(|v| v.get("name"))
353                    .and_then(|n| n.as_str())
354                    .unwrap_or(&p.name);
355                let pname_len = param_ue_name.len();
356                let pname_lit = format!("b\"{}\\0\"", param_ue_name);
357                out.push_str(&format!(
358                    "                {{\n\
359                     \x20                   let param_prop = rusteal_core::ffi_dispatch::reflection_get_function_param(\n\
360                     \x20                       sig_func, {pname_lit}.as_ptr(), {pname_len});\n\
361                     \x20                   (rusteal_core::ffi_dispatch::reflection_get_property_offset(param_prop), param_prop)\n\
362                     \x20               }},\n"
363                ));
364            }
365
366            out.push_str(
367                "            ]\n\
368                 \x20       });\n\
369                 \x20       #[allow(unused_variables)] let param_info = param_info;\n"
370            );
371        }
372
373        // Build the closure wrapper that extracts typed params from NativePtr
374        if d.params.is_empty() {
375            out.push_str(&format!(
376                "        let owner = self.owner;\n\
377                 \x20       let prop = self.prop;\n\
378                 \x20       rusteal_core::delegate_registry::{api_fn}(owner, prop, move |_params: rusteal_core::ffi_dispatch::NativePtr| {{\n\
379                 \x20           callback();\n\
380                 \x20       }})\n\
381                 \x20   }}\n"
382            ));
383            out.push_str("}\n\n");
384            continue;
385        }
386
387        out.push_str(&format!(
388            "        let owner = self.owner;\n\
389             \x20       let prop = self.prop;\n\
390             \x20       rusteal_core::delegate_registry::{api_fn}(owner, prop, move |params: rusteal_core::ffi_dispatch::NativePtr| {{\n\
391             \x20           unsafe {{\n"
392        ));
393
394        // Extract each parameter
395        for (i, p) in d.params.iter().enumerate() {
396            let var_name = &p.name;
397            match &p.conversion {
398                ParamConversion::Primitive(ty) => {
399                    out.push_str(&format!(
400                        "                let {var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{ty}>(params, param_info[{i}].0 as usize);\n"
401                    ));
402                }
403                ParamConversion::ObjectRef(_cls) => {
404                    out.push_str(&format!(
405                        "                let {var_name} = rusteal_core::UObjectRef::from_raw(\n\
406                         \x20                   rusteal_core::ffi_dispatch::native_mem_read::<rusteal_core::UObjectHandle>(params, param_info[{i}].0 as usize)\n\
407                         \x20               );\n"
408                    ));
409                }
410                ParamConversion::Enum { rust_type, repr } => {
411                    out.push_str(&format!(
412                        "                let __raw_{var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{repr}>(params, param_info[{i}].0 as usize);\n\
413                         \x20               let {var_name} = {rust_type}::from_value(__raw_{var_name}).unwrap_or_else(|| std::mem::transmute(__raw_{var_name}));\n"
414                    ));
415                }
416                ParamConversion::FName => {
417                    out.push_str(&format!(
418                        "                let {var_name} = {{\n\
419                         \x20                   let mut __buf = [0u8; 8];\n\
420                         \x20                   let mut __written: u32 = 0;\n\
421                         \x20                   rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
422                         \x20                       param_info[{i}].1,\n\
423                         \x20                       params,\n\
424                         \x20                       param_info[{i}].0,\n\
425                         \x20                       __buf.as_mut_ptr(),\n\
426                         \x20                       8,\n\
427                         \x20                       &mut __written,\n\
428                         \x20                   ));\n\
429                         \x20                   rusteal_core::FNameHandle(u64::from_ne_bytes(__buf))\n\
430                         \x20               }};\n"
431                    ));
432                }
433                ParamConversion::String => {
434                    out.push_str(&format!(
435                        "                let {var_name} = {{\n\
436                         \x20                   let mut __buf = vec![0u8; 260];\n\
437                         \x20                   let mut __written: u32 = 0;\n\
438                         \x20                   let __err = rusteal_core::ffi_dispatch::delegate_read_param(\n\
439                         \x20                       param_info[{i}].1,\n\
440                         \x20                       params,\n\
441                         \x20                       param_info[{i}].0,\n\
442                         \x20                       __buf.as_mut_ptr(),\n\
443                         \x20                       __buf.len() as u32,\n\
444                         \x20                       &mut __written,\n\
445                         \x20                   );\n\
446                         \x20                   if __err == rusteal_core::RustealErrorCode::BufferTooSmall && __written > 0 {{\n\
447                         \x20                       __buf.resize(__written as usize, 0);\n\
448                         \x20                       rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
449                         \x20                           param_info[{i}].1,\n\
450                         \x20                           params,\n\
451                         \x20                           param_info[{i}].0,\n\
452                         \x20                           __buf.as_mut_ptr(),\n\
453                         \x20                           __buf.len() as u32,\n\
454                         \x20                           &mut __written,\n\
455                         \x20                       ));\n\
456                         \x20                   }}\n\
457                         \x20                   if __written >= 4 {{\n\
458                         \x20                       let __slen = u32::from_ne_bytes([__buf[0], __buf[1], __buf[2], __buf[3]]) as usize;\n\
459                         \x20                       String::from_utf8_lossy(&__buf[4..4 + __slen]).into_owned()\n\
460                         \x20                   }} else {{\n\
461                         \x20                       String::new()\n\
462                         \x20                   }}\n\
463                         \x20               }};\n"
464                    ));
465                }
466                ParamConversion::Struct { cpp_name, .. } => {
467                    out.push_str(&format!(
468                        "                let {var_name} = {{\n\
469                         \x20                   let __size = rusteal_core::ffi_dispatch::reflection_get_property_size(param_info[{i}].1) as usize;\n\
470                         \x20                   let mut __buf = vec![0u8; __size];\n\
471                         \x20                   let mut __written: u32 = 0;\n\
472                         \x20                   rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
473                         \x20                       param_info[{i}].1,\n\
474                         \x20                       params,\n\
475                         \x20                       param_info[{i}].0,\n\
476                         \x20                       __buf.as_mut_ptr(),\n\
477                         \x20                       __size as u32,\n\
478                         \x20                       &mut __written,\n\
479                         \x20                   ));\n\
480                         \x20                   rusteal_core::OwnedStruct::<{cpp_name}>::from_bytes(__buf)\n\
481                         \x20               }};\n"
482                    ));
483                }
484            }
485        }
486
487        // Call the user's callback with extracted params
488        let param_names: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
489        let call_args = param_names.join(", ");
490        out.push_str(&format!(
491            "                callback({call_args});\n\
492             \x20           }}\n\
493             \x20       }})\n\
494             \x20   }}\n"
495        ));
496
497        out.push_str("}\n\n");
498    }
499}