3 minute read

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