Skip to main content

il2cpp_bridge_rs/structs/core/hierarchy/
class.rs

1//! Class metadata wrapper and lookup helpers.
2
3use crate::api::{self, cache};
4use crate::structs::collections::Il2cppArray;
5use crate::structs::core::{Field, Method, Property};
6use std::ffi::c_void;
7use std::sync::Arc;
8
9use super::object::Object;
10
11/// Hydrated IL2CPP class metadata.
12///
13/// A `Class` is the main entry point for metadata-driven workflows after
14/// fetching an [`crate::structs::Assembly`] from the cache. From here you can
15/// locate methods, fields, properties, create objects, or search for live
16/// instances in the scene.
17#[derive(Debug, Clone)]
18pub struct Class {
19    /// Pointer to the internal IL2CPP class structure
20    pub address: *mut c_void,
21    /// Pointer to the image defining this class
22    pub image: *mut c_void,
23    /// Metadata token for this class
24    pub token: u32,
25    /// Name of the class
26    pub name: String,
27    /// Name of the parent class (if any)
28    pub parent: Option<String>,
29    /// Namespace of the class
30    pub namespace: String,
31    /// Whether the class is an enum
32    pub is_enum: bool,
33    /// Whether the class is generic
34    pub is_generic: bool,
35    /// Whether the class is an inflated generic type
36    pub is_inflated: bool,
37    /// Whether the class is an interface
38    pub is_interface: bool,
39    /// Whether the class is abstract
40    pub is_abstract: bool,
41    /// Whether the class is blittable
42    pub is_blittable: bool,
43    /// Whether the class is a value type
44    pub is_valuetype: bool,
45    /// Flags associated with the class
46    pub flags: i32,
47    /// Rank of the class (if array)
48    pub rank: i32,
49    /// Size of an instance of this class
50    pub instance_size: i32,
51    /// Size of array elements (if this is an array class)
52    pub array_element_size: i32,
53    /// Number of fields in this class
54    pub num_fields_count: usize,
55    /// Enum base type pointer (if this is an enum)
56    pub enum_basetype: *mut c_void,
57    /// Pointer to static field data
58    pub static_field_data: *mut c_void,
59    /// Name of the assembly defining this class
60    pub assembly_name: String,
61    /// Assembly that this class belongs to
62    pub assembly: Option<std::sync::Arc<crate::structs::Assembly>>,
63    /// List of fields in this class
64    pub fields: Vec<Field>,
65
66    /// List of methods in this class
67    pub methods: Vec<Method>,
68    /// List of properties in this class
69    pub properties: Vec<Property>,
70    /// List of interfaces implemented by this class
71    pub interfaces: Vec<*mut c_void>,
72    /// List of nested types within this class
73    pub nested_types: Vec<*mut c_void>,
74    /// Element class (if this is an array class)
75    pub element_class: *mut c_void, // For arrays
76    /// Declaring type (if this is a nested class)
77    pub declaring_type: *mut c_void, // For nested classes
78    /// Pointer to the Type object representing this class
79    pub ty: *mut c_void,
80    /// Pointer to the System.Type object
81    pub object: *mut c_void,
82}
83
84unsafe impl Send for Class {}
85unsafe impl Sync for Class {}
86
87/// Selector trait used by [`Class::method`] and [`crate::structs::Object::method`].
88pub trait MethodSelector {
89    /// Returns true if the method matches the selector criteria
90    fn matches(&self, method: &Method) -> bool;
91}
92
93/// Selects a method by name.
94impl MethodSelector for &str {
95    fn matches(&self, method: &Method) -> bool {
96        method.name == *self
97    }
98}
99
100/// Selects a method by name and parameter type names.
101impl MethodSelector for (&str, &[&str]) {
102    fn matches(&self, method: &Method) -> bool {
103        if method.name != self.0 {
104            return false;
105        }
106
107        if method.args.len() != self.1.len() {
108            return false;
109        }
110
111        for (i, param_name) in self.1.iter().enumerate() {
112            let arg_type = &method.args[i].type_info;
113            if arg_type.name != *param_name && arg_type.cpp_name() != *param_name {
114                return false;
115            }
116        }
117
118        true
119    }
120}
121
122/// Selects a method by name and parameter type names using a fixed-size array.
123impl<const N: usize> MethodSelector for (&str, [&str; N]) {
124    fn matches(&self, method: &Method) -> bool {
125        (self.0, self.1.as_slice()).matches(method)
126    }
127}
128
129/// Selects a method by name and parameter count.
130impl MethodSelector for (&str, usize) {
131    fn matches(&self, method: &Method) -> bool {
132        method.name == self.0 && method.args.len() == self.1
133    }
134}
135
136impl std::fmt::Display for Class {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        write!(f, "{}", self.dump_string())
139    }
140}
141
142impl Class {
143    /// Generates a string representation of the class, including fields and methods
144    ///
145    /// # Returns
146    /// * `String` - The formatted string dump of the class
147    pub fn dump_string(&self) -> String {
148        let mut s = String::new();
149
150        // Header comment: // Namespace: <ns> — <dll>
151        let ns_part = if !self.namespace.is_empty() {
152            self.namespace.clone()
153        } else {
154            String::new()
155        };
156
157        let dll_part = if !self.assembly_name.is_empty() {
158            if self.assembly_name.ends_with(".dll") {
159                self.assembly_name.clone()
160            } else {
161                format!("{}.dll", self.assembly_name)
162            }
163        } else {
164            String::new()
165        };
166
167        match (!ns_part.is_empty(), !dll_part.is_empty()) {
168            (true, true) => s.push_str(&format!(
169                "// Namespace: {} \u{2014} {}\n",
170                ns_part, dll_part
171            )),
172            (true, false) => s.push_str(&format!("// Namespace: {}\n", ns_part)),
173            (false, true) => s.push_str(&format!("// Image: {}\n", dll_part)),
174            (false, false) => {}
175        }
176
177        let abstract_kw = if self.is_abstract && !self.is_interface {
178            "abstract "
179        } else {
180            ""
181        };
182
183        let type_kw = if self.is_enum {
184            "enum"
185        } else if self.is_interface {
186            "interface"
187        } else if self.is_valuetype {
188            "struct"
189        } else {
190            "class"
191        };
192
193        // TypeDefIndex: lower 24 bits of the metadata token
194        let typedef_index = self.token & 0x00FF_FFFF;
195
196        let inheritance = if let Some(parent) = &self.parent {
197            format!(" : {}", parent)
198        } else {
199            String::new()
200        };
201
202        if self.is_enum {
203            let underlying_type = self.enum_underlying_type();
204            s.push_str(&format!(
205                "public {} {} : {} // TypeDefIndex: {}\n{{\n",
206                type_kw, self.name, underlying_type, typedef_index
207            ));
208
209            let mut has_variants = false;
210            for field in self
211                .fields
212                .iter()
213                .filter(|field| field.is_static && field.name != "value__")
214            {
215                has_variants = true;
216                if let Some(value) = Self::enum_value_string(field, underlying_type) {
217                    s.push_str(&format!("    {} = {},\n", field.name, value));
218                } else {
219                    s.push_str(&format!("    {},\n", field.name));
220                }
221            }
222
223            if !has_variants {
224                s.push_str("    // Empty enum\n");
225            }
226
227            s.push_str("}\n");
228            return s;
229        }
230
231        s.push_str(&format!(
232            "public {}{} {}{} // TypeDefIndex: {}\n{{\n",
233            abstract_kw, type_kw, self.name, inheritance, typedef_index
234        ));
235
236        // Fields section
237        if !self.fields.is_empty() {
238            s.push_str("    // Fields\n");
239            for field in &self.fields {
240                s.push_str(&format!("    {}\n", field));
241            }
242        }
243
244        // Properties section
245        if !self.properties.is_empty() {
246            if !self.fields.is_empty() {
247                s.push('\n');
248            }
249            s.push_str("    // Properties\n");
250            for prop in &self.properties {
251                s.push_str(&format!("    {}\n", prop));
252            }
253        }
254
255        // Split methods into constructors vs regular methods
256        let constructors: Vec<_> = self
257            .methods
258            .iter()
259            .filter(|m| m.name == ".ctor" || m.name == ".cctor")
260            .collect();
261
262        let regular_methods: Vec<_> = self
263            .methods
264            .iter()
265            .filter(|m| m.name != ".ctor" && m.name != ".cctor")
266            .collect();
267
268        // Constructors section
269        if !constructors.is_empty() {
270            if !self.fields.is_empty() || !self.properties.is_empty() {
271                s.push('\n');
272            }
273            s.push_str("    // Constructors\n");
274            for method in &constructors {
275                for line in method.to_string().lines() {
276                    s.push_str(&format!("    {}\n", line));
277                }
278                s.push('\n');
279            }
280        }
281
282        // Methods section
283        if !regular_methods.is_empty() {
284            if constructors.is_empty() && (!self.fields.is_empty() || !self.properties.is_empty()) {
285                s.push('\n');
286            }
287            s.push_str("    // Methods\n");
288            for method in &regular_methods {
289                for line in method.to_string().lines() {
290                    s.push_str(&format!("    {}\n", line));
291                }
292                s.push('\n');
293            }
294        }
295
296        s.push_str("}\n");
297        s
298    }
299
300    /// Creates a managed instance using `System.Activator.CreateInstance`.
301    ///
302    /// Use this when you want constructor semantics rather than raw allocation.
303    pub fn create_instance(&self) -> Result<Object, String> {
304        unsafe {
305            if self.ty.is_null() {
306                return Err(format!("Could not get Type for class '{}'", self.name));
307            }
308
309            let corlib = cache::mscorlib();
310            let activator_class = corlib
311                .class("System.Activator")
312                .ok_or_else(|| "Could not find System.Activator class".to_string())?;
313
314            let method = activator_class
315                .method(("CreateInstance", ["System.Type"]))
316                .ok_or_else(|| "Could not find CreateInstance(Type) method".to_string())?;
317
318            if self.object.is_null() {
319                return Err("Could not get Type object".to_string());
320            }
321
322            match method.call::<Object>(&[self.object]) {
323                Ok(result_handle) => {
324                    if result_handle.ptr.is_null() {
325                        return Err("CreateInstance returned null".to_string());
326                    }
327                    Ok(result_handle)
328                }
329                Err(e) => Err(format!("CreateInstance invocation failed: {}", e)),
330            }
331        }
332    }
333
334    /// Allocates a new object of this class via `il2cpp_object_new`.
335    ///
336    /// This is the low-level allocation path and does not imply constructor
337    /// execution.
338    pub fn new_object(&self) -> Result<Object, String> {
339        unsafe {
340            let obj = Object::from_ptr(api::object_new(self.address));
341            if obj.ptr.is_null() {
342                return Err("Could not create object".to_string());
343            }
344            Ok(obj)
345        }
346    }
347
348    /// Creates a `ScriptableObject` instance of this class.
349    pub fn create_scriptable_instance(&self) -> Result<Object, String> {
350        unsafe {
351            if self.object.is_null() {
352                return Err(format!(
353                    "Could not get Type object for class '{}'",
354                    self.name
355                ));
356            }
357
358            let core_module = cache::coremodule();
359            let so_class = core_module
360                .class("UnityEngine.ScriptableObject")
361                .ok_or("Class 'UnityEngine.ScriptableObject' not found")?;
362
363            let method = so_class
364                .method(("CreateInstance", ["System.Type"]))
365                .ok_or("Method 'CreateInstance' not found in ScriptableObject")?;
366
367            let result = method.call::<Object>(&[self.object])?;
368
369            if result.ptr.is_null() {
370                return Err("ScriptableObject.CreateInstance returned null".to_string());
371            }
372
373            Ok(result)
374        }
375    }
376
377    /// Finds live objects of this type in the scene.
378    ///
379    /// This wraps `UnityEngine.Object.FindObjectsOfType` and returns bound
380    /// [`Object`] wrappers for each match.
381    pub fn find_objects_of_type(&self, include_inactive: bool) -> Vec<Object> {
382        if self.object.is_null() {
383            return Vec::new();
384        }
385
386        let type_object = self.object;
387
388        let unity_engine_core = cache::coremodule();
389        let unity_object = match unity_engine_core.class("UnityEngine.Object") {
390            Some(c) => c,
391            None => return Vec::new(),
392        };
393
394        let method = unity_object
395            .method(("FindObjectsOfType", ["System.Type", "System.Boolean"]))
396            .or_else(|| unity_object.method(("FindObjectsOfType", ["System.Type"])));
397
398        match method {
399            Some(method) => unsafe {
400                let params = if method.args.len() == 2 {
401                    vec![type_object, &include_inactive as *const bool as *mut c_void]
402                } else {
403                    vec![type_object]
404                };
405
406                match method.call::<*mut Il2cppArray<Object>>(&params) {
407                    Ok(array) => {
408                        if array.is_null() {
409                            return Vec::new();
410                        }
411                        (*array).to_vector().into_iter().collect()
412                    }
413                    Err(_) => Vec::new(),
414                }
415            },
416            None => Vec::new(),
417        }
418    }
419
420    /// Finds a method in this class or its parent chain.
421    ///
422    /// Selectors may match by name, by name plus parameter type names, or by
423    /// name plus parameter count.
424    pub fn method<S: MethodSelector>(&self, selector: S) -> Option<Method> {
425        if let Some(method) = self.methods.iter().find(|m| selector.matches(m)).cloned() {
426            return Some(method);
427        }
428
429        if self.parent.is_some() {
430            return self
431                .get_parent_class()
432                .and_then(|parent| parent.method(selector));
433        }
434
435        None
436    }
437
438    /// Finds a field in this class or its parent chain.
439    pub fn field(&self, name: &str) -> Option<Field> {
440        if let Some(field) = self.fields.iter().find(|f| f.name == name).cloned() {
441            return Some(field);
442        }
443
444        if self.parent.is_some() {
445            return self
446                .get_parent_class()
447                .and_then(|parent| parent.field(name));
448        }
449
450        None
451    }
452
453    /// Finds a property in this class or its parent chain.
454    pub fn property(&self, name: &str) -> Option<Property> {
455        if let Some(prop) = self.properties.iter().find(|p| p.name == name).cloned() {
456            return Some(prop);
457        }
458
459        if self.parent.is_some() {
460            return self
461                .get_parent_class()
462                .and_then(|parent| parent.property(name));
463        }
464
465        None
466    }
467
468    /// Retrieves the parent class by hydrating it from the parent pointer
469    fn get_parent_class(&self) -> Option<Class> {
470        unsafe {
471            let parent_ptr = crate::api::class_get_parent(self.address);
472            if parent_ptr.is_null() {
473                return None;
474            }
475            cache::class_from_ptr(parent_ptr)
476        }
477    }
478
479    /// Initializes the class if it hasn't been initialized yet
480    ///
481    /// Calls `il2cpp_runtime_class_init`.
482    pub fn init(&self) {
483        unsafe { api::runtime_class_init(self.address) }
484    }
485
486    /// Checks if the class has references
487    ///
488    /// # Returns
489    /// * `bool` - True if class has references
490    pub fn has_references(&self) -> bool {
491        unsafe { api::class_has_references(self.address) }
492    }
493
494    /// Checks if this class is assignable from another class
495    ///
496    /// # Arguments
497    /// * `other` - The other class to check against
498    ///
499    /// # Returns
500    /// * `bool` - True if assignable
501    pub fn is_assignable_from(&self, other: &Class) -> bool {
502        unsafe { api::class_is_assignable_from(self.address, other.address) }
503    }
504
505    /// Checks if this class is a subclass of another class
506    ///
507    /// # Arguments
508    /// * `other` - The prospective parent class
509    /// * `check_interfaces` - Whether to check interfaces as well
510    ///
511    /// # Returns
512    /// * `bool` - True if it is a subclass
513    pub fn is_subclass_of(&self, other: &Class, check_interfaces: bool) -> bool {
514        unsafe { api::class_is_subclass_of(self.address, other.address, check_interfaces) }
515    }
516
517    /// Gets the size of the value type
518    ///
519    /// # Returns
520    /// * `i32` - The size of the value type in bytes
521    pub fn get_value_size(&self) -> i32 {
522        unsafe { api::class_value_size(self.address, std::ptr::null_mut()) }
523    }
524
525    /// Checks if this class has the specified parent class (wrapper for il2cpp_class_has_parent)
526    ///
527    /// # Arguments
528    /// * `parent` - The parent class to check for
529    ///
530    /// # Returns
531    /// * `bool` - True if parent exists in the inheritance hierarchy
532    pub fn has_parent(&self, parent: &Class) -> bool {
533        unsafe { api::class_has_parent(self.address, parent.address) }
534    }
535
536    /// Checks if this class has the specified attribute (wrapper for il2cpp_class_has_attribute)
537    ///
538    /// # Arguments
539    /// * `attr_class` - The attribute class to check for
540    ///
541    /// # Returns
542    /// * `bool` - True if the class has the attribute
543    pub fn has_attribute(&self, attr_class: &Class) -> bool {
544        unsafe { api::class_has_attribute(self.address, attr_class.address) }
545    }
546
547    /// Inflates a generic class with the specified type arguments to create a concrete generic type.
548    ///
549    /// # Arguments
550    /// * `classes` - Slice of classes to use as type arguments
551    ///
552    /// # Returns
553    /// * `Result<Arc<Class>, String>` - The inflated concrete class
554    pub fn inflate(&self, classes: &[&Class]) -> Result<Arc<Class>, String> {
555        unsafe {
556            if !self.is_generic {
557                return Err(format!(
558                    "Class '{}' is not a generic type definition and cannot be inflated",
559                    self.name
560                ));
561            }
562
563            if self.object.is_null() {
564                return Err(format!(
565                    "Could not get Type object for class '{}'",
566                    self.name
567                ));
568            }
569
570            if classes.is_empty() {
571                return Err("No type arguments provided".to_string());
572            }
573
574            let mut type_args = Vec::with_capacity(classes.len());
575            for (i, cls) in classes.iter().enumerate() {
576                if cls.object.is_null() {
577                    return Err(format!(
578                        "Class '{}' (arg {}) has no Type object",
579                        cls.name, i
580                    ));
581                }
582                type_args.push(cls.object);
583            }
584
585            let corlib = cache::mscorlib();
586            let type_class = corlib
587                .class("System.Type")
588                .ok_or_else(|| "Could not find System.Type class".to_string())?;
589
590            let type_array = Il2cppArray::<*mut c_void>::new(&type_class, type_args.len());
591            if type_array.is_null() {
592                return Err("Could not create Type[] array".to_string());
593            }
594
595            let array_ref = &mut *type_array;
596            for (i, &type_arg) in type_args.iter().enumerate() {
597                array_ref.set(i, type_arg);
598            }
599
600            let type_obj = Object::from_ptr(self.object);
601
602            let make_generic_type_method = type_obj
603                .method(("MakeGenericType", ["System.Type[]"]))
604                .ok_or_else(|| "Could not find MakeGenericType(Type[]) method".to_string())?;
605
606            let inflated_type_obj =
607                make_generic_type_method.call::<*mut c_void>(&[type_array as *mut c_void])?;
608
609            if inflated_type_obj.is_null() {
610                return Err("MakeGenericType returned null".to_string());
611            }
612
613            let inflated_class_ptr = api::class_from_system_type(inflated_type_obj);
614            if inflated_class_ptr.is_null() {
615                return Err("Could not get Il2CppClass from inflated Type".to_string());
616            }
617
618            api::runtime_class_init(inflated_class_ptr);
619
620            cache::class_from_ptr(inflated_class_ptr)
621                .map(Arc::new)
622                .ok_or_else(|| {
623                    "Could not convert inflated class pointer to Class struct".to_string()
624                })
625        }
626    }
627
628    /// Gets the underlying type of an enum
629    fn enum_underlying_type(&self) -> &'static str {
630        let type_name = self
631            .fields
632            .iter()
633            .find(|field| field.name == "value__")
634            .map(|field| field.type_info.name.as_str())
635            .unwrap_or("System.Int32");
636
637        match type_name {
638            "System.Byte" | "Byte" | "byte" => "byte",
639            "System.SByte" | "SByte" | "sbyte" => "sbyte",
640            "System.Int16" | "Int16" | "short" => "short",
641            "System.UInt16" | "UInt16" | "ushort" => "ushort",
642            "System.Int32" | "Int32" | "int" => "int",
643            "System.UInt32" | "UInt32" | "uint" => "uint",
644            "System.Int64" | "Int64" | "long" => "long",
645            "System.UInt64" | "UInt64" | "ulong" => "ulong",
646            _ => "int",
647        }
648    }
649
650    /// Gets the value of an enum field as a string
651    fn enum_value_string(field: &Field, underlying_type: &str) -> Option<String> {
652        unsafe {
653            match underlying_type {
654                "byte" => field.get_value::<u8>().ok().map(|value| value.to_string()),
655                "sbyte" => field.get_value::<i8>().ok().map(|value| value.to_string()),
656                "short" => field.get_value::<i16>().ok().map(|value| value.to_string()),
657                "ushort" => field.get_value::<u16>().ok().map(|value| value.to_string()),
658                "int" => field.get_value::<i32>().ok().map(|value| value.to_string()),
659                "uint" => field.get_value::<u32>().ok().map(|value| value.to_string()),
660                "long" => field.get_value::<i64>().ok().map(|value| value.to_string()),
661                "ulong" => field.get_value::<u64>().ok().map(|value| value.to_string()),
662                _ => None,
663            }
664        }
665    }
666}