Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net serialization - mixing [Serializable] with custom in inheritance tree

I'm converting a Java game to C# (Titan Attacks by Puppy Games) and am pretty much done now apart from the last task which is serialization of the game state for save files.

Typical hierarchy: Resource (base)->Feature->Screen/Effect/Entity->GameScreen/LaserEffect/Invader

The Java code uses standard ObjectOutputStream / ObjectInputStream to perform binary serialization but annoyingly performs some readResolve / writeResolve work at the base class level (Resource) to customize the serialization process (if a resource is named it doesn't serialize it and simply returns a proxy with a name that is used later to fetch the resource out of a hashmap).

My naive solutions is to blindly copy this approach and implement ISerializable in the base class to override the TYPE...

public virtual void GetObjectData(SerializationInfo info, StreamingContext context) {
    if (name != null) {
        // Got a name, so send a SerializedResource that just references us
        info.AddValue("name", this.name);
        info.SetType(typeof(SerializedResource));
        return;
    }

    //Serialize just MY fields (defined in Resource)
    this.SerializeMyFields(info, context, typeof(Resource));
}

Q) So, I'm pretty sure that all bets are off for built-in serialization and I have to implement ISerializable all the way down the inheritance chain along with the serialization constructor?

Note GetObjectData is virtual so derived classes can serialize their fields and then call the base class. This works but it's a ton of tedious work adding to LOTS of classes (100s).

Some derived types (Sprite, InvaderBehaviour, etc) also perform custom serialization work too to make matters worse.

I've looked at Jeffrey Richter's articles on the subject and tried using a ResourceSurrogateSelector : ISerializationSurrogate type construct instead but those serialization methods only get called if the type being serialized is a Resource and not a type derived from resource (i.e. would not get called serializing an Invader or GameScreen)

Q) is there a smart way to do this?

I've managed to keep the two code-bases pretty close to each other and this has made the conversion much easier - I'd like to continue this approach here (so no XmlSerializer, Protobuf, etc) unless there's a really compelling reason not to.

I've thought about writing some Java to automate the process and reflect the types that implement the Serializable interface and create a .cs file with all the .Net serialization code in so that I don't pollute the main class files (I'd make them partial)

PS - Target platforms will be Windows8 / Surface / XBox360 on the .Net side of things (so version 4) and probably PS Vita / maybe iOS using Mono. Saves are deserialized on the platform they were serialized on.

EDIT An answer by Sergey Teplyakov in this post.... .NET, C#: How to add a custom serialization attribute that acts as ISerializable interface ...has led me to the ISurrogateSelector interface which looks like it will help with selecting the desired derived classes.

like image 284
Paul Cunningham Avatar asked Nov 12 '22 14:11

Paul Cunningham


1 Answers

This is what I've managed to come up with so far and I'm pretty happy with it :-) Just got to add readResolve/writeReplace and I'm about done! (I'll probably wrap the Object, SerializationInfo, StreamingContext args up in an ObjectOutputStream for good measure too).

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

using java.io; //My implementation of various Java classes

namespace NewSerializationTest {

public sealed class JavaSerializableSurrogateSelector : ISurrogateSelector
{
    public void ChainSelector(ISurrogateSelector selector) { throw new NotImplementedException(); }

    public ISurrogateSelector GetNextSelector() { throw new NotImplementedException(); }

    public ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector)
    {
        if (typeof(Serializable).IsAssignableFrom(type))
        {
            selector = this;
            return new JavaSerializationSurrogate();
        }

        //Type is not marked (java.io.)Serializable
        selector = null;
        return null;
    }
}

public sealed class JavaSerializationSurrogate : ISerializationSurrogate {

    //Method called to serialize a java.io.Serializable object
    public void GetObjectData(Object obj, SerializationInfo info, StreamingContext context) {

        //Do the entire tree looking for the 'marker' methods
        var type = obj.GetType();
        while (type != null) 
        {
            var writeObject = type.GetMethod("writeObject", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext), typeof(Type) }, null );
            if (writeObject != null) {
                //Class has declared custom serialization so call through to that
                writeObject.Invoke(obj, new object[] { info, context, type });
            } else {
                //Default serialization of all non-transient fields at this level only (not the entire tree)
                obj.SerializeFields(info, context, type);
            }

            type = type.BaseType;   
        }
    }

    //Method called to deserialize a java.io.Serializable object
    public Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) {

        //Do the entire tree looking for the 'marker' methods
        var type = obj.GetType();
        while (type != null) 
        {
            var readObject = type.GetMethod("readObject", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext), typeof(Type) }, null );
            if (readObject != null) {
                //Class has declared custom serialization so call through to that
                readObject.Invoke(obj, new object[] { info, context, type });
            } else {
                //Default serialization of all non-transient fields at this level only (not the entire tree)
                obj.DeserializeFields(info, context, type);
            }

            type = type.BaseType;   
        }

        return null;
    }
}

[Serializable]
class A  : java.io.Serializable {
    public string Field1;
}

[Serializable]
class B : A {
    public string Field2; 

    private void readObject(SerializationInfo stream, StreamingContext context, Type declaringType) {
        stream.defaultReadObject(context, this, declaringType);

        Debug.WriteLine("B: readObject");
    } 

    private void writeObject(SerializationInfo stream, StreamingContext context, Type declaringType) {
        stream.defaultWriteObject(context, this, declaringType);

        Debug.WriteLine("B: writeObject");
    } 
}

[Serializable]
class C: B {
    public string Field3;

    private void writeObject(SerializationInfo stream, StreamingContext context, Type declaringType) {
        stream.defaultWriteObject(context, this, declaringType);

        Debug.WriteLine("C: writeObject");
    } 
}

public static class SerializationInfoExtensions {

    public static void defaultWriteObject(this SerializationInfo info, StreamingContext context, object o, Type declaringType) {
        o.SerializeFields(info, context, declaringType);
    }

    public static void defaultReadObject(this SerializationInfo info, StreamingContext context, object o, Type declaringType) {
        o.DeserializeFields(info, context, declaringType);
    }
}

class Program {
    static void Main(string[] args) {

        var myC = new C { Field1 = "tom", Field2 = "dick", Field3 = "harry" }; 

        using (var ms = new MemoryStream()) {
            var binaryFormatter = new BinaryFormatter();
            binaryFormatter.SurrogateSelector = new JavaSerializableSurrogateSelector();

            binaryFormatter.Serialize(ms, myC);
            ms.Position = 0;
            var myCDeserialized = binaryFormatter.Deserialize(ms);
        }
    }
}

/// <summary>
/// Extensions to the object class.
/// </summary>
public static class ObjectExtensions
{
    /// <summary>
    /// Serializes an object's class fields.
    /// </summary>
    /// <param name="source">The source object to serialize.</param>
    /// <param name="info">SerializationInfo.</param>
    /// <param name="context">StreamingContext.</param>
    /// <param name="declaringType">The level in the inheritance whose fields are to be serialized - pass null to serialize the entire tree.</param>
    public static void SerializeFields(this object source, SerializationInfo info, StreamingContext context, Type declaringType)
    {
        //Serialize the entire inheritance tree if there is no declaringType passed.
        var serializeTree = declaringType == null;

        //Set the level in the class heirarchy we are interested in - if there is no declaring type use the source type (and the entire tree will be done).
        var targetType = declaringType ?? source.GetType();

        //Get the set of serializable members for the target type
        var memberInfos = FormatterServices.GetSerializableMembers(targetType, context);

        // Serialize the base class's fields to the info object
        foreach (var mi in memberInfos)
        {
            if (serializeTree || mi.DeclaringType == targetType) {
                //Specify the name to use as the key - if the entire tree is being done then the names will already have a prefix. Otherwise, we need to 
                //append the name of the declaring type.
                var name = serializeTree ? mi.Name : mi.DeclaringType.Name + "$" + mi.Name;

                info.AddValue(name, ((FieldInfo)mi).GetValue(source));
            }
        }
    }

    /// <summary>
    /// Deserializes an object's fields.
    /// </summary>
    /// <param name="source">The source object to serialize.</param>
    /// <param name="info">SerializationInfo.</param>
    /// <param name="context">StreamingContext.</param>
    /// <param name="declaringType">The level in the inheritance whose fields are to be deserialized - pass null to deserialize the entire tree.</param>
    public static void DeserializeFields(this object source, SerializationInfo info, StreamingContext context, Type declaringType)
    {
        //Deserialize the entire inheritance tree if there is no declaringType passed.
        var deserializeTree = declaringType == null;

         //Set the level in the class heirarchy we are interested in - if there is no declaring type use the source type (and the entire tree will be done).
        var targetType = declaringType ?? source.GetType();

        var memberInfos = FormatterServices.GetSerializableMembers(targetType, context);

        // Deserialize the base class's fields from the info object
        foreach (var mi in memberInfos)
        {
            //Only serialize the fields at the specific level requested.
            if (deserializeTree || mi.DeclaringType == declaringType) 
            {
                // To ease coding, treat the member as a FieldInfo object
                var fi = (FieldInfo) mi;

                //Specify the name to use as the key - if the entire tree is being done then the names will already have a prefix. Otherwise, we need to 
                //append the name of the declaring type.
                var name = deserializeTree ? mi.Name : mi.DeclaringType.Name + "$" + mi.Name;

                // Set the field to the deserialized value
                fi.SetValue(source, info.GetValue(name, fi.FieldType));
            }
        }
    }
}
}
like image 89
Paul Cunningham Avatar answered Nov 15 '22 05:11

Paul Cunningham