KeRNeLith / QuikGraph

Generic Graph Data Structures and Algorithms for .NET
https://kernelith.github.io/QuikGraph/
Microsoft Public License
471 stars 67 forks source link

[BUG] GraphML serialization fails with WriteDelegateCompiler error "Operation is not supported on this platform." #33

Closed Vasar007 closed 3 years ago

Vasar007 commented 3 years ago

Describe the bug

I used QuickGraph in oen of my Unity project. I have a GraphML serialization logic which perfectly works in Debug mode. However, when I run project in Release mode (see steps to reproduce), serialization always fails with stacktrace:

 System.TypeInitializationException: The type initializer for 'WriteDelegateCompiler' threw an exception. ---> System.PlatformNotSupportedException: Operation is not supported on this platform.
  at QuikGraph.Serialization.GraphMLSerializer`3+WriteDelegateCompiler[TVertex,TEdge,TGraph].CreateWriteDelegate (System.Type nodeType, System.Type delegateType) [0x00042] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLSerializer`3+WriteDelegateCompiler[TVertex,TEdge,TGraph]..cctor () [0x00000] in <9ecb0810e6744db493d1d934627823d5>:0 
   --- End of inner exception stack trace ---
  at (wrapper managed-to-native) System.Object.__icall_wrapper_mono_generic_class_init(intptr)
  at QuikGraph.Serialization.GraphMLSerializer`3+WriterWorker[TVertex,TEdge,TGraph].WriteGraphHeader () [0x000f3] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLSerializer`3+WriterWorker[TVertex,TEdge,TGraph].Serialize () [0x00018] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLSerializer`3[TVertex,TEdge,TGraph].Serialize (System.Xml.XmlWriter writer, TGraph graph, QuikGraph.VertexIdentity`1[TVertex] vertexIdentity, QuikGraph.EdgeIdentity`2[TVertex,TEdge] edgeIdentity) [0x00049] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLExtensions.SerializeToGraphML[TVertex,TEdge,TGraph] (TGraph graph, System.Xml.XmlWriter writer, QuikGraph.VertexIdentity`1[TVertex] vertexIdentity, QuikGraph.EdgeIdentity`2[TVertex,TEdge] edgeIdentity) [0x00005] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLExtensions.SerializeToGraphML[TVertex,TEdge,TGraph] (TGraph graph, System.Xml.XmlWriter writer) [0x0002b] in <9ecb0810e6744db493d1d934627823d5>:0 

To Reproduce

Steps to reproduce the behavior:

  1. Create any Unity project
  2. Import QuickGraph and QuikGraph.Serialization packages
  3. Write some code with GraphML serialization logic
  4. Attach script to some GameObject
  5. Build a Unity project in Release mode (File -> Build and Run)
  6. Run project in Release mode
  7. Serialization will fail

Expected behavior

Serialization will work. Also you can make a note in documentation instead that some platforms (which ones?) are not supported in the QuikGraph.Serialization package.

Vasar007 commented 3 years ago

Additional context

Note: in my code I use extension methods:

using System.Xml;
using QuikGraph;
using QuikGraph.Serialization;

public static class GraphExtensions
{
    public static void SerializeToGraphML<TVertex, TEdge>(this IEdgeListGraph<TVertex, TEdge> graph,
        string filePath)
        where TEdge : IEdge<TVertex>
    {
        graph.SerializeToGraphML<TVertex, TEdge, IEdgeListGraph<TVertex, TEdge>>(filePath);
    }

    public static void SerializeToGraphML<TVertex, TEdge>(this IEdgeListGraph<TVertex, TEdge> graph,
        XmlWriter xmlWriter)
        where TEdge : IEdge<TVertex>
    {
        graph.SerializeToGraphML<TVertex, TEdge, IEdgeListGraph<TVertex, TEdge>>(xmlWriter);
    }
}

The reason I wrote them is that SerializeToGraphML method requires to write full list of the type arguments almost always (I have my custom vertex type) which is a bit of annoying.

So, is there any option to help compiler to infer type arguments? image

(UPD: I mixed up with my configurations, so, error is still raised :))

KeRNeLith commented 3 years ago

Hello,

I was able to test the behavior your describing in a sample project under Unity 2019.4.21f1. From what I observed using both QuikGraph 2.3.0 (target net45) and QuikGraph.Serialization 2.3.0 (target net40), when I run the game using the play button and a script like the following:

TestGraph = new AdjacencyGraph<int, Edge<int>>();
TestGraph.AddVertex(1);
TestGraph.AddVertex(2);
TestGraph.AddVertex(3);
TestGraph.AddEdge(new Edge<int>(1, 2));
TestGraph.AddEdge(new Edge<int>(3, 1));
TestGraph.AddEdge(new Edge<int>(3, 2));

TestGraph.SerializeToGraphML<int, Edge<int>, AdjacencyGraph<int, Edge<int>>>("./graph.graphml");

I'm indeed able to see that target file is generated properly with the given content: image

Then I tried to File > Build and Run, as you said, and I'm not really an expert of Unity so I was not able to see the callstack your joining but I noticed that the content of the file in such situation is different. If you have any help for me on the subject ;-). I'm getting the following result: image

Which seems to be the result of being unable to write vertices.

Using this page as resource, even if it does not really match my Unity version. So I moved the scripting runtime version from .NET Standard 2.0 to .NET 4.x image

By doing so, running again the File > Build and Run, allowed me to have a working version of the serialization. I know it sounds more like a workaround but until now I was not able to figure out why such difference. Will certainly require more knowledge on Unity ecosystem. BTW it certainly better fits for a usage of QuikGraph .NET Framework assemblies. I also tried using .NET Standard 2.0 version of QuikGraph assemblies but was unable to make them working 😢. I observed the same behavior as before.

Concerning the other point to reduce the number of template parameter needed, then it's indeed because those methods are heavily templated and so deduction is quite not possible. But to make it more friendly to use you can indeed move to methods with much more specific types that will feed template parameters and so allow template deduction, or at least help. Note that you can create your own graph structure inheriting from the one you're currently using in order to make it less verbose. As an example if you use AdjacencyGraph<CampaignMapNode, Edge<CampaignMapNode>>, you may create a class like:

public class CompaignGraph : AdjacencyGraph<CampaignMapNode, Edge<CampaignMapNode>>
{
}

And so it will certainly help in several points.

Vasar007 commented 3 years ago

I used this code to get a stacktrace:

try
{
    // Serialize graph.
    GraphSerializer.SaveGraph(graph);
    // Deserialize graph.
    graph = GraphSerializer.LoadGraph<AdjacencyGraph<CampaignMapNode, Edge<CampaignMapNode>>>();
}
catch (Exception ex)
{
    Debug.LogError(ex.ToString());
}

And then you can find stacktrace in the Unity log file.

I know it sounds more like a workaround but until now I was not able to figure out why such difference.

I prefer to stay on .NET Standard if it is possible.

As for stacktrace:

 System.TypeInitializationException: The type initializer for 'WriteDelegateCompiler' threw an exception. ---> System.PlatformNotSupportedException: Operation is not supported on this platform.
  at QuikGraph.Serialization.GraphMLSerializer`3+WriteDelegateCompiler[TVertex,TEdge,TGraph].CreateWriteDelegate (System.Type nodeType, System.Type delegateType) [0x00042] in <9ecb0810e6744db493d1d934627823d5>:0 
  at QuikGraph.Serialization.GraphMLSerializer`3+WriteDelegateCompiler[TVertex,TEdge,TGraph]..cctor () [0x00000] in <9ecb0810e6744db493d1d934627823d5>:0

It seems like exception was thrown here:

image image

So, we can see that serializer writes footer and header but some error occurs in the WriteGraphHeader method.

Vasar007 commented 3 years ago

I think one of Reflection method/class in the highlighted code cannot be used on .NET Standard + Mono.

I also tried using .NET Standard 2.0 version of QuikGraph assemblies but was unable to make them working

Did you try to reproduce this issue on the Mono runtime? Because Unity uses exactly Mono runtime.

KeRNeLith commented 3 years ago

Hum so I installed Mono on Windows to make some tests. Here is my version:

& 'C:\Program Files\Mono\bin\mono.exe' --version
Mono JIT compiler version 6.12.0 (Visual Studio built mono)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
        TLS:           __thread
        SIGSEGV:       normal
        Notification:  Thread + polling
        Architecture:  amd64
        Disabled:      none
        Misc:          softdebug
        Interpreter:   yes
        LLVM:          supported, not enabled.
        Suspend:       preemptive
        GC:            sgen (concurrent by default)

Then I compiled with Visual Studio a console application (target .NET 5.0, so referencing .NET Standard 2.0 QuikGraph libraries) doing just:

var testGraph = new AdjacencyGraph<int, Edge<int>>();
testGraph.AddVertex(1);
testGraph.AddVertex(2);
testGraph.AddVertex(3);
testGraph.AddEdge(new Edge<int>(1, 2));
testGraph.AddEdge(new Edge<int>(3, 1));
testGraph.AddEdge(new Edge<int>(3, 2));

try
{
    testGraph.SerializeToGraphML<int, Edge<int>, AdjacencyGraph<int, Edge<int>>>("./graph.graphml");
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

And then ran the program like: & 'C:\Program Files\Mono\bin\mono.exe' .\TestConsoleApp.dll

But it seems I don't get any issue in this case :-s

After other searches I found this issue related to Unity itself. The problem you spotted is with the usage of DynamicMethod. This API is present in .NET since a long time for .NET Framework but it's not available for .NET Standard before 2.1. In QuikGraph there is a reference to nuget System.Reflection.Emit.Lightweight for the .NET Standard 2.0 build. Since you only have the choice of .NET Standard 2.0 in Unity project settings I'm not sure you will be able to run it using the standard. Note that the mentionned issue seems to maybe provide a way to workaround.

A dummy test I made is just declaring a DynamicMethod in a script and let Unity compile it and when project is configured to use .NET 4.x I have no error, and if I keep the .NET Standard 2.0 then you get the compilation error: image

This seems to simply explain the reason why depending on the project setting you use you will have error or not.

Vasar007 commented 3 years ago

Hmm, but I use Unity 2020.3 and project compiles without any errors. There is only this serialization issue in runtime.

Vasar007 commented 3 years ago

Somehow all works in the Debug mode and I do not know why :)

However, when I added NuGet packages System.Reflection.Emit.Lightweight and System.Reflection.Emit.ILGeneration I caught NotSupportedException even in the Debug mode.

Vasar007 commented 3 years ago

So, there is no option to fix this issue, isn't it? I should only change target framework, right?

If that is true, I think you can write about such pitfall in the documentation.

Finally, is there another way to serialize a graoh? Will XML or .NET serialization work in the same configuration as I use? I don't have enough time to check these ways right now.

KeRNeLith commented 3 years ago

The support of NET Standard in Unity is in my opinion an ongoing task which they are working on. But I'm not aware about the effort they are putting in. So I'm afraid for now you will not have other option to use .NET 4.x support to make it working in Unity.

You're right I can mention that point in the documentation to warn future users, at least for versions under or equal 2020.3.

In QuikGraph.Serialization you will indeed find other possible serialization ways, if there are fitting your needs. I don't know if graphml serialization was a requirement for you or not.

All my following tests are using the following graph:

testGraph = new AdjacencyGraph<int, Edge<int>>();
testGraph.AddVertex(1);
testGraph.AddVertex(2);
testGraph.AddVertex(3);
testGraph.AddEdge(new Edge<int>(1, 2));
testGraph.AddEdge(new Edge<int>(3, 1));
testGraph.AddEdge(new Edge<int>(3, 2));

I tested keeping the Unity project setting to NET Standard 2.0 (File > Build and Run also tested) and used XML serialization which is working:

var settings = new XmlWriterSettings { Indent = true, IndentChars = "    " };
using (XmlWriter xmlWriter = XmlWriter.Create("./graph.xml", settings))
{
    testGraph.SerializeToXml(
        xmlWriter,
        v => v.ToString(),
        testGraph.GetEdgeIdentity(),
        "graph",
        "vertex",
        "edge",
        "");
}

Or Binary serialization which is also working:

using (var writer = new FileStream("./graph.bin", FileMode.OpenOrCreate))
{
    testGraph.SerializeToBinary(writer);
}
Vasar007 commented 3 years ago

Okay, thanks! I think I can try to use XML serialization. I don't have any requirenments because I just work on my own pet project :)

So, you are free to close this issue. But I think note in the documentation could help for other users in the future.

KeRNeLith commented 3 years ago

Yep I will close this issue and make a mention in the QuikGraph wiki/documentation. Thanks for your feedback that has been instructive and will be helpful for others!