unity-game-enginewebglzenject

Zenject's LinqExtensions.cs failing in WebGL build


I have a game made in Unity 2021.1.26f1 which relies on Zenject. It runs great in the editor and in a Windows build. When I build for WebGL, however, I receive the following error:

System.Linq.Enumerable.FirstOrDefault[TSource] (System.Collections.Generic.IEnumerable'1[T] source, System.Func'2[T,TResult] predicate) (at <000000000000000000000000000000>:0)

Following that error I located the relevant lines in LinqExtensions.cs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using UnityEngine;
using ModestTree.Util;
using UnityEngine;
using Zenject;


namespace ModestTree
{
 
    public static class LinqExtensions
    {

        public static IEnumerable<T> Yield<T>(this T item)
        {
            
            yield return item;
        }

        // Return the first item when the list is of length one and otherwise returns default
        public static TSource OnlyOrDefault<TSource>(this IEnumerable<TSource> source)
        {
            Assert.IsNotNull(source);
            
            if (source.Count() > 1)
            {
                return default(TSource);

            }

            return source.FirstOrDefault();

        }

I went into the Zenject documentation and found this section:

https://github.com/modesttree/Zenject/blob/master/README.md

Does this work on AOT platforms such as iOS and WebGL? Yes. However, there are a few things that you should be aware of. One of the things that Unity's IL2CPP compiler does is strip out any code that is not used. It calculates what code is used by statically analyzing the code to find usage. This is great, except that this will sometimes strip out methods/types that we don't refer to explicitly (and instead access via reflection instead).

In some versions of Unity, or with some settings applied (eg. a higher level of code stripping), IL2CPP can sometimes strip out the constructors of classes, resulting in errors on IL2CPP platforms. The recommended fix in these cases is to either edit link.xml to force your types to not be stripped (see Unity docs) or to add an '[Inject]' attribute above the constructor. Adding this attribute signals to IL2CPP to not strip out this method.

Unfortunately the use of '[INJECT]' in the code I provided above throws an error and I can't seem to resolve the dependences.

[INJECT]
    public static class LinqExtensions
    {

        public static IEnumerable<T> Yield<T>(this T item)
        {
            
            yield return item;
        }

        // Return the first item when the list is of length one and otherwise returns default
        public static TSource OnlyOrDefault<TSource>(this IEnumerable<TSource> source)
        {
            Assert.IsNotNull(source);
            
            if (source.Count() > 1)
            {
                return default(TSource);

            }

            return source.FirstOrDefault();

        }

Throws this error:

Assets\Plugins\Zenject\Zenject\Source\Internal\LinqExtensions.cs(13,6): error CS0246: The type or namespace name 'INJECTAttribute' could not be found (are you missing a using directive or an assembly reference?)

Assets\Plugins\Zenject\Zenject\Source\Internal\LinqExtensions.cs(13,6): error CS0246: The type or namespace name 'INJECT' could not be found (are you missing a using directive or an assembly reference?)

Any assistance would be great!

I tried to build a game using Zenject to run in WebGL. It is supported so I expected it to run the same as when built for Windows.


Solution

  • Let's say you have a class:

    public class MyClass {
        public MyClass() {
        }
    }
    

    or even

    public class MyClass {
        [Inject]
        public MycClass() {
        }
    }
    

    and then

    Container.Bind<MyClass>();
    

    in your installer.

    In runtime, Zenject uses reflection to find MyClass and instantiate it. The constructor is never called directly. This makes the compiler think that the class is not used by your code and thus the stripping can happen.

    That excerpt from the documentation you quote describes exactly this problem.

    If you suspect that code stripping is the issue, you should add UnityEngine.Scripting.Preserve:

    using UnityEngine.Scripting;
    
    [Preserve]
    public class MyClass {
        [Inject]
        public MycClass() {
        }
    }
    
    

    in front of every class you are injecting.

    OnlyOrDefault itself should not be stripped out as it is refereced in like 10 different places by Zenject code.


    Looking at your error:

    System.Linq.Enumerable.FirstOrDefault[TSource] (System.Collections.Generic.IEnumerable'1[T] source, System.Func'2[T,TResult] predicate) (at <000000000000000000000000000000>:0)
    

    and the code you suspect:

    public static TSource OnlyOrDefault<TSource>(this IEnumerable<TSource> source)
    {
        Assert.IsNotNull(source);
                
        if (source.Count() > 1)
        {
            return default(TSource);
        }
    
        return source.FirstOrDefault();
    }
    

    we can see the following:

    1. Assert.IsNotNull is stripped only if ZEN_STRIP_ASSERTS_IN_BUILDS is set, which is not set by default. So this check should work and fail early if source is null. Even if we strip this check, the point of failure should be in if (source.Count() > 1) as we access source to get the number of elements. So source is likely not null.

    2. Looks like source doesn't have more than 1 item in it, as we were able to reach FirstOrDefault. So it is either one element there or 0 elements there, which should be properly handled by FirstOrDefault.

    Though it still fails and this makes zero sense.

    Unless you know that WebGL release builds are known for providing misleading outputs. Highly likely source is null here and you have to debug one of the callers of OnlyOrDefault to understand why this happens. But even this is just a guess and something else can be the issue.


    What you can try:

    1. If you are using Zenject instead of Extenject, try Extenject. I can confirm that Extenject can be used with WebGL builds. LinqExtensions.cs is in its runtime, code of OnlyOrDefault is the same.

    2. Still check whether ZEN_STRIP_ASSERTS_IN_BUILDS is set in your Project Settings->Player->Scripting define symbols. If this is the case then remove it and test whether the problem is gone or have changed.

    3. Go to File-> Build Settings and enable Development build. This should give you a better stack trace. Potential problem here is that it is not guaranteed to fail the same way. The final code can work differently because of no stripping, extra time needed to process debugging symbols etc. I also remember having some issues with getting complete error outputs from Development WebGL builds in Web Browser logs, but unfortunately I don't really remember what it was exactly.

    4. As you have access to the code, add Debug.Log($"Source: {source}") and also add some logs to the callers of OnlyOrDefault. You can use something like runtime console to see them. You can also create your own UI and output text to it. But don't add it via Zenject, add it to the scene directly. This may not work if the app crashes instead of just reporting the exception and continue working. It is also possible that the issue happens too early and UI is not ready to catch it.

    5. If neither 2. nor 3. allow you to get the output, you can try to use other output channels. E.g. you can try to send it to your JS code or write to IndexedDB. As far as I remember IndexedDB can be accessed via the browser dev tools.

    6. Try to rewrite OnlyOrDefault for testing purposes, e.g.

      if (source == null)
          throw new ArgumentNullException(nameof(source));
      
      var sourceArray = source.ToArray();
      if (sourceArray.Length != 1)
          return default(TSource);
      
      return sourceArray[0];
      

      and see where the error is reported afterwards.