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        // A class (`TSubclassOf<T>`), typed by its meta class.
197        "ClassProperty"
198            if value
199                .get("meta_class_name")
200                .and_then(|v| v.as_str())
201                .is_some_and(|meta| ctx.classes.contains_key(meta)) =>
202        {
203            let meta = value["meta_class_name"].as_str().unwrap_or_default();
204            Some(DelegateParam {
205                name: param_name,
206                rust_type: format!("rusteal_core::SubclassOf<{meta}>"),
207                conversion: ParamConversion::ObjectRef(meta.to_string()),
208            })
209        }
210        "ObjectProperty" | "ClassProperty" => {
211            let cls = value.get("class_name").and_then(|v| v.as_str());
212            if let Some(cls) = cls {
213                if ctx.classes.contains_key(cls) {
214                    Some(DelegateParam {
215                        name: param_name,
216                        rust_type: format!("rusteal_core::UObjectRef<{cls}>"),
217                        conversion: ParamConversion::ObjectRef(cls.to_string()),
218                    })
219                } else {
220                    None
221                }
222            } else {
223                Some(DelegateParam {
224                    name: param_name,
225                    rust_type: "rusteal_core::UObjectHandle".into(),
226                    conversion: ParamConversion::Primitive("rusteal_core::UObjectHandle".into()),
227                })
228            }
229        }
230        "EnumProperty" => {
231            let en = value.get("enum_name").and_then(|v| v.as_str())?;
232            if !ctx.enums.contains_key(en) {
233                return None;
234            }
235            Some(DelegateParam {
236                name: param_name,
237                rust_type: en.to_string(),
238                conversion: ParamConversion::Enum {
239                    rust_type: en.to_string(),
240                    repr: ctx.enum_actual_repr(en).unwrap_or("u8").to_string(),
241                },
242            })
243        }
244        "StructProperty" => {
245            let sn = value.get("struct_name").and_then(|v| v.as_str())?;
246            let si = ctx.structs.get(sn)?;
247            if !si.has_static_struct {
248                return None;
249            }
250            Some(DelegateParam {
251                name: param_name,
252                rust_type: format!("rusteal_core::OwnedStruct<{}>", si.cpp_name),
253                conversion: ParamConversion::Struct {
254                    struct_name: sn.to_string(),
255                    cpp_name: si.cpp_name.clone(),
256                },
257            })
258        }
259        _ => None,
260    }
261}
262
263fn prim_param(name: String, ty: &str) -> Option<DelegateParam> {
264    Some(DelegateParam {
265        name,
266        rust_type: ty.into(),
267        conversion: ParamConversion::Primitive(ty.into()),
268    })
269}
270
271// ---------------------------------------------------------------------------
272// Code generation
273// ---------------------------------------------------------------------------
274
275/// Generate trait method implementations for delegate properties (used as default impls).
276pub fn generate_delegate_impls(
277    out: &mut String,
278    delegates: &[DelegateInfo],
279) {
280    for d in delegates {
281        let rust_name = &d.rust_name;
282        let struct_name = &d.struct_name;
283        let class_name = d.class_name;
284        let prop_name = &d.prop.name;
285        let prop_name_len = prop_name.len();
286        let byte_lit = format!("b\"{}\\0\"", prop_name);
287
288        out.push_str(&format!(
289            "    fn {rust_name}(&self) -> {struct_name} {{\n\
290             \x20       static PROP: std::sync::OnceLock<rusteal_core::FPropertyHandle> = std::sync::OnceLock::new();\n\
291             \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
292             \x20           rusteal_core::ffi_dispatch::reflection_find_property(\n\
293             \x20               {class_name}::static_class(), {byte_lit}.as_ptr(), {prop_name_len}\n\
294             \x20           )\n\
295             \x20       }});\n\
296             \x20       {struct_name} {{ owner: self.handle(), prop }}\n\
297             \x20   }}\n\n"
298        ));
299    }
300}
301
302/// Generate delegate wrapper structs with typed bind/add methods.
303/// These are emitted at the top of the class file (before the trait).
304pub fn generate_delegate_structs(
305    out: &mut String,
306    delegates: &[DelegateInfo],
307    class_name: &str,
308) {
309    for d in delegates {
310        let struct_name = &d.struct_name;
311        let is_multicast = d.is_multicast;
312
313        out.push_str(&format!(
314            "pub struct {struct_name} {{\n\
315             \x20   pub owner: rusteal_core::UObjectHandle,\n\
316             \x20   pub prop: rusteal_core::FPropertyHandle,\n\
317             }}\n\n"
318        ));
319
320        // Build the closure parameter types and extraction code.
321        // UHT exports the stripped function name (e.g., "OnEditableTextBoxCommittedEvent"),
322        // but UE stores the signature UFunction with "__DelegateSignature" suffix.
323        let sig_name_base = d.prop.func_info.as_ref()
324            .and_then(|fi| fi.get("name"))
325            .and_then(|n| n.as_str())
326            .unwrap_or(&d.prop.name);
327        let sig_name = format!("{sig_name_base}__DelegateSignature");
328        let sig_name_len = sig_name.len();
329        let sig_byte_lit = format!("b\"{}\\0\"", sig_name);
330
331        // Build closure param types for the user-facing closure signature
332        let callback_params: Vec<String> = d.params.iter().map(|p| p.rust_type.clone()).collect();
333        let callback_sig = callback_params.join(", ");
334
335        let method_name = if is_multicast { "add" } else { "bind" };
336        let api_fn = if is_multicast { "bind_multicast" } else { "bind_unicast" };
337
338        out.push_str(&format!(
339            "impl {struct_name} {{\n"
340        ));
341
342        // Bind a UFunction by name, as C++'s AddDynamic / BindDynamic.
343        let ufunction_method = if is_multicast { "add_ufunction" } else { "bind_ufunction" };
344        out.push_str(&format!(
345            "    /// Bind `target`'s UFunction named `function` (a `#[ufunction]` of a Rust\n\
346             \x20   /// class, say), as C++'s `{}` does{}.\n\
347             \x20   pub fn {ufunction_method}(&self, target: &rusteal_core::UObjectRef<impl rusteal_core::UeClass>, function: &str) -> rusteal_core::RustealResult<()> {{\n\
348             \x20       let target = target.checked()?.raw();\n\
349             \x20       rusteal_core::check_ffi(unsafe {{\n\
350             \x20           rusteal_core::ffi_dispatch::delegate_add_function(self.owner, self.prop, target, function.as_ptr(), function.len() as u32)\n\
351             \x20       }})\n\
352             \x20   }}\n\n",
353            if is_multicast { "AddDynamic" } else { "BindDynamic" },
354            if is_multicast { ": once, however many times it is called" } else { "" },
355        ));
356
357        // Generate the bind/add method
358        out.push_str(&format!(
359            "    pub fn {method_name}(&self, mut callback: impl FnMut({callback_sig}) + Send + 'static) -> rusteal_core::RustealResult<rusteal_core::DelegateBinding> {{\n"
360        ));
361
362        // If there are params, we need to resolve offsets + property handles via OnceLock
363        if !d.params.is_empty() {
364            let n_params = d.params.len();
365            out.push_str(&format!(
366                "        static PARAM_INFO: std::sync::OnceLock<[(u32, rusteal_core::FPropertyHandle); {n_params}]> = std::sync::OnceLock::new();\n\
367                 \x20       let param_info = PARAM_INFO.get_or_init(|| unsafe {{\n\
368                 \x20           let sig_func = rusteal_core::ffi_dispatch::reflection_find_function_by_class(\n\
369                 \x20               {class_name}::static_class(),\n\
370                 \x20               {sig_byte_lit}.as_ptr(), {sig_name_len});\n\
371                 \x20           [\n"
372            ));
373
374            for p in &d.params {
375                let param_ue_name = &d.prop.func_info.as_ref()
376                    .and_then(|fi| fi.get("params"))
377                    .and_then(|ps| ps.as_array())
378                    .and_then(|arr| arr.iter().find(|v| {
379                        v.get("name").and_then(|n| n.as_str()).map(to_snake_case) == Some(p.name.clone())
380                    }))
381                    .and_then(|v| v.get("name"))
382                    .and_then(|n| n.as_str())
383                    .unwrap_or(&p.name);
384                let pname_len = param_ue_name.len();
385                let pname_lit = format!("b\"{}\\0\"", param_ue_name);
386                out.push_str(&format!(
387                    "                {{\n\
388                     \x20                   let param_prop = rusteal_core::ffi_dispatch::reflection_get_function_param(\n\
389                     \x20                       sig_func, {pname_lit}.as_ptr(), {pname_len});\n\
390                     \x20                   (rusteal_core::ffi_dispatch::reflection_get_property_offset(param_prop), param_prop)\n\
391                     \x20               }},\n"
392                ));
393            }
394
395            out.push_str(
396                "            ]\n\
397                 \x20       });\n\
398                 \x20       #[allow(unused_variables)] let param_info = param_info;\n"
399            );
400        }
401
402        // Build the closure wrapper that extracts typed params from NativePtr
403        if d.params.is_empty() {
404            out.push_str(&format!(
405                "        let owner = self.owner;\n\
406                 \x20       let prop = self.prop;\n\
407                 \x20       rusteal_core::delegate_registry::{api_fn}(owner, prop, move |_params: rusteal_core::ffi_dispatch::NativePtr| {{\n\
408                 \x20           callback();\n\
409                 \x20       }})\n\
410                 \x20   }}\n"
411            ));
412            out.push_str("}\n\n");
413            continue;
414        }
415
416        out.push_str(&format!(
417            "        let owner = self.owner;\n\
418             \x20       let prop = self.prop;\n\
419             \x20       rusteal_core::delegate_registry::{api_fn}(owner, prop, move |params: rusteal_core::ffi_dispatch::NativePtr| {{\n\
420             \x20           unsafe {{\n"
421        ));
422
423        // Extract each parameter
424        for (i, p) in d.params.iter().enumerate() {
425            let var_name = &p.name;
426            match &p.conversion {
427                ParamConversion::Primitive(ty) => {
428                    out.push_str(&format!(
429                        "                let {var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{ty}>(params, param_info[{i}].0 as usize);\n"
430                    ));
431                }
432                ParamConversion::ObjectRef(_cls) => {
433                    out.push_str(&format!(
434                        "                let {var_name} = rusteal_core::ObjectPointer::from_object_handle(\n\
435                         \x20                   rusteal_core::ffi_dispatch::native_mem_read::<rusteal_core::UObjectHandle>(params, param_info[{i}].0 as usize)\n\
436                         \x20               );\n"
437                    ));
438                }
439                ParamConversion::Enum { rust_type, repr } => {
440                    out.push_str(&format!(
441                        "                let __raw_{var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{repr}>(params, param_info[{i}].0 as usize);\n\
442                         \x20               let {var_name} = {rust_type}::from_value(__raw_{var_name}).unwrap_or_else(|| std::mem::transmute(__raw_{var_name}));\n"
443                    ));
444                }
445                ParamConversion::FName => {
446                    out.push_str(&format!(
447                        "                let {var_name} = {{\n\
448                         \x20                   let mut __buf = [0u8; 8];\n\
449                         \x20                   let mut __written: u32 = 0;\n\
450                         \x20                   rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
451                         \x20                       param_info[{i}].1,\n\
452                         \x20                       params,\n\
453                         \x20                       param_info[{i}].0,\n\
454                         \x20                       __buf.as_mut_ptr(),\n\
455                         \x20                       8,\n\
456                         \x20                       &mut __written,\n\
457                         \x20                   ));\n\
458                         \x20                   rusteal_core::FNameHandle(u64::from_ne_bytes(__buf))\n\
459                         \x20               }};\n"
460                    ));
461                }
462                ParamConversion::String => {
463                    out.push_str(&format!(
464                        "                let {var_name} = {{\n\
465                         \x20                   let mut __buf = vec![0u8; 260];\n\
466                         \x20                   let mut __written: u32 = 0;\n\
467                         \x20                   let __err = rusteal_core::ffi_dispatch::delegate_read_param(\n\
468                         \x20                       param_info[{i}].1,\n\
469                         \x20                       params,\n\
470                         \x20                       param_info[{i}].0,\n\
471                         \x20                       __buf.as_mut_ptr(),\n\
472                         \x20                       __buf.len() as u32,\n\
473                         \x20                       &mut __written,\n\
474                         \x20                   );\n\
475                         \x20                   if __err == rusteal_core::RustealErrorCode::BufferTooSmall && __written > 0 {{\n\
476                         \x20                       __buf.resize(__written as usize, 0);\n\
477                         \x20                       rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
478                         \x20                           param_info[{i}].1,\n\
479                         \x20                           params,\n\
480                         \x20                           param_info[{i}].0,\n\
481                         \x20                           __buf.as_mut_ptr(),\n\
482                         \x20                           __buf.len() as u32,\n\
483                         \x20                           &mut __written,\n\
484                         \x20                       ));\n\
485                         \x20                   }}\n\
486                         \x20                   if __written >= 4 {{\n\
487                         \x20                       let __slen = u32::from_ne_bytes([__buf[0], __buf[1], __buf[2], __buf[3]]) as usize;\n\
488                         \x20                       String::from_utf8_lossy(&__buf[4..4 + __slen]).into_owned()\n\
489                         \x20                   }} else {{\n\
490                         \x20                       String::new()\n\
491                         \x20                   }}\n\
492                         \x20               }};\n"
493                    ));
494                }
495                ParamConversion::Struct { cpp_name, .. } => {
496                    out.push_str(&format!(
497                        "                let {var_name} = {{\n\
498                         \x20                   let __size = rusteal_core::ffi_dispatch::reflection_get_property_size(param_info[{i}].1) as usize;\n\
499                         \x20                   let mut __buf = vec![0u8; __size];\n\
500                         \x20                   let mut __written: u32 = 0;\n\
501                         \x20                   rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
502                         \x20                       param_info[{i}].1,\n\
503                         \x20                       params,\n\
504                         \x20                       param_info[{i}].0,\n\
505                         \x20                       __buf.as_mut_ptr(),\n\
506                         \x20                       __size as u32,\n\
507                         \x20                       &mut __written,\n\
508                         \x20                   ));\n\
509                         \x20                   rusteal_core::OwnedStruct::<{cpp_name}>::from_bytes(__buf)\n\
510                         \x20               }};\n"
511                    ));
512                }
513            }
514        }
515
516        // Call the user's callback with extracted params
517        let param_names: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
518        let call_args = param_names.join(", ");
519        out.push_str(&format!(
520            "                callback({call_args});\n\
521             \x20           }}\n\
522             \x20       }})\n\
523             \x20   }}\n"
524        ));
525
526        out.push_str("}\n\n");
527    }
528}