Fastest C# Serializer and Infinitly Fast Deserializer for .NET, .NET Core and Unity.
- Fastest C# Serializer, code is extremely tuned by both sides of implementation and binary layout(see: performance)
- Deserialize/Reserialize is infinitly fast because formatter can access to serialized data without parsing/packing(see: architecture)
- Strongly Typed and C# Code as schema, no needs to other IDL like
.proto
,.fbs
... - Smart API, only to use
Serialize<T>
andDeserialize<T>
- Native support of Dictionary, MultiDictionary(ILookup)
- First-class support to Unity(IL2CPP), it's faster than native JsonUtility
ZeroFormatter is similar as FlatBuffers but ZeroFormatter has clean API(FlatBuffers API is too ugly, see: sample; we can not use reguraly) and C# specialized. If you need to performance such as Game, Distributed Computing, Microservices etc..., ZeroFormatter will help you.
for .NET, .NET Core
- PM> Install-Package ZeroFormatter
for Unity(Interfaces can reference both .NET 3.5 and Unity for share types)
- PM> Install-Package ZeroFormatter.Interfaces
- PM> Install-Package ZeroFormatter.Unity
Visual Studio Analyzer
- PM> Install-Package ZeroFormatter.Analyzer
Define class and mark as [ZeroFormattable]
.
[ZeroFormattable]
public class MyClass
{
[Index(0)]
public virtual int Age { get; set; }
[Index(1)]
public virtual string FirstName { get; set; }
[Index(2)]
public virtual string LastName { get; set; }
[IgnoreFormat]
public string FullName { get { return FirstName + LastName; } }
[Index(3)]
public virtual IList<int> List { get; set; }
}
class Program
{
static void Main(string[] args)
{
var mc = new MyClass
{
Age = 99,
FirstName = "hoge",
LastName = "huga",
List = new List<int> { 1, 10, 100 }
};
var bytes = ZeroFormatterSerializer.Serialize(mc);
var mc2 = ZeroFormatterSerializer.Deserialize<MyClass>(bytes);
// ZeroFormatter.DynamicObjectSegments.MyClass
Console.WriteLine(mc2.GetType().FullName);
}
}
Serializable target must mark ZeroFormattableAttribute
, there public property must be virtual
and requires IndexAttribute
.
Primitive, Enum, TimeSpan, DateTime, DateTimeOffset, IList<>
, IDictionary<>
, IReadOnlyList<>
, IReadOnlyDictionary<>
, ILookup<>
, byte[]
, ZeroFormatter.KeyTuple
and there nullable.
T[]
, List<T>
, Dictionary<TKey, TValue>
is not supported type. You should use IList<>
, IDictionary<>
instead.
Dictionary's key is native serialized in binary, but has limitation of keys. Key can use Primitive, Enum and ZeroFormatter.KeyTuple for mulitple keys.
TODO:...
ZeroFormatter.Unity works on all platforms(PC, Android, iOS, etc...). But it can 'not' use dynamic serializer generation due to IL2CPP issue. But pre code generate helps it. Code Generator is located in packages\ZeroFormatter.Interfaces.*.*.*\tools\zfc\zfc.exe
. zfc is using Roslyn so analyze source code, pass the target csproj
.
zfc arguments help:
-i, --input=VALUE [required]Input path of analyze csproj
-o, --output=VALUE [required]Output path(file) or directory base(in separated mode)
-s, --separate [optional, default=false]Output files are separated
-u, --unuseunityattr [optional, default=false]Unuse UnityEngine's RuntimeInitializeOnLoadMethodAttribute on ZeroFormatterInitializer
Generated formatters must needs register on Startup. By default, zfc generate automatic register code on RuntimeInitializeOnLoad
timing.
ZeroFormatter can not serialize Unity native types by default but you can make custom formatter by define pseudo type. For example create Vector2
to ZeroFormatter target.
#if INCLUDE_ONLY_CODE_GENERATION
using ZeroFormatter;
namespace UnityEngine
{
[ZeroFormattable]
public struct Vector2
{
[Index(0)]
public float x;
[Index(1)]
public float y;
public Vector2(float x, float y)
{
this.x = x;
this.y = y;
}
}
}
#endif
INCLUDE_ONLY_CODE_GENERATION
is special symbol of zfc, include generator target but does not includes compile.
Benchmarks comparing to other serializers.
ZeroFormatter is fastest(compare to protobuf-net, 2~3x fast) and has infinitely fast deserializer.
ZeroFormatter can become custom binary layout framework. You can create own typed formatter. For example, add supports Guid
and Uri
.
public class GuidFormatter : Formatter<Guid>
{
public override int? GetLength()
{
// If size is fixed, return fixed size.
return 16;
}
public override int Serialize(ref byte[] bytes, int offset, Guid value)
{
// BinaryUtil is helpers of byte[] operation
return BinaryUtil.WriteBytes(ref bytes, offset, value.ToByteArray());
}
public override Guid Deserialize(ref byte[] bytes, int offset, DirtyTracker tracker, out int byteSize)
{
byteSize = 16;
var guidBytes = BinaryUtil.ReadBytes(ref bytes, offset, 16);
return new Guid(guidBytes);
}
}
public class UriFormatter : Formatter<Uri>
{
public override int? GetLength()
{
// If size is variable, return null.
return null;
}
public override int Serialize(ref byte[] bytes, int offset, Uri value)
{
// Formatter<T> can get child serializer
return Formatter<string>.Default.Serialize(ref bytes, offset, value.ToString());
}
public override Uri Deserialize(ref byte[] bytes, int offset, DirtyTracker tracker, out int byteSize)
{
var uriString = Formatter<string>.Default.Deserialize(ref bytes, offset, tracker, out byteSize);
return (uriString == null) ? null : new Uri(uriString);
}
}
You need to register formatter on application startup.
ZeroFormatter.Formatters.Formatter<Guid>.Register(new GuidFormatter());
ZeroFormatter.Formatters.Formatter<Uri>.Register(new UriFormatter());
One more case, how to create generic formatter. KeyValuePair<TKey, TValue>
is also not suppoted type by default. Let's add support.
public class KeyValuePairFormatter<TKey, TValue> : Formatter<KeyValuePair<TKey, TValue>>
{
public override int? GetLength()
{
return null;
}
public override int Serialize(ref byte[] bytes, int offset, KeyValuePair<TKey, TValue> value)
{
var startOffset = offset;
offset += Formatter<TKey>.Default.Serialize(ref bytes, offset, value.Key);
offset += Formatter<TValue>.Default.Serialize(ref bytes, offset, value.Value);
return offset - startOffset;
}
public override KeyValuePair<TKey, TValue> Deserialize(ref byte[] bytes, int offset, DirtyTracker tracker, out int byteSize)
{
int size;
byteSize = 0;
var key = Formatter<TKey>.Default.Deserialize(ref bytes, offset, tracker, out size);
offset += size;
byteSize += size;
var value = Formatter<TValue>.Default.Deserialize(ref bytes, offset, tracker, out size);
offset += size;
byteSize += size;
return new KeyValuePair<TKey, TValue>(key, value);
}
}
And register generic resolver on startup.
ZeroFormatter.Formatters.Formatter.AppendFormatterResolver(t =>
{
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
var formatterType = typeof(KeyValuePairFormatter<,>).MakeGenericType(t.GetGenericArguments());
return Activator.CreateInstance(formatterType);
}
// return null means type is not supported.
return null;
});
Fixed Length Format
Fixed Length formats is ... (TODO:write description)
Enum is serialized there underlying type.
Type | Layout |
---|---|
Int16 | [short(2)] |
Int32 | [int(4)] |
Int64 | [long(8)] |
UInt16 | [ushort(2)] |
UInt32 | [uint(4)] |
UInt64 | [ulong(8)] |
Single | [float(4)] |
Double | [double(8)] |
Boolean | [bool(1)] |
Byte | [byte(1)] |
SByte | [sbyte(1)] |
Char | [ushort(2)] |
Decimal | [lo:int(4)][mid:int(4)][hi:int(4)][flags:int(4)] |
TimeSpan | [seconds:long(8)][nanos:int(4)] |
DateTime | [seconds:long(8)][nanos:int(4)] |
DateTimeOffset | [seconds:long(8)][nanos:int(4)] |
Int16? | [hasValue:bool(1)][short(2)] |
Int32? | [hasValue:bool(1)][int(4)] |
Int64? | [hasValue:bool(1)][long(8)] |
UInt16? | [hasValue:bool(1)][ushort(2)] |
UInt32? | [hasValue:bool(1)][uint(4)] |
UInt64? | [hasValue:bool(1)][ulong(8)] |
Single? | [hasValue:bool(1)][float(4)] |
Double? | [hasValue:bool(1)][double(8)] |
Boolean? | [hasValue:bool(1)][bool(1)] |
Byte? | [hasValue:bool(1)][byte(1)] |
SByte? | [hasValue:bool(1)][sbyte(1)] |
Char? | [hasValue:bool(1)][ushort(2)] |
Decimal? | [hasValue:bool(1)][lo:int(4)][mid:int(4)][hi:int(4)][flags:int(4)] |
TimeSpan? | [hasValue:bool(1)][seconds:long(8)][nanos:int(4)] |
DateTime? | [hasValue:bool(1)][seconds:long(8)][nanos:int(4)] |
DateTimeOffset? | [hasValue:bool(1)][seconds:long(8)][nanos:int(4)] |
Variable Length Format
TODO:...
Type | Layout | Note |
---|---|---|
byte[] | [length:int(4)][byte(length)] | if length = -1, indicates null |
String | [length:int(4)][utf8Bytes:(length)] | if length = -1, indicates null |
KeyTuple | [Item1:T1, Item2:T2,...] | T is generic type, T1 ~ T8, mainly used for Dictionary Key |
KeyTuple? | [hasValue:bool(1)][Item1:T1, Item2:T2 ...] | T is T1 ~ T8 |
FixedSizeList | [length:int(4)][elements:T...] | represents IList<T> where T is fixed length format. if length = -1, indicates null |
VariableSizeList | [byteSize:int(4)][length:int(4)][elementOffset...:int(4 * length)][elements:T...] | represents IList<T> where T is variable length format. if length = -1, indicates null |
Object Format
Object is variant of Variable Length Format that can define user own format. Layout is [byteSize:int(4)][lastIndex:int(4)][indexOffset...:int(4 * lastIndex)][Property1:T1, Property2:T2, ...]
. If byteSize = -1, indicates null.
Dictionary Format
| Dictionary | [length:int(4)][(key:TKey, value:TValue)
...] | represents IDictionary<TKey, TValue>
, if length == -1, indicates null |
| MultiDictionary | [length:int(4)][(key:TKey, elements:List<TElement>)
...] | represents ILookup<TKey, TElement>
, if length == -1, indicates null |
| LazyDictionary | [byteSize:int(4)][length:int(4)][buckets:FixedSizeList<int>
][entries:VariableSizeList<DictionaryEntry>
] | represents ILazyDictionary<TKey, TValue>
, if byteSize == -1, indicates null |
| DictionaryEntry | [hashCode:int(4)][next:int(4)][key:TKey][value:TValue] | substructure of LazyDictionary |
| LazyMultiDictionary | [byteSize:int(4)][length:int(4)][groupings:VariableSizeList<VariableSizeList<GroupingSemengt>>
] | represents ILazyLookup<TKey, TElement>
, if byteSize == -1, indicates null |
| GroupingSegment | [key:TKey] [hashCode:int(4)][elements:VariableSizeList<TElement>
] | substructure of LazyMultiDictionary
Struct Format
Struct is variant of both Fixed Length or Variable Length format. If all properties... TODO:...
EqualityComparer
TODO:...
https://github.com/neuecc/ZeroFormatter/blob/master/src/ZeroFormatter/Comparers/WireFormatEqualityComparers.cs https://github.com/neuecc/ZeroFormatter/blob/master/src/ZeroFormatter/Comparers/KeyTupleEqualityComparer.cs
Currently No and I have no plans. Welcome to contribute port to other languages.
Yoshifumi Kawai(a.k.a. neuecc) is a software developer in Japan.
He is the Director/CTO at Grani, Inc.
Grani is a top social game developer in Japan.
He is awarding Microsoft MVP for Visual C# since 2011.
He is known as the creator of UniRx(Reactive Extensions for Unity)
Blog: https://medium.com/@neuecc (English)
Blog: http://neue.cc/ (Japanese)
Twitter: https://twitter.com/neuecc (Japanese)
This library is under the MIT License.