2

I have changed my serialization to DataContracts but now I am having problem with a specific class. It works fine on my Mac, but not on my android devices when built using IL2CPP. The thread stops at the writeObject function. My three classes related to the error:

[DataContract]
[KnownType(typeof(TaskIdentifier))]
[KnownType(typeof(TraceableTaskItem))]
[KnownType(typeof(List<TraceableTaskItem>))]
public class TraceableTaskContainer
{
    [DataMember]
    protected TaskIdentifier _taskIdent;

    [DataMember]
    protected List<TraceableTaskItem> _lNotAccomplishedTaskItems = new List<TraceableTaskItem>();

//.....
}
[DataContract]
[KnownType(typeof(DateTime))]
[KnownType(typeof(ItemReviewStage))]
public class TraceableTaskItem : GenericTaskItem, IEquatable<TraceableTaskItem>, IComparable<TraceableTaskItem>
{
    [DataMember]
    public string sDisplayTextInTraceableTaskReport;

    [DataMember]
    protected DateTime NextReviewDate;

    [DataMember] //ItemReviewStage is a enum
    protected ItemReviewStage reviewStage = ItemReviewStage.NewTask;

   
    public TraceableTaskItem() //important to deserialize old classes, do not remove it
    {

    }
//....
}
[DataContract]
//[KnownType(typeof(List<bool>))]
abstract public class GenericTaskItem
{
    [DataMember]
    public string sItemID = "";

    //[DataMember]
    protected List<bool> lTimesAnsweredCorrectly = new List<bool>();

    protected List<List<string>> llWrongAnswers = new List<List<string>>();

//...
}

The code works with the commented lines above. But as soon as I uncomment DataMember on the lTimesAnsweredCorrely and with or without uncommenting the equivalent KnownType line (I have tested both), the code stops working on my mobile devices. Any idea how can I fix this?

Exception:

"System.Reflection.TargetInvocationException: 
Exception has been thrown by the target of an invocation. 
---> System.ExecutionEngineException: Attempting to call method \'System.Runtime.Serialization.XmlObjectSerializerWriteContext::
IncrementCollectionCountGeneric<System.Boolean>\' 
for which no ahead of time (AOT) code was generated.\n  at 
System.Reflection.MonoMethod.Invoke (System.Object obj, 
System.Reflection.BindingFlags invokeAttr, 
System.Reflection.Binder binder, System.Object[] parameters, 
System.Globalization.CultureInfo culture) [0x00000] 
in <00000000000000000000000000000000>:0 \n  at 
System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] 
in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlFormatWriterInterpreter.WriteCollection (System.Runtime.Serialization.CollectionDataContract collectionContract) [0x00000] 
in <00000000000000000000000000000000>:0 \n  at 
System.Runtime.Serialization.XmlFormatWriterInt… string



 StackTrace: "  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlFormatWriterInterpreter.WriteCollection (System.Runtime.Serialization.CollectionDataContract collectionContract) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlFormatWriterInterpreter.WriteCollectionToXml (System.Runtime.Serialization.XmlWriterDelegator xmlWriter, System.Object obj, System.Runtime.Serialization.XmlObjectSerializerWriteContext context, System.Runtime.Serialization.CollectionDataContract collectionContract) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlForma… string

Source: "mscorlib" string

inner exception: 
 InnerException "System.ExecutionEngineException: Attempting to call method \'System.Runtime.Serialization.XmlObjectSerializerWriteContext::
IncrementCollectionCountGeneric<System.Boolean>\' for which no ahead of time (AOT) code was generated.\n  at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlFormatWriterInterpreter.WriteCollection (System.Runtime.Serialization.CollectionDataContract collectionContract) [0x00000] in <00000000000000000000000000000000>:0 \n  at System.Runtime.Serialization.XmlFormatWriterInterpreter.WriteCollectionToXml (System.Runtime.Serialization.XmlWriterDelegator xmlWriter, System.Object obj, System.Ru… System.Exception

Update

The problem seems to be with bool and int only, a List of string works just as expected.

dbc
  • 104,963
  • 20
  • 228
  • 340
Vanessa M
  • 21
  • 3
  • Is an exception thrown? If so, can you share the full `ToString()` output of the exception including the exception type, message, traceback and inner exception(s) if any? – dbc Aug 15 '22 at 19:47
  • What happens if you make `lTimesAnsweredCorrectly` be a public property? I.e. `public List lTimesAnsweredCorrectly { get; set; } = new List();`? (I'm thinking there may be some odd android-specific security restriction accessing non-public fields via reflection that you are hitting.) – dbc Aug 15 '22 at 19:51
  • Incidentally `[KnownType(typeof(T))]` attributes are only required when serializing polymorphic object graphs where the actual type being serialized differs from the declared type. – dbc Aug 15 '22 at 19:52
  • Also, what runtime are you using on android? [tag:mono]? – dbc Aug 15 '22 at 20:46
  • Hi, first thanks for your comments. Making the property public didn't change the result. I am posting the exception below, I am using IL2CPP. – Vanessa M Aug 16 '22 at 19:48
  • The exception was too big for a comment, I have added it to the original post – Vanessa M Aug 16 '22 at 19:53
  • I edited your tags and title to reflect that your problem is very platform-specific. I don't know how to solve your problem, but with the correct tags and title it may attract somebody familiar with IL2CPP + Android + Unity3d who does. One suggestion: try changing `List` to `bool []` and see if the problem goes away. – dbc Aug 16 '22 at 20:43
  • But possibly related: [How to prevent code stripping with IL2CPP?](https://stackoverflow.com/q/59029987) or [Invoke generic method via reflection in C# IL2CPP on iOS](https://stackoverflow.com/q/56183606/3744182). – dbc Aug 16 '22 at 20:48
  • 1
    thank you. I have tried a few more things and will let it here in case in the future someone bumps into this thread again. The problem seems to be with bool and int only, a List of string works just as expected. – Vanessa M Aug 20 '22 at 08:21
  • In that case you might [answer your own question](https://stackoverflow.com/help/self-answer) to let others know how you solved the problem. (I'd guess the problem arises for any list of value types.) – dbc Aug 20 '22 at 14:08
  • I haven't solved the problem, no... – Vanessa M Aug 21 '22 at 19:20
  • Oh, do you need a workaround then? I can suggest one, if you need it. – dbc Aug 21 '22 at 19:31

1 Answers1

0

Based on your statement

The problem seems to be with bool and int only, a List of string works just as expected.

It appears that, on Android, the ahead-of-time compilation for your framework isn't generating the necessary code for the .NET internal method XmlObjectSerializerWriteContext.IncrementCollectionCountGeneric<T>(XmlWriterDelegator xmlWriter, ICollection<T> collection) when T is a value type such as bool or int, but it is generating the necessary code for reference types. [1]

This may be a bug in the runtime or framework you are using on Android. You may want to report an issue to the vendor.

As a workaround, since collections such as List<string> do serialize successfully, you can create surrogate collections for your List<bool> and List<int> that appear to the serializer to be collections of strings, and perform the necessary string-to-bool (or string-to-int) conversion internally.

First, define the following classes:

[CollectionDataContract(Name = "ArrayOfboolean", ItemName = "boolean", Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
public class XmlBoolList : ConvertingList<bool, List<bool>, string>
{
    public XmlBoolList(List<bool> list) : base(() => list, b => XmlConvert.ToString(b), s => XmlConvert.ToBoolean(s)) { }
    public XmlBoolList() : this(new List<bool>()) { }
}

[CollectionDataContract(Name = "ArrayOfint", ItemName = "int", Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
public class XmlIntList : ConvertingList<int, List<int>, string>
{
    public XmlIntList(List<int> list) : base(() => list, i => XmlConvert.ToString(i), s => XmlConvert.ToInt32(s)) { }
    public XmlIntList() : this(new List<int>()) { }
}

public class ConvertingList<TIn, TList, TOut> : IList<TOut> where TList : IList<TIn>
{
    readonly Func<TList> getList;
    readonly Func<TIn, TOut> toOuter;
    readonly Func<TOut, TIn> toInner;

    public ConvertingList(Func<TList> getList, Func<TIn, TOut> toOuter, Func<TOut, TIn> toInner)
    {
        if (getList == null || toOuter == null || toInner == null)
            throw new ArgumentNullException();
        this.getList = getList;
        this.toOuter = toOuter;
        this.toInner = toInner;
    }

    public TList List => getList();
    TIn ToInner(TOut outer)=> toInner(outer);
    TOut ToOuter(TIn inner)=> toOuter(inner);

    #region IList<TOut> Members

    public int IndexOf(TOut item) => List.IndexOf(toInner(item));
    public void Insert(int index, TOut item) => List.Insert(index, ToInner(item));
    public void RemoveAt(int index) => List.RemoveAt(index);
    public TOut this[int index] { get => ToOuter(List[index]); set => List[index] = ToInner(value); }

    #endregion

    #region ICollection<TOut> Members

    public void Add(TOut item) => List.Add(ToInner(item));
    public void Clear() => List.Clear();
    public bool Contains(TOut item) => List.Contains(ToInner(item));
    public void CopyTo(TOut[] array, int arrayIndex)
    {
        foreach (var item in this)
            array[arrayIndex++] = item;
    }
    public int Count => List.Count;
    public bool IsReadOnly => List.IsReadOnly;
    public bool Remove(TOut item) => List.Remove(ToInner(item));

    #endregion

    #region IEnumerable<TOut> Members

    public IEnumerator<TOut> GetEnumerator()
    {
        foreach (var item in List)
            yield return ToOuter(item);
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    #endregion
}

Now modify your type GenericTaskItem contain a List<bool> by adding a private surrogate XmlBoolList property for serialization purposes:

abstract public class GenericTaskItem
{
    [DataMember]
    public string sItemID = "";

    [IgnoreDataMember]
    protected List<bool> lTimesAnsweredCorrectly = new List<bool>();
    
    [DataMember(Name = "lTimesAnsweredCorrectly")]
    XmlBoolList lTimesAnsweredCorrectlySurrogate { get => lTimesAnsweredCorrectly == null ? null : new XmlBoolList(lTimesAnsweredCorrectly); set => lTimesAnsweredCorrectly = value?.List; }

    //...
}

Now the serializer will serialize XmlBoolList as an array of reference types, specifically.

Notes:

  • The XmlConvert class may be used to convert .NET primitives from and to XML values.

  • XmlBoolList and XmlIntList are designed to have the same collection data contracts as, respectively, List<bool> and List<int>.

  • [KnownType(typeof(T))] attributes are only required when serializing polymorphic object graphs where the actual type being serialized differs from the declared type, which is not the case in your List<bool> and XmlBoolList properties. See Data Contract Known Types for details.

  • You might investigate using data contract surrogates to replace collections of value types such as List<bool> globally rather than on a per-property basis.

Demo fiddle here.


[1] This is surprising -- but not entirely surprising. The .NET runtime implements support for generics of reference types vs generics of value types very differently; see Generics in the Run Time (C# Programming Guide) for details.

dbc
  • 104,963
  • 20
  • 228
  • 340