Skip to content

Commit

Permalink
Breaking Changes for FastSerialization (#2121)
Browse files Browse the repository at this point in the history
* - Rename SerializationConfiguration --> SerializationSettings.
 - Require an instance of SerializationSettings for serializers and
   deserializers.

* Require a SerializationSettings property for both IStreamReader and
IStreamWriter to allow writers to directly create readers.

* Refactor SerializationSettings to clone on mutation.  This ensures that
usres can't change the set of allowed types after it is set and handed
to the deserialization code.

* Refactor FastSerialization factories and add known types.

Remove the ability for FastSerialization to create arbitrary types that
are specified in serialized files.  Instead, force users to register all
types before or during serialization.  Types can be registered via calls
to Deserializer.RegisterType and Deserializer.RegisterFactory.  Users
can also implement Deserializer.OnUnregisteredType to handle
unregistered types encountered during deserialization.  It is incumbent
on the implementor to not just blindly call Type.GetType in
OnUnregisteredType, and instead only create known types.

* Add tests and remove RegisterType(string) because users will need to
provide enough context to find the right assembly.

* Move StreamReaderAlignment into SerializationSettings.

* Undo RID change to HeapDump.csproj.

* Rename SetXXX --> WithXXX since it doesn't change the current instance.

* Simplify based on feedback.
  • Loading branch information
brianrob authored Nov 5, 2024
1 parent d1ad99b commit a358962
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 139 deletions.
161 changes: 101 additions & 60 deletions src/FastSerialization/FastSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,41 @@ enum StreamReaderAlignment : int
};

/// <summary>
/// These settings apply to use of Serializer and Deserializer specifically.
/// Settings used by <code>Serializer</code> and <code>Deserializer</code>.
/// </summary>
#if FASTSERIALIZATION_PUBLIC
public
#endif
sealed class SerializationConfiguration
sealed class SerializationSettings
{
public StreamLabelWidth StreamLabelWidth { get; set; } = StreamLabelWidth.EightBytes;
internal StreamLabelWidth StreamLabelWidth { get; }

internal StreamReaderAlignment StreamReaderAlignment { get; }

public static SerializationSettings Default { get; } = new SerializationSettings(
StreamLabelWidth.EightBytes,
StreamReaderAlignment.EightBytes);

public SerializationSettings WithStreamLabelWidth(StreamLabelWidth width)
{
return new SerializationSettings(
width,
StreamReaderAlignment);
}

public SerializationSettings WithStreamReaderAlignment(StreamReaderAlignment alignment)
{
return new SerializationSettings(
StreamLabelWidth,
alignment);
}

private SerializationSettings(
StreamLabelWidth streamLabelWidth, StreamReaderAlignment streamReaderAlignment)
{
StreamLabelWidth = streamLabelWidth;
StreamReaderAlignment = streamReaderAlignment;
}
}

/// <summary>
Expand Down Expand Up @@ -184,6 +211,11 @@ interface IStreamWriter : IDisposable
/// IStreamReader.GotoSuffixLabel for more)
/// </summary>
void WriteSuffixLabel(StreamLabel value);

/// <summary>
/// The settings associated with this writer.
/// </summary>
SerializationSettings Settings { get; }
}


Expand Down Expand Up @@ -253,6 +285,11 @@ interface IStreamReader : IDisposable
/// and then seeking to that position.
/// </summary>
void GotoSuffixLabel();

/// <summary>
/// The settings associated with this reader.
/// </summary>
SerializationSettings Settings { get; }
}

#if !DOTNET_V35
Expand Down Expand Up @@ -516,7 +553,7 @@ sealed class Serializer : IDisposable
/// <param name="filePath">The destination file.</param>
/// <param name="entryObject">The object to serialize.</param>
/// <param name="share">Optional sharing mode for the destination file. Defaults to <see cref="FileShare.Read"/>.</param>
public Serializer(string filePath, IFastSerializable entryObject, FileShare share = FileShare.Read) : this(new IOStreamStreamWriter(filePath, share: share), entryObject) { }
public Serializer(string filePath, IFastSerializable entryObject, FileShare share = FileShare.Read) : this(new IOStreamStreamWriter(filePath, settings: SerializationSettings.Default, share: share), entryObject) { }

/// <summary>
/// Create a serializer that writes <paramref name="entryObject"/> to a <see cref="Stream"/>. The serializer
Expand All @@ -533,7 +570,7 @@ public Serializer(Stream outputStream, IFastSerializable entryObject)
/// closes.
/// </summary>
public Serializer(Stream outputStream, IFastSerializable entryObject, bool leaveOpen)
: this(new IOStreamStreamWriter(outputStream, leaveOpen: leaveOpen), entryObject)
: this(new IOStreamStreamWriter(outputStream, SerializationSettings.Default, leaveOpen: leaveOpen), entryObject)
{
}

Expand Down Expand Up @@ -1035,18 +1072,18 @@ sealed class Deserializer : IDisposable
/// <summary>
/// Create a Deserializer that reads its data from a given file
/// </summary>
public Deserializer(string filePath, SerializationConfiguration config = null)
public Deserializer(string filePath, SerializationSettings settings)
{
IOStreamStreamReader reader = new IOStreamStreamReader(filePath, config);
IOStreamStreamReader reader = new IOStreamStreamReader(filePath, settings);
Initialize(reader, filePath);
}

/// <summary>
/// Create a Deserializer that reads its data from a given System.IO.Stream. The stream will be closed when the Deserializer is done with it.
/// </summary>
public Deserializer(Stream inputStream, string streamName, SerializationConfiguration config = null)
public Deserializer(Stream inputStream, string streamName, SerializationSettings settings)
{
IOStreamStreamReader reader = new IOStreamStreamReader(inputStream, config: config);
IOStreamStreamReader reader = new IOStreamStreamReader(inputStream, settings: settings);
Initialize(reader, streamName);
}

Expand All @@ -1055,9 +1092,9 @@ public Deserializer(Stream inputStream, string streamName, SerializationConfigur
/// <paramref name="leaveOpen"/> parameter determines whether the deserializer will close the stream when it
/// closes.
/// </summary>
public Deserializer(Stream inputStream, string streamName, bool leaveOpen, SerializationConfiguration config = null)
public Deserializer(Stream inputStream, string streamName, bool leaveOpen, SerializationSettings settings)
{
IOStreamStreamReader reader = new IOStreamStreamReader(inputStream, leaveOpen: leaveOpen, config: config);
IOStreamStreamReader reader = new IOStreamStreamReader(inputStream, leaveOpen: leaveOpen, settings: settings);
Initialize(reader, streamName);
}

Expand Down Expand Up @@ -1610,38 +1647,61 @@ public StreamLabel ResolveForwardReference(ForwardReference reference, bool pres
public String Name { get; private set; }

/// <summary>
/// If set this function is set, then it is called whenever a type name from the serialization
/// data is encountered. It is your you then need to look that up. If it is not present
/// it uses Type.GetType(string) which only checks the current assembly and mscorlib.
/// Called when <code>Deserializer</code> encounters a type that is not registered, allowing the implementation
/// to return a factory delegate to be cached and used for subsequent encounters.
/// </summary>
public Func<string, Type> TypeResolver { get; set; }
public Func<string, Func<IFastSerializable>> OnUnregisteredType { get; set; }

/// <summary>
/// For every IFastSerializable object being deserialized, the Deserializer needs to create 'empty' objects
/// that 'FromStream' is invoked on. The Deserializer gets these 'empty' objects by calling a 'factory'
/// delegate for that type. Thus all types being deserialized must have a factory.
/// Registers a creation factory for a type.
///
/// RegisterFactory registers such a factory for particular 'type'.
/// When the <code>Deserializer</code>encounters a serialized type, it will look for a registered factory or type registration
/// so that it knows how to construct an empty instance of the type that can be filled. All non-primitive types
/// must either be registered by calling RegisterFactory or RegisterType.
/// </summary>
public void RegisterFactory(Type type, Func<IFastSerializable> factory)
{
factories[type.FullName] = factory;
}

/// <summary>
/// Registers a creation factory for a type name.
///
/// When the <code>Deserializer</code>encounters a serialized type, it will look for a registered factory or type registration
/// so that it knows how to construct an empty instance of the type that can be filled. All non-primitive types
/// must either be registered by calling RegisterFactory or RegisterType.
/// </summary>
public void RegisterFactory(string typeName, Func<IFastSerializable> factory)
{
factories[typeName] = factory;
}

/// <summary>
/// For every IFastSerializable object being deserialized, the Deserializer needs to create 'empty' objects
/// that 'FromStream' is invoked on. The Deserializer gets these 'empty' objects by calling a 'factory'
/// delegate for that type. Thus all types being deserialized must have a factory.
/// Registers a type that can be created by instantiating the parameterless constructor.
///
/// RegisterDefaultFactory registers a factory that is passed a type parameter and returns a new IFastSerialable object.
/// When the <code>Deserializer</code>encounters a serialized type, it will look for a registered factory or type registration
/// so that it knows how to construct an empty instance of the type that can be filled. All non-primitive types
/// must either be registered by calling RegisterFactory or RegisterType.
/// </summary>
public void RegisterDefaultFactory(Func<Type, IFastSerializable> defaultFactory)
public void RegisterType(Type type)
{
RegisterFactory(type, () =>
{
return CreateDefault(type);
});
}

private static IFastSerializable CreateDefault(Type type)
{
this.defaultFactory = defaultFactory;
try
{
return (IFastSerializable)Activator.CreateInstance(type);
}
catch (MissingMethodException)
{
throw new SerializationException(
$"Unable to create an object of type {type.FullName}. It must either have a parameterless constructor or have been registered with the deserializer via RegisterFactory.");
}
}

// For FromStream method bodies, reading tagged values (for post V1 field additions)
Expand Down Expand Up @@ -1964,49 +2024,31 @@ private IFastSerializable ReadObjectDefinition(Tags tag, StreamLabel objectLabel

internal Func<IFastSerializable> GetFactory(string fullName)
{
Func<IFastSerializable> ret;
if (factories.TryGetValue(fullName, out ret))
// Check for a registered factory.
Func<IFastSerializable> factory = null;
if (factories.TryGetValue(fullName, out factory))
{
return ret;
return factory;
}

Type type;
if (TypeResolver != null)
{
type = TypeResolver(fullName);
}
else
// If there is not a registered factory, give the implementation of IFastSerializable an opportunity to create a factory.
if (OnUnregisteredType != null)
{
type = Type.GetType(fullName);
factory = OnUnregisteredType(fullName);
if (factory != null)
{
// Save the factory for future encounters of this type name.
RegisterFactory(fullName, factory);
}
}

if (type == null)
if (factory == null)
{
throw new TypeLoadException("Could not find type " + fullName);
throw new TypeLoadException(
$"Could not create an instance of type {fullName}. The type must be registered with the deserializer via a call to RegisterFactory or RegisterType.");
}

return delegate
{
// If we have a default factory, use it.
if (defaultFactory != null)
{
IFastSerializable instance = defaultFactory(type);
if (instance != null)
{
return instance;
}
}
// Factory of last resort.
try
{
return (IFastSerializable)Activator.CreateInstance(type);
}
catch (MissingMethodException)
{
throw new SerializationException("Failure deserializing " + type.FullName +
".\r\nIt must either have a parameterless constructor or been registered with the serializer.");
}
};
return factory;
}

private void FindEndTag(SerializationType type, IFastSerializable objectBeingDeserialized)
Expand Down Expand Up @@ -2144,7 +2186,6 @@ private Tags ReadTag()
/// </summary>
internal bool deferForwardReferences;
private Dictionary<string, Func<IFastSerializable>> factories;
private Func<Type, IFastSerializable> defaultFactory;
#endregion
};

Expand Down
18 changes: 13 additions & 5 deletions src/FastSerialization/MemoryMappedFileStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,19 @@ public class MemoryMappedFileStreamReader : IStreamReader
private long _capacity;
private long _offset;

public MemoryMappedFileStreamReader(string mapName, long length)
: this(MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.Read), length, leaveOpen: false)
public MemoryMappedFileStreamReader(string mapName, long length, SerializationSettings settings)
: this(MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.Read), length, leaveOpen: false, settings)
{
}

public MemoryMappedFileStreamReader(MemoryMappedFile file, long length, bool leaveOpen)
public MemoryMappedFileStreamReader(MemoryMappedFile file, long length, bool leaveOpen, SerializationSettings settings)
{
_file = file;
_fileLength = length;
_leaveOpen = leaveOpen;

Settings = settings ?? throw new ArgumentNullException(nameof(settings));

if (IntPtr.Size == 4)
{
_capacity = Math.Min(_fileLength, BlockCopyCapacity);
Expand All @@ -53,11 +55,17 @@ public MemoryMappedFileStreamReader(MemoryMappedFile file, long length, bool lea
_viewAddress = _view.SafeMemoryMappedViewHandle.DangerousGetHandle();
}

public static MemoryMappedFileStreamReader CreateFromFile(string path)
public static MemoryMappedFileStreamReader CreateFromFile(string path, SerializationSettings settings)
{
long capacity = new FileInfo(path).Length;
MemoryMappedFile file = MemoryMappedFile.CreateFromFile(path, FileMode.Open, Guid.NewGuid().ToString("N"), capacity, MemoryMappedFileAccess.Read);
return new MemoryMappedFileStreamReader(file, capacity, leaveOpen: false);
return new MemoryMappedFileStreamReader(file, capacity, leaveOpen: false, settings);
}

public SerializationSettings Settings
{
get;
private set;
}

public DeferedStreamLabel Current
Expand Down
16 changes: 8 additions & 8 deletions src/FastSerialization/SegmentedMemoryStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public class SegmentedMemoryStreamReader
/// <summary>
/// Create a IStreamReader (reads binary data) from a given byte buffer
/// </summary>
public SegmentedMemoryStreamReader(SegmentedList<byte> data, SerializationConfiguration config = null) : this(data, 0, data.Count, config) { }
public SegmentedMemoryStreamReader(SegmentedList<byte> data, SerializationSettings settings) : this(data, 0, data.Count, settings) { }
/// <summary>
/// Create a IStreamReader (reads binary data) from a given subregion of a byte buffer
/// </summary>
public SegmentedMemoryStreamReader(SegmentedList<byte> data, long start, long length, SerializationConfiguration config = null)
public SegmentedMemoryStreamReader(SegmentedList<byte> data, long start, long length, SerializationSettings settings)
{
SerializationConfiguration = config ?? new SerializationConfiguration();
Settings = settings ?? throw new ArgumentNullException(nameof(settings));

if (SerializationConfiguration.StreamLabelWidth == StreamLabelWidth.FourBytes)
if (Settings.StreamLabelWidth == StreamLabelWidth.FourBytes)
{
readLabel = () =>
{
Expand Down Expand Up @@ -157,7 +157,7 @@ public virtual void Goto(StreamLabel label)
{
Debug.Assert(label != StreamLabel.Invalid);

if (SerializationConfiguration.StreamLabelWidth == StreamLabelWidth.FourBytes)
if (Settings.StreamLabelWidth == StreamLabelWidth.FourBytes)
{
Debug.Assert((long)label <= int.MaxValue);
position = (uint)label;
Expand All @@ -174,7 +174,7 @@ public virtual StreamLabel Current
{
get
{
if (SerializationConfiguration.StreamLabelWidth == StreamLabelWidth.FourBytes)
if (Settings.StreamLabelWidth == StreamLabelWidth.FourBytes)
{
return (StreamLabel)(uint)position;
}
Expand Down Expand Up @@ -206,9 +206,9 @@ public void Dispose()
protected virtual void Dispose(bool disposing) { }

/// <summary>
/// Returns the SerializationConfiguration for this stream reader.
/// Returns the <code>SerializationSettings</code> for this stream reader.
/// </summary>
internal SerializationConfiguration SerializationConfiguration { get; private set; }
internal SerializationSettings Settings { get; private set; }
#endregion

#region private
Expand Down
14 changes: 7 additions & 7 deletions src/FastSerialization/SegmentedMemoryStreamWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ namespace FastSerialization
{
public class SegmentedMemoryStreamWriter
{
public SegmentedMemoryStreamWriter(SerializationConfiguration config = null) : this(64, config) { }
public SegmentedMemoryStreamWriter(long initialSize, SerializationConfiguration config = null)
public SegmentedMemoryStreamWriter(SerializationSettings settings) : this(64, settings) { }
public SegmentedMemoryStreamWriter(long initialSize, SerializationSettings settings)
{
SerializationConfiguration = config ?? new SerializationConfiguration();
Settings = settings ?? throw new ArgumentNullException(nameof(settings));

if (SerializationConfiguration.StreamLabelWidth == StreamLabelWidth.FourBytes)
if (Settings.StreamLabelWidth == StreamLabelWidth.FourBytes)
{
writeLabel = (value) =>
{
Expand Down Expand Up @@ -100,14 +100,14 @@ public void WriteToStream(Stream outputStream)
public SegmentedMemoryStreamReader GetReader()
{
var readerBytes = bytes;
return new SegmentedMemoryStreamReader(readerBytes, 0, readerBytes.Count, SerializationConfiguration);
return new SegmentedMemoryStreamReader(readerBytes, 0, readerBytes.Count, Settings);
}
public void Dispose() { }

/// <summary>
/// Returns the SerializationConfiguration for this stream writer.
/// Returns the SerializerSettings for this stream writer.
/// </summary>
internal SerializationConfiguration SerializationConfiguration { get; private set; }
internal SerializationSettings Settings { get; private set; }

#region private
protected virtual void MakeSpace()
Expand Down
Loading

0 comments on commit a358962

Please sign in to comment.