Resolving IL2CPP Build Issues
It’s usually a good idea to test built versions of your application regularly because code will often behave differently than in the editor. Doing this regularly helps to
- easily pinpoint when a bug was introduced
- avoid “surprises” when someone requests a build
This includes early in development; not just when getting close to a release. The importance is magnified when building for IL2CPP because of managed code stripping, especially when reflection is in use. I recently did one of these routine checks for a project I’m currently assigned to at work (early in development). It had been 1-2 weeks since my last check, and let’s just say it resulted in enough content for this blog post 😅 There were three main issues that came up.
Awake vs OnEnable Order
The first was related to editor vs build rather than IL2CPP specifically, because the same issue occurred for Mono. The undefined order of Awake
vs OnEnable
in separate scripts resulted in a NullReferenceException
in the built version of the application, but not in the editor. In this case, Awake was executing before OnEnable in the editor, and vice versa in the build.
// GameObjectA
private void Awake()
{
_a = new A();
}
public void DoSomething()
{
_a.DoSomething(); // exception, _a is null!
}
// GameObjectB
private void OnEnable()
{
gameObjectA.DoSomething();
}
The quick fix in this case was wrapping _a
in a property for lazy initialization instead of in Awake
.
Marshal.SizeOf
The next exception to deal with was
ArgumentException: The t parameter is a generic type.
which was being thrown by a call to Marshal.SizeOf
in the IL2CPP build, but not in the editor or Mono build. The specific line was in a binary reader class:
Marshal.SizeOf<HeaderSegment<TKey>>()
Other calls like Marshal.SizeOf<TKey>()
work fine; it’s just not happy when T
is generic.
I found it mentioned in a 2020 dotnet/runtime GitHub issue where someone commented it’s by design, but of course it doesn’t help explain the difference in behavior between Mono and IL2CPP. There was also a comment about using Unsafe.SizeOf
, instead, so I gave that a try and it worked. Since I didn’t feel like adding another DLL at the moment, I just used the UnsafeUtility
included in the Unity.Collections.LowLevel.Unsafe
namespace, which is just a wrapper for a lot of the System.Runtime.CompilerServices.Unsafe
methods, anyway.
A day or two later I did some research on these two SizeOf
variations, which lead to resolving another related issue. I’ll cover that in the next post.
SQLite Stripping
The final issue only surfaced after I bumped up the managed stripping level to Medium (Minimal and Low were fine). The goal was to get up to the maximum level, High. I got a NullReferenceException
on the following line when trying to do a read.
var columnName = Table.FindColumnWithPropertyName (mem.Member.Name).Name;
FindColumnWithPropertyName
was returning null, so accessing the Name
property resulted in the NRE. So now, I’ll walk you through the process I took to get to the root cause. Here’s an abbreviated class that resembles the table being queried:
public class LineInfo
{
public string LineName { get; set; }
}
Why is FindColumnWithPropertyName returning null?
public Column FindColumnWithPropertyName (string propertyName)
{
var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName);
return exact;
}
Answer: The Columns property is empty here.
Why is the Columns property empty?
Columns
is initialized in the TableMapping
constructor but I noticed GetPublicMembers was returning an empty list.
var members = GetPublicMembers(type);
Why is GetPublicMembers returning an empty list?
private void GetPublicMembers()
{
...
newMembers.AddRange(
from p in ti.DeclaredProperties
where !memberNames.Contains(p.Name) &&
p.CanRead && p.CanWrite &&
p.GetMethod != null && p.SetMethod != null &&
p.GetMethod.IsPublic && p.SetMethod.IsPublic &&
!p.GetMethod.IsStatic && !p.SetMethod.IsStatic
select p);
...
}
Reflection…of course. This query gets all public instance properties that have both a getter and setter. But it turns out p.CanWrite
was returning false and p.SetMethod
was null, meaning there’s no setter.
Why is the setter missing?
This was a clear indication that it must being getting stripped, so I added a PreserveAttribute on the class, but it didn’t work. So then I tried applying the attribute to each property instead, and that did the trick. I thought applying it to the class meant it preserves all members, but apparently I was wrong. I found a GitHub issue that mentioned this exact stripping side effect: “IL2CPP Code stripping makes properties unwritable”.
public class LineInfo
{
[UnityEngine.Scripting.Preserve]
public string LineName { get; set; }
}
Conclusion
In the end, the goal of having a functional IL2CPP build of the application with managed stripping level set to High
was achieved. Ensuring a successful build in CI is a good start, but actually running that build can save time down the road. This can even be Play Mode tests configured to run on the target platform.
Comments