IL2CPP and Managed Code Stripping¶
OxHeart works on IL2CPP platforms (iOS, Android, consoles, Windows/macOS/Linux IL2CPP — WebGL is not supported, see the Platform Support table in README.md), but because the engine relies on reflection for service creation and discovery, managed code stripping needs to be understood by every project that builds with IL2CPP.
What the Engine Does via Reflection¶
| Mechanism | Reflection used |
|---|---|
| Engine service creation | ConfigurableServiceInitializer resolves constructors via reflection and invokes them |
IServiceRegistrar auto-discovery |
All loaded assemblies are scanned; registrars are created with Activator.CreateInstance |
ServiceRegistrationInfo custom services |
Interface and implementation types are resolved from string type names via Type.GetType |
At Managed Stripping Level Medium or High, the Unity linker removes types and
members it cannot prove are used. Reflection call paths are invisible to it, so without
protection the linker can strip service constructors or whole classes, and the engine
fails at runtime (services come back null, registrars never run).
What the Engine Already Protects (you do not need to do this)¶
OxHeart imports as a .unitypackage that lands at Assets/OxHeart/, and it ships a
link.xml at Assets/OxHeart/link.xml, which preserves all four runtime assemblies:
<linker>
<assembly fullname="OxHeart-Core" preserve="all" />
<assembly fullname="OxHeart-Runtime" preserve="all" />
<assembly fullname="OxHeart-Helpers" preserve="all" />
<assembly fullname="OxHeart-Utility" preserve="all" />
</linker>
Unity honors any link.xml located anywhere under the project's Assets/ folder, so
because OxHeart installs under Assets/, engine-side types are safe at any stripping level
with no action on your part.
If you relocate OxHeart into
Packages/. Unity does not scan alink.xmlat a UPM package root — package-root link files are intentionally ignored, so this bundled file only works while OxHeart lives underAssets/(its default). If you move the folder into your project'sPackages/directory, copy the four<assembly>entries above into alink.xmlsomewhere under your ownAssets/, or the linker will strip the engine's reflection-only service constructors.
What Your Project Must Do¶
The shipped link.xml only covers OxHeart's assemblies. Game-side types that are
reached only through reflection need protection in your project:
1. IServiceRegistrar implementations¶
Registrars are discovered by assembly scanning and instantiated via reflection — nothing
references them statically. Add [Preserve]:
using UnityEngine.Scripting;
[Preserve]
public class GameServiceRegistrar : IServiceRegistrar
{
public int Priority => 0;
public void RegisterServices(IServiceLocator serviceLocator, ServiceConfiguration serviceConfig)
{
// ...
}
}
[Preserve] on the class keeps the class, its constructors, and its methods.
2. Custom services registered via ServiceRegistrationInfo (inspector-driven)¶
These are resolved from assembly-qualified string type names, which the linker cannot
see at all. Either add [Preserve] to both the interface and the implementation, or add
them to a link.xml in your project's Assets/ folder:
<linker>
<assembly fullname="MyGame">
<type fullname="MyGame.Services.IWalletService" preserve="all" />
<type fullname="MyGame.Services.WalletService" preserve="all" />
</assembly>
</linker>
3. Types you serialize with the save system¶
JsonDataSerializer can construct objects via Activator.CreateInstance<T>(). Your
PersistableData subclasses are normally referenced statically by your own code, but if
a data class is only ever created through deserialization, give it [Preserve] too.
Auto-properties can lose data silently — read this before shipping¶
Stripping applies to members, not just whole types, and the save system reads your data by reflection. A public auto-property whose getter is never called anywhere in your own code — common for fields that are written during play and only ever read back by the loader — can be removed by the linker even though its class survives. Newtonsoft then never sees the member, so the key is simply absent from the save file: no exception, no warning, no quarantine. The data is gone and nothing tells you.
public class PlayerSave : PersistableData
{
public override string DataId => "player";
public int Coins { get; set; } // ← at risk if your code never reads Coins
}
Verified on Unity 6000.0, IL2CPP, Managed Stripping High, with the data class in a game
assembly (not preserved by OxHeart's link.xml):
| What you write | Survives High stripping |
|---|---|
public int Coins; (public field) |
✅ |
[SerializeField] private int _coins; + accessors |
✅ recommended |
public int Coins { get; set; }, getter called somewhere in your code |
✅ |
public int Coins { get; set; }, getter only ever called by the serializer |
❌ key dropped from the save |
[Preserve] on the class is not enough for this case. It keeps the type and its
constructors, and the object still serializes — but the auto-property is still stripped and
its key still disappears. Two things do work:
- Use a
[SerializeField]private backing field instead of an auto-property. The engine's contract resolver picks these up deliberately (including private fields declared on base classes), and they survive at any stripping level. This is the recommended shape for save data, and it matches how you already write serialized Unity fields. - Root the type in your own
link.xml— anylink.xmlunderAssets/is honored:
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.Saves.PlayerSave" preserve="all" />
</assembly>
</linker>
If you already ship auto-property save types and cannot change their shape, use option 2 —
changing the member shape would alter the serialized key names (Coins → _coins) and needs
a SchemaVersion bump plus a migration step, exactly like any other field rename.
Verifying Your Build¶
- Set Project Settings → Player → Managed Stripping Level to the level you ship with (test High if unsure — it is the strictest).
- Make an IL2CPP build for your target platform.
- Run it and confirm:
- The engine boots (no
ServiceNotFoundExceptionfor enabled services). - Your
IServiceRegistrarlog lines appear (External registrar executed: ...). - A save/load round-trip works if you use the save system.
If a service disappears only in IL2CPP builds (works in the Editor and Mono builds), stripping is almost always the cause — check the linker log in the build output for the type name, then apply one of the protections above.
When a service class survives stripping but its constructors do not, the engine logs an
explicit error at initialization: Service type '…' has no public constructors — likely
stripped by the managed linker. That message in the Player log is a direct signal to
add or fix a link.xml entry for the named assembly.
See Also¶
- Unity Manual — Managed code stripping
Samples/Scripts/09_GameServiceRegistrarSample.cs— registrar sample with[Preserve]applied- ServiceLocator.md, ServiceDependencies.md