Skip to main content

rusteal_codegen/rust_gen/
delegates.rs

1use crate::context::CodegenContext;
2use crate::naming::to_snake_case;
3use crate::schema::PropertyInfo;
4use crate::type_map::{self, ConversionKind};
5
6pub struct DelegateInfo<'a> {
7    pub prop: &'a PropertyInfo,
8    pub class_name: &'a str,
9    pub rust_name: String,
10    pub struct_name: String,
11    pub is_multicast: bool,
12    pub params: Vec<DelegateParam>,
13}
14
15pub struct DelegateParam {
16    pub name: String,
17    pub rust_type: String,
18    pub conversion: ParamConversion,
19}
20
21pub enum ParamConversion {
22    Primitive(String),
23    ObjectRef(String),
24    Enum {
25        rust_type: String,
26        repr: String,
27    },
28    FName,
29    String,
30    Struct {
31        struct_name: String,
32        cpp_name: String,
33    },
34}
35
36pub fn collect_delegate_props<'a>(
37    props: &'a [PropertyInfo],
38    class_name: &'a str,
39    ctx: &CodegenContext,
40) -> Vec<DelegateInfo<'a>> {
41    let mut result = Vec::new();
42
43    for prop in props {
44        let mapped = type_map::map_property_type(
45            &prop.prop_type,
46            prop.class_name.as_deref(),
47            prop.struct_name.as_deref(),
48            prop.enum_name.as_deref(),
49            prop.enum_underlying_type.as_deref(),
50            prop.meta_class_name.as_deref(),
51            prop.interface_name.as_deref(),
52        );
53
54        if !matches!(
55            mapped.rust_to_ffi,
56            ConversionKind::Delegate | ConversionKind::MulticastDelegate
57        ) {
58            continue;
59        }
60
61        let is_multicast = matches!(mapped.rust_to_ffi, ConversionKind::MulticastDelegate);
62
63        let func_info = match &prop.func_info {
64            Some(fi) => fi,
65            None => continue,
66        };
67
68        let params_json = match func_info.get("params").and_then(|p| p.as_array()) {
69            Some(params) => params,
70            None => &Vec::new() as &Vec<serde_json::Value>,
71        };
72
73        let mut params = Vec::new();
74        let mut all_supported = true;
75
76        for param_value in params_json {
77            let param_name = param_value
78                .get("name")
79                .and_then(|n| n.as_str())
80                .unwrap_or("unknown");
81
82            let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
83                Some(t) => t,
84                None => {
85                    all_supported = false;
86                    break;
87                }
88            };
89
90            match resolve_delegate_param(param_name, param_type, param_value, ctx) {
91                Some(dp) => params.push(dp),
92                None => {
93                    all_supported = false;
94                    break;
95                }
96            }
97        }
98
99        if !all_supported {
100            continue;
101        }
102
103        let rust_name = to_snake_case(&prop.name);
104        let struct_name = format!("{}{}Delegate", class_name, prop.name);
105
106        result.push(DelegateInfo {
107            prop,
108            class_name,
109            rust_name,
110            struct_name,
111            is_multicast,
112            params,
113        });
114    }
115
116    result
117}
118
119fn resolve_delegate_param(
120    name: &str,
121    prop_type: &str,
122    value: &serde_json::Value,
123    ctx: &CodegenContext,
124) -> Option<DelegateParam> {
125    let param_name = to_snake_case(name);
126
127    match prop_type {
128        "BoolProperty" => Some(DelegateParam {
129            name: param_name,
130            rust_type: "bool".into(),
131            conversion: ParamConversion::Primitive("bool".into()),
132        }),
133        "Int8Property" => Some(DelegateParam {
134            name: param_name,
135            rust_type: "i8".into(),
136            conversion: ParamConversion::Primitive("i8".into()),
137        }),
138        "ByteProperty" => {
139            if let Some(en) = value.get("enum_name").and_then(|v| v.as_str()) {
140                if ctx.enums.contains_key(en) {
141                    Some(DelegateParam {
142                        name: param_name,
143                        rust_type: en.to_string(),
144                        conversion: ParamConversion::Enum {
145                            rust_type: en.to_string(),
146                            repr: ctx.enum_actual_repr(en).unwrap_or("u8").to_string(),
147                        },
148                    })
149                } else {
150                    None
151                }
152            } else {
153                Some(DelegateParam {
154                    name: param_name,
155                    rust_type: "u8".into(),
156                    conversion: ParamConversion::Primitive("u8".into()),
157                })
158            }
159        }
160        "Int16Property" => prim_param(param_name, "i16"),
161        "UInt16Property" => prim_param(param_name, "u16"),
162        "IntProperty" => prim_param(param_name, "i32"),
163        "UInt32Property" => prim_param(param_name, "u32"),
164        "Int64Property" => prim_param(param_name, "i64"),
165        "UInt64Property" => prim_param(param_name, "u64"),
166        "FloatProperty" => prim_param(param_name, "f32"),
167        "DoubleProperty" => prim_param(param_name, "f64"),
168        "NameProperty" => Some(DelegateParam {
169            name: param_name,
170            rust_type: "rusteal_core::FNameHandle".into(),
171            conversion: ParamConversion::FName,
172        }),
173        "StrProperty" | "TextProperty" => Some(DelegateParam {
174            name: param_name,
175            rust_type: "String".into(),
176            conversion: ParamConversion::String,
177        }),
178        "ClassProperty"
179            if value
180                .get("meta_class_name")
181                .and_then(|v| v.as_str())
182                .is_some_and(|meta| ctx.classes.contains_key(meta)) =>
183        {
184            let meta = value["meta_class_name"].as_str().unwrap_or_default();
185
186            Some(DelegateParam {
187                name: param_name,
188                rust_type: format!("rusteal_core::SubclassOf<{meta}>"),
189                conversion: ParamConversion::ObjectRef(meta.to_string()),
190            })
191        }
192        "ObjectProperty" | "ClassProperty" => {
193            let cls = value.get("class_name").and_then(|v| v.as_str());
194
195            if let Some(cls) = cls {
196                if ctx.classes.contains_key(cls) {
197                    Some(DelegateParam {
198                        name: param_name,
199                        rust_type: format!("rusteal_core::UObjectRef<{cls}>"),
200                        conversion: ParamConversion::ObjectRef(cls.to_string()),
201                    })
202                } else {
203                    None
204                }
205            } else {
206                Some(DelegateParam {
207                    name: param_name,
208                    rust_type: "rusteal_core::UObjectHandle".into(),
209                    conversion: ParamConversion::Primitive("rusteal_core::UObjectHandle".into()),
210                })
211            }
212        }
213        "EnumProperty" => {
214            let en = value.get("enum_name").and_then(|v| v.as_str())?;
215
216            if !ctx.enums.contains_key(en) {
217                return None;
218            }
219
220            Some(DelegateParam {
221                name: param_name,
222                rust_type: en.to_string(),
223                conversion: ParamConversion::Enum {
224                    rust_type: en.to_string(),
225                    repr: ctx.enum_actual_repr(en).unwrap_or("u8").to_string(),
226                },
227            })
228        }
229        "StructProperty" => {
230            let sn = value.get("struct_name").and_then(|v| v.as_str())?;
231            let si = ctx.structs.get(sn)?;
232
233            if !si.has_static_struct {
234                return None;
235            }
236
237            Some(DelegateParam {
238                name: param_name,
239                rust_type: format!("rusteal_core::OwnedStruct<{}>", si.cpp_name),
240                conversion: ParamConversion::Struct {
241                    struct_name: sn.to_string(),
242                    cpp_name: si.cpp_name.clone(),
243                },
244            })
245        }
246        _ => None,
247    }
248}
249
250fn prim_param(name: String, ty: &str) -> Option<DelegateParam> {
251    Some(DelegateParam {
252        name,
253        rust_type: ty.into(),
254        conversion: ParamConversion::Primitive(ty.into()),
255    })
256}
257
258pub fn generate_delegate_impls(out: &mut String, delegates: &[DelegateInfo]) {
259    for d in delegates {
260        let rust_name = &d.rust_name;
261        let struct_name = &d.struct_name;
262        let class_name = d.class_name;
263        let prop_name = &d.prop.name;
264        let prop_name_len = prop_name.len();
265        let byte_lit = format!("b\"{prop_name}\\0\"");
266
267        out.push_str(&format!(
268            "    fn {rust_name}(&self) -> {struct_name} {{\n\
269             \x20       static PROP: std::sync::OnceLock<rusteal_core::FPropertyHandle> = std::sync::OnceLock::new();\n\
270             \x20       let prop = *PROP.get_or_init(|| unsafe {{\n\
271             \x20           rusteal_core::ffi_dispatch::reflection_find_property(\n\
272             \x20               {class_name}::static_class(), {byte_lit}.as_ptr(), {prop_name_len}\n\
273             \x20           )\n\
274             \x20       }});\n\
275             \x20       {struct_name} {{ owner: self.handle(), prop }}\n\
276             \x20   }}\n\n"
277        ));
278    }
279}
280
281pub fn generate_delegate_structs(out: &mut String, delegates: &[DelegateInfo], class_name: &str) {
282    for d in delegates {
283        let struct_name = &d.struct_name;
284        let is_multicast = d.is_multicast;
285
286        out.push_str(&format!(
287            "pub struct {struct_name} {{\n\
288             \x20   pub owner: rusteal_core::UObjectHandle,\n\
289             \x20   pub prop: rusteal_core::FPropertyHandle,\n\
290             }}\n\n"
291        ));
292
293        let sig_name_base = d
294            .prop
295            .func_info
296            .as_ref()
297            .and_then(|fi| fi.get("name"))
298            .and_then(|n| n.as_str())
299            .unwrap_or(&d.prop.name);
300
301        let sig_name = format!("{sig_name_base}__DelegateSignature");
302        let sig_name_len = sig_name.len();
303        let sig_byte_lit = format!("b\"{sig_name}\\0\"");
304
305        let callback_params: Vec<String> = d.params.iter().map(|p| p.rust_type.clone()).collect();
306        let callback_sig = callback_params.join(", ");
307
308        let method_name = if is_multicast { "add" } else { "bind" };
309
310        let api_fn = if is_multicast {
311            "bind_multicast"
312        } else {
313            "bind_unicast"
314        };
315
316        out.push_str(&format!("impl {struct_name} {{\n"));
317
318        let ufunction_method = if is_multicast {
319            "add_ufunction"
320        } else {
321            "bind_ufunction"
322        };
323
324        out.push_str(&format!(
325            "    /// Bind `target`'s UFunction named `function` (a `#[ufunction]` of a Rust\n\
326             \x20   /// class, say), as C++'s `{}` does{}.\n\
327             \x20   #[track_caller]\n\
328             \x20   pub fn {ufunction_method}(&self, target: &rusteal_core::UObjectRef<impl rusteal_core::UeClass>, function: &str) -> rusteal_core::RustealResult<()> {{\n\
329             \x20       let target = target.checked()?.raw();\n\
330             \x20       rusteal_core::check_ffi(unsafe {{\n\
331             \x20           rusteal_core::ffi_dispatch::delegate_add_function(self.owner, self.prop, target, function.as_ptr(), function.len() as u32)\n\
332             \x20       }})\n\
333             \x20   }}\n\n",
334            if is_multicast { "AddDynamic" } else { "BindDynamic" },
335            if is_multicast { ": once, however many times it is called" } else { "" },
336        ));
337
338        out.push_str(&format!(
339            "    #[track_caller]\n    pub fn {method_name}(&self, mut callback: impl FnMut({callback_sig}) + Send + 'static) -> rusteal_core::RustealResult<rusteal_core::DelegateBinding> {{\n"
340        ));
341
342        if !d.params.is_empty() {
343            let n_params = d.params.len();
344
345            out.push_str(&format!(
346                "        static PARAM_INFO: std::sync::OnceLock<[(u32, rusteal_core::FPropertyHandle); {n_params}]> = std::sync::OnceLock::new();\n\
347                 \x20       let param_info = PARAM_INFO.get_or_init(|| unsafe {{\n\
348                 \x20           let sig_func = rusteal_core::ffi_dispatch::reflection_find_function_by_class(\n\
349                 \x20               {class_name}::static_class(),\n\
350                 \x20               {sig_byte_lit}.as_ptr(), {sig_name_len});\n\
351                 \x20           [\n"
352            ));
353
354            for p in &d.params {
355                let param_ue_name = &d
356                    .prop
357                    .func_info
358                    .as_ref()
359                    .and_then(|fi| fi.get("params"))
360                    .and_then(|ps| ps.as_array())
361                    .and_then(|arr| {
362                        arr.iter().find(|v| {
363                            v.get("name").and_then(|n| n.as_str()).map(to_snake_case)
364                                == Some(p.name.clone())
365                        })
366                    })
367                    .and_then(|v| v.get("name"))
368                    .and_then(|n| n.as_str())
369                    .unwrap_or(&p.name);
370
371                let pname_len = param_ue_name.len();
372                let pname_lit = format!("b\"{param_ue_name}\\0\"");
373
374                out.push_str(&format!(
375                    "                {{\n\
376                     \x20                   let param_prop = rusteal_core::ffi_dispatch::reflection_get_function_param(\n\
377                     \x20                       sig_func, {pname_lit}.as_ptr(), {pname_len});\n\
378                     \x20                   (rusteal_core::ffi_dispatch::reflection_get_property_offset(param_prop), param_prop)\n\
379                     \x20               }},\n"
380                ));
381            }
382
383            out.push_str(
384                "            ]\n\
385                 \x20       });\n\
386                 \x20       #[allow(unused_variables)] let param_info = param_info;\n",
387            );
388        }
389
390        if d.params.is_empty() {
391            out.push_str(&format!(
392                "        let owner = self.owner;\n\
393                 \x20       let prop = self.prop;\n\
394                 \x20       rusteal_core::delegate_registry::{api_fn}(owner, prop, move |_params: rusteal_core::ffi_dispatch::NativePtr| {{\n\
395                 \x20           callback();\n\
396                 \x20       }})\n\
397                 \x20   }}\n"
398            ));
399
400            out.push_str("}\n\n");
401            continue;
402        }
403
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           unsafe {{\n"
409        ));
410
411        for (i, p) in d.params.iter().enumerate() {
412            let var_name = &p.name;
413
414            match &p.conversion {
415                ParamConversion::Primitive(ty) => {
416                    out.push_str(&format!(
417                        "                let {var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{ty}>(params, param_info[{i}].0 as usize);\n"
418                    ));
419                }
420                ParamConversion::ObjectRef(_cls) => {
421                    out.push_str(&format!(
422                        "                let {var_name} = rusteal_core::ObjectPointer::from_object_handle(\n\
423                         \x20                   rusteal_core::ffi_dispatch::native_mem_read::<rusteal_core::UObjectHandle>(params, param_info[{i}].0 as usize)\n\
424                         \x20               );\n"
425                    ));
426                }
427                ParamConversion::Enum { rust_type, repr } => {
428                    out.push_str(&format!(
429                        "                let __raw_{var_name} = rusteal_core::ffi_dispatch::native_mem_read::<{repr}>(params, param_info[{i}].0 as usize);\n\
430                         \x20               let {var_name} = {rust_type}::from_value(__raw_{var_name}).unwrap_or_else(|| std::mem::transmute(__raw_{var_name}));\n"
431                    ));
432                }
433                ParamConversion::FName => {
434                    out.push_str(&format!(
435                        "                let {var_name} = {{\n\
436                         \x20                   let mut __buf = [0u8; 8];\n\
437                         \x20                   let mut __written: u32 = 0;\n\
438                         \x20                   rusteal_core::ffi_infallible(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                       8,\n\
444                         \x20                       &mut __written,\n\
445                         \x20                   ));\n\
446                         \x20                   rusteal_core::FNameHandle(u64::from_ne_bytes(__buf))\n\
447                         \x20               }};\n"
448                    ));
449                }
450                ParamConversion::String => {
451                    out.push_str(&format!(
452                        "                let {var_name} = {{\n\
453                         \x20                   let mut __buf = vec![0u8; 260];\n\
454                         \x20                   let mut __written: u32 = 0;\n\
455                         \x20                   let __err = rusteal_core::ffi_dispatch::delegate_read_param(\n\
456                         \x20                       param_info[{i}].1,\n\
457                         \x20                       params,\n\
458                         \x20                       param_info[{i}].0,\n\
459                         \x20                       __buf.as_mut_ptr(),\n\
460                         \x20                       __buf.len() as u32,\n\
461                         \x20                       &mut __written,\n\
462                         \x20                   );\n\
463                         \x20                   if __err == rusteal_core::RustealErrorCode::BufferTooSmall && __written > 0 {{\n\
464                         \x20                       __buf.resize(__written as usize, 0);\n\
465                         \x20                       rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
466                         \x20                           param_info[{i}].1,\n\
467                         \x20                           params,\n\
468                         \x20                           param_info[{i}].0,\n\
469                         \x20                           __buf.as_mut_ptr(),\n\
470                         \x20                           __buf.len() as u32,\n\
471                         \x20                           &mut __written,\n\
472                         \x20                       ));\n\
473                         \x20                   }}\n\
474                         \x20                   if __written >= 4 {{\n\
475                         \x20                       let __slen = u32::from_ne_bytes([__buf[0], __buf[1], __buf[2], __buf[3]]) as usize;\n\
476                         \x20                       String::from_utf8_lossy(&__buf[4..4 + __slen]).into_owned()\n\
477                         \x20                   }} else {{\n\
478                         \x20                       String::new()\n\
479                         \x20                   }}\n\
480                         \x20               }};\n"
481                    ));
482                }
483                ParamConversion::Struct { cpp_name, .. } => {
484                    out.push_str(&format!(
485                        "                let {var_name} = {{\n\
486                         \x20                   let __size = rusteal_core::ffi_dispatch::reflection_get_property_size(param_info[{i}].1) as usize;\n\
487                         \x20                   let mut __buf = vec![0u8; __size];\n\
488                         \x20                   let mut __written: u32 = 0;\n\
489                         \x20                   rusteal_core::ffi_infallible(rusteal_core::ffi_dispatch::delegate_read_param(\n\
490                         \x20                       param_info[{i}].1,\n\
491                         \x20                       params,\n\
492                         \x20                       param_info[{i}].0,\n\
493                         \x20                       __buf.as_mut_ptr(),\n\
494                         \x20                       __size as u32,\n\
495                         \x20                       &mut __written,\n\
496                         \x20                   ));\n\
497                         \x20                   rusteal_core::OwnedStruct::<{cpp_name}>::from_bytes(__buf)\n\
498                         \x20               }};\n"
499                    ));
500                }
501            }
502        }
503
504        let param_names: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
505        let call_args = param_names.join(", ");
506
507        out.push_str(&format!(
508            "                callback({call_args});\n\
509             \x20           }}\n\
510             \x20       }})\n\
511             \x20   }}\n"
512        ));
513
514        out.push_str("}\n\n");
515    }
516}