Full Generic Sharing allows you to write code that’s more expressive and easier to test. It not only eliminates a whole class of scripting errors detectable at runtime, but ensures that code on platforms like mobile devices and consoles behave more predictably. Read on to learn how.
Generics are powerful features of C#. They allow code to express behaviors independently from types. As developers, we expect List<string> to behave just like List<int> or List<T>, where T is any type.
For years, IL2CPP has used generic sharing in cases where T is a reference type (string, object, etc.) This works well because reference types in C# are always represented by a pointer, so the size and implementation of List<string> will match the size and implementation of List<object>. But what happens if T is an int (four bytes) on a 64-bit system (where pointers are eight bytes)? IL2CPP must generate special code for List<int>, List<double>, List<MyValueType>, and so on.
That’s why in Unity 2022.1, IL2CPP already generates special code that can handle List<T> for any T, reference, or value type. This technology is called Full Generic Sharing.
While generic virtual methods are expressive features of C# that work well with just-in-time (JIT) compilation, they are difficult to implement for ahead-of-time (AOT) compilation cases, such as IL2CPP. That’s where Full Generic Sharing comes in.
Let’s take a look at a generic virtual method example from the Unity Manual:
This code demonstrates the expressiveness of generic virtual methods. In other words, we can send data of any type (the “message”) from any class that implements the IManager interface to any class that implements the IReceiver interface. With IL2CPP in Unity 2021.2, this seemingly simple code does not work. At runtime, the following error appears in the player log:
Let’s unpack this error.
Because the call to Send Message in the Start method occurs through an interface (IManager, that is the “virtual” part of generic virtual), IL2CPP doesn’t detect what method will be called at runtime when the code is compiled.
You might be wondering: Why can’t IL2CPP figure this out? Well, it can! It’s possible for IL2CPP to search all of the code available at compile time and determine the places where this call might end up. But this search is expensive; it takes precious time while you wait for the project to build, and it can cause IL2CPP to generate extra code that will never be called, increasing the final executable size.
That --generic-virtual-method-iterations argument (mentioned in the error message) permits you to tell IL2CPP how much time it should spend searching. For a JIT compiler, this kind of generic virtual method call is really straightforward. It can “see” the target method at runtime and do the right thing. In Unity 2022.1, IL2CPP has learned the same trick. It now generates a new, special version of SendMessage – the fully shared version.
This will work regardless of the T, reference, or value type, meaning that if IL2CPP cannot see what the target method should be at compile time, it will call this fully shared version instead. The C# code is equally expressive, works at runtime, and compiles just as fast.
The Full Generic Sharing technology is incredibly useful in the way that it enables code on AOT platforms to behave much more like code on JIT platforms. This leads to fewer surprises at runtime.
It turns out these ExecutionEngineException errors show up in other cases as well. Whenever IL2CPP fails to determine what code to run, this error can occur. We often see this in serializers, where some new serialized data deserializes to a type that IL2CPP cannot surmise. But in Unity 2022.1, IL2CPP no longer produces an ExecutionEngineException, eliminating a whole class of errors that are difficult to rectify.
Also consider that some code uses nested recursive generic types. Seeing as IL2CPP can continue processing these types at compile time infinitely, we have to impose a limit on how much time the build process should take.
IL2CPP used to produce the following error when your code needed some of those deeply nested types at runtime: “IL2CPP encountered a managed type that it cannot convert ahead of time. The type uses generic or array types, which are nested beyond the maximum depth that can be converted.” Today, Full Generic Sharing allows IL2CPP to use an implementation that never fails, so you will no longer encounter this error message.
Imagine you have a project that you want to resize and make as small as possible. While you might have executable code for List<int>, List<double>, and List<string>, you might also want to rethink balancing so many different implementations.
Wouldn’t it be great to have just one, fully shared generic implementation for any List<T>? Well, check out the IL2CPP Code Generation option “Faster (smaller) builds” in Player Settings. It leverages Full Generic Sharing to give you the smallest possible executable code with the fastest possible build time – not to mention, quick incremental builds. If you decide to use List<DateTime> (or any other T) in the project, IL2CPP no longer needs to generate or compile new code for that implementation.
If you want to start writing code that leverages IL2CPP Full Generic Sharing, simply download Unity 2022.1 beta from the Unity Hub or on our download page. Remember, the beta is not intended for use in production-stage projects, so be sure to back up your existing projects.
That said, we would love to know how Unity 2022.1 works for you. Please visit the beta forum to leave us your thoughts. We would greatly appreciate your feedback on Full Generic Sharing or any other feature you’re currently working with. As a bonus for your involvement, each original and reproducible bug report will boost your chances of winning one of our sweepstakes prizes. Find the details in the beta release blog post.