knah / Il2CppAssemblyUnhollower

A tool to generate Managed->IL2CPP proxy assemblies
GNU Lesser General Public License v3.0
503 stars 87 forks source link

Implement support for Unity (de)serializing fields of injected MonoBehaviors #53

Open knah opened 3 years ago

knah commented 3 years ago

Right now injected classes have no (extra) fields. This is largely due to the fact that field storage is allocated in the managed-side object, with the il2cpp side object only holding a gchandle to the managed one. This presents an issue with cloning gameobjects with components of injected types, and loading them from assetbundles - Unity can't access managed fields, and, as such, they are not copied during instantiation/bundle load.

Preliminary research indicates that Unity uses field offsets and (optionally) il2cpp_gc_wbarrier_set_field to directly access fields on il2cpp objects - this means that the usual approach of "hook set_field so it redirects to the managed field" is likely non-viable.

This means that an alternative approach for handling fields on injected types is necessary. I have a few approaches that I'd consider viable:

  1. Create an annotation for auto-properties alongside the lines of RedirectToNativeField. When performing class injection, a field in il2cpp class would be created for every property marked with that attribute, and its getter and setter would be patched to access the field in native object.
    • This might take some effort to interop nicely with defining those components in the editor. SerializeField and FormerlySerializedAsAttribute attributes can be applied to auto-property backing fields to make them serialized and have the appropriate name. Alternatively, injected fields can be made to match property backing field name.
    • Alternatively, the separate editor version of the component can have plain fields with correct names - removing the { get; set; } block is relatively easy.
    • Patching property getter/setter might require special magic tricks on certain runtimes (see Harmony/netcore - I believe those issues are resolved currently).
  2. Create a wrapper type for native fields, such as NativeFieldWrapper<T>. Fields of this type can be declared in the injected class, and class injector would generate a matching field in the il2cpp class for every field of that type. The wrapper would provide access to those il2cpp fields.
    • It would have been nice if C# had property delegation like Kotlin - this would look way cleaner.
    • Interop with defining those fields in the editor would be even more challenging, unless the "separate script code" approach is used.
  3. Use ISerializationCallbackReceiver to manually serialize/deserialize state to a single hardcoded string/byte[] field. This would likely work fairly well with editor interop, but might not be compatible with older unity versions if they're missing this interface. Additionally, this would require interface implementation to be supported by class injection.
  4. Maybe something else I didn't think about?

A thing to consider for injected fields would be GC integration - IL2CPP can use either boehm (which requires no special GC magic), or something sgen-like which would likely require correct GC descriptors on classes to tell it where reference-typed fields are located in the object.