Code archives/Miscellaneous/(De)serialization llibrary

This code has been declared by its author to be Public Domain code.

Download source code

(De)serialization llibrary by Jim Teeuwen2008
This small library offers a more OOP oriented way to dynamically store and load object structures to/from a file.
This can be used in many situations; for instance, to store a game state for save-game purposes, or to record a demo.

I have had this code lying around for quite a while, but decided to post it here so it can be of some use to anyone who needs it.

The heart is a small interface that is implemented by the actual serializers. Posted below are 2 implementations for a serializer. The first stores the data in binary format. The second stores it as plain-text (ascii). There is a third that stores the data as an XML tree, but this one requires an external XML module and is therefor not included here.

Going through the code for the first 2 serializers, should make it easy enough to adapt it to any other format you wish to use.

Using it is piss-easy. Just create your game related types however you see fit. When you want to store an object in a file, do this:

local mygame:Game = new Game
...
Local bf:IFormatter = New BinaryFormatter;
local file:TStream = WriteFile( "mysavegame.xxx" );
bf.Serialize( file, mygame );
CloseFile(file);
...

And to Load the object from a file, do this:
Local bf:IFormatter = New BinaryFormatter;
local file:TStream = ReadFile( "mysavegame.xxx" );
mygame = Game( bf.Deserialize( file ) );
CloseFile(file);

Demo code is included at the very bottom of the code listing.

Note: It should be noted that this library is by no means feature complete. It will handle basic data types and custom types with basic data type fields just fine. Even arrays of any of the above as well as nested object trees. It does however not deal with TList and TMap objects. You will have to add these yourself.
'// IFormatter.bmx
Rem
	The Interface that all Formatters implement.
	Makes for a somewhat more OOP-compliant approach.
End Rem
Type IFormatter Abstract

	Method Serialize(fs:TStream, obj:Object) Abstract
	Method Deserialize:Object(fs:TStream) Abstract

End Type



'// BinaryFormatter.bmx
Type BinaryFormatter Extends IFormatter

	Const TYPETKN_UNDEFINED:Byte = 0;
	Const TYPETKN_BYTE:Byte = 1;
	Const TYPETKN_SHORT:Byte = 2;
	Const TYPETKN_INT:Byte = 3;
	Const TYPETKN_LONG:Byte = 4;
	Const TYPETKN_FLOAT:Byte = 5;
	Const TYPETKN_DOUBLE:Byte = 6;
	Const TYPETKN_OBJECT:Byte = 7;
	Const TYPETKN_STRING:Byte = 8;
	Const TYPETKN_ARRAY:Byte = 9;

	Method Deserialize:Object(fs:TStream)
		Return Self.RecursiveDeserialize(fs);
	End Method
	Method Serialize(fs:TStream, obj:Object)
		Self.RecursiveSerialize(fs, obj);
	End Method
	Method RecursiveDeserialize:Object(fs:TStream)
		Local strlen:Int = fs.ReadInt();
		Local name:String = fs.ReadString(strlen); 
		Local typeid:TTypeId = TTypeId.ForName(name);
		Local obj:Object = typeid.NewObject();
		Local token:Byte = GetTypeToken(typeid);

		Select (token)
			Case TYPETKN_UNDEFINED;
				For Local fld:TField = EachIn typeid.EnumFields()
					token = GetTypeToken(fld.TypeId());
					strlen = fs.ReadInt();
					name = fs.ReadString(strlen);

					Select (token)
						Case TYPETKN_BYTE;
							fld.SetInt(obj, fs.ReadByte());
				
						Case TYPETKN_SHORT;
							fld.SetInt(obj, fs.ReadShort());
				
						Case TYPETKN_INT;
							fld.SetInt(obj, fs.ReadInt());
				
						Case TYPETKN_LONG;
							fld.SetLong(obj, fs.ReadLong());
				
						Case TYPETKN_FLOAT;
							fld.SetFloat(obj, fs.ReadFloat());
				
						Case TYPETKN_DOUBLE;
							fld.SetDouble(obj, fs.ReadDouble());
				
						Case TYPETKN_STRING;
							strlen = fs.ReadInt();
							fld.SetString(obj, fs.ReadString(strlen));

						Case TYPETKN_ARRAY;
							ReadArray(fs, fld.Get(obj));

						Default  '// Undefined, object
							fld.Set(obj, RecursiveDeserialize(fs));
				
					End Select
				Next
		End Select

		Return obj;
	End Method
	Method RecursiveSerialize(fs:TStream, obj:Object)
		Local typeid:TTypeId = TTypeId.ForObject(obj);
		Local token:Byte = GetTypeToken(typeid);

		fs.WriteInt( typeid.Name().Length );
		fs.WriteString( typeid.Name() );
		
		Select (token)
			Case TYPETKN_UNDEFINED;
				For Local fld:TField = EachIn typeid.EnumFields()
					token = GetTypeToken(fld.TypeId());
					fs.WriteInt(fld.Name().Length);
					fs.WriteString(fld.Name());
						
					Select (token)
						Case TYPETKN_BYTE;
							fs.WriteByte( Byte(fld.GetInt(obj)) );
				
						Case TYPETKN_SHORT;
							fs.WriteShort( Short(fld.GetInt(obj)) );
				
						Case TYPETKN_INT;
							fs.WriteInt( fld.GetInt(obj) );
				
						Case TYPETKN_LONG;
							fs.WriteLong( fld.GetLong(obj) );
				
						Case TYPETKN_FLOAT;
							fs.WriteFloat( fld.GetFloat(obj) );
				
						Case TYPETKN_DOUBLE;
							fs.WriteDouble( fld.GetDouble(obj) );
				
						Case TYPETKN_STRING;
							fs.WriteInt( fld.GetString(obj).Length );
							fs.WriteString( fld.GetString(obj) );
				
						Case TYPETKN_ARRAY;
							WriteArray(fs, fld.Get(obj));

						Default  '// Undefined, object
							RecursiveSerialize(fs, fld.Get(obj));
				
					End Select
				Next
			End Select
		
	End Method
	Method WriteArray(fs:TStream, arr:Object)
		Local typeid:TTypeId = TTypeId.ForObject(arr);
		Local alen:Int = typeid.ArrayLength(arr) ;
		fs.WriteInt(alen);
		For Local n:Int = 0 To alen -1
			Local o:Object = typeid.GetArrayElement(arr, n);
			Local token:Byte = GetTypeToken(typeid.ElementType());
			fs.WriteByte(token);
			If( token = TYPETKN_UNDEFINED ) Then
				RecursiveSerialize(fs, o);
			Else If( token = TYPETKN_ARRAY ) Then
				WriteArray(fs, o);
			Else
				fs.WriteInt(o.ToString().Length);
				fs.WriteString(o.ToString());
			End If
		Next
	End Method
	Method ReadArray(fs:TStream, arr:Object)
		Local typeid:TTypeId = TTypeId.ForObject(arr);
		Local alen:Int = fs.ReadInt();
		For Local n:Int = 0 To alen -1
			Local token:Byte = fs.ReadByte();
			Local o:Object = typeid.GetArrayElement(arr, n);
			If( token = TYPETKN_UNDEFINED ) Then
				typeid.SetArrayElement(arr, n, RecursiveDeserialize(fs));
			Else If( token = TYPETKN_ARRAY ) Then
				ReadArray(fs, o);
			Else
				Local strlen:Int = fs.ReadInt();
				Local val:String = fs.ReadString(strlen);
				typeid.SetArrayElement(arr, n, val);
			End If
		Next
	End Method
	Method GetTypeToken:Byte(tid:TTypeId)
		Select (tid)
			Case ByteTypeId; Return TYPETKN_BYTE;
			Case ShortTypeId; Return TYPETKN_SHORT;
			Case IntTypeId; Return TYPETKN_INT;
			Case LongTypeId; Return TYPETKN_LONG;
			Case FloatTypeId; Return TYPETKN_FLOAT;
			Case DoubleTypeId; Return TYPETKN_DOUBLE;
			Case ObjectTypeId; Return TYPETKN_OBJECT;
			Case StringTypeId; Return TYPETKN_STRING;
			Case ArrayTypeId; Return TYPETKN_ARRAY;
			Default;
				If(tid.ExtendsType( ArrayTypeId )) Then
					Return TYPETKN_ARRAY;
				End If
				Return TYPETKN_UNDEFINED;
		End Select
	End Method

End Type




'// AsciiFormatter.bmx
Type AsciiFormatter Extends IFormatter

	Const TYPETKN_UNDEFINED:Byte = 0;
	Const TYPETKN_BYTE:Byte = 1;
	Const TYPETKN_SHORT:Byte = 2;
	Const TYPETKN_INT:Byte = 3;
	Const TYPETKN_LONG:Byte = 4;
	Const TYPETKN_FLOAT:Byte = 5;
	Const TYPETKN_DOUBLE:Byte = 6;
	Const TYPETKN_OBJECT:Byte = 7;
	Const TYPETKN_STRING:Byte = 8;
	Const TYPETKN_ARRAY:Byte = 9;

	Field Indent:Int;

	Method Deserialize:Object(fs:TStream)
		Return Self.RecursiveDeserialize( fs );
	End Method
	Method Serialize(fs:TStream, obj:Object)
		Indent = 0;
		Self.RecursiveSerialize(fs, obj);
	End Method
	Method RecursiveDeserialize:Object( fs:TStream )
		Local typeid:TTypeId = TTypeId.ForName( fs.ReadLine().Trim() );
		Local obj:Object = typeid.NewObject();
		Local token:Byte = GetTypeToken(typeid);

		Select (token)
			Case TYPETKN_UNDEFINED;
				For Local fld:TField = EachIn typeid.EnumFields()
					token = GetTypeToken( fld.TypeId() );

					Select (token)
						Case TYPETKN_ARRAY;
							ReadArray( fs, fld.Get(obj) );

						Case TYPETKN_UNDEFINED, TYPETKN_OBJECT;
							fld.Set( obj, RecursiveDeserialize( fs ) );

						Default;
							fld.Set( obj, GetVal(fs)[1] );
				
						End Select
				Next
		End Select
		Return obj;
	End Method
	Method RecursiveSerialize(fs:TStream, obj:Object)
		Local typeid:TTypeId = TTypeId.ForObject(obj);
		Local token:Byte = GetTypeToken(typeid);

		Write( fs, typeid.Name() );
		Indent :+ 1;
	
		Select (token)
			Case TYPETKN_UNDEFINED;
				For Local fld:TField = EachIn typeid.EnumFields()
					token = GetTypeToken(fld.TypeId());
	
					Select (token)
						Case TYPETKN_ARRAY;
							WriteArray(fs, fld.Get(obj), fld.Name());

						Case TYPETKN_UNDEFINED,  TYPETKN_OBJECT;
							RecursiveSerialize(fs, fld.Get(obj));
				
						Default;
							Write(fs, fld.Name() + "=" + fld.GetString(obj));

					End Select
				Next
		End Select

		Indent :- 1;

	End Method
	Method WriteArray( fs:TStream, arr:Object, name:String)
		Local typeid:TTypeId = TTypeId.ForObject(arr);
		Local alen:Int = typeid.ArrayLength(arr) ;
		Write(fs, name + " Size=" + alen);
		Indent :+ 1;
		For Local n:Int = 0 To alen -1
			Local o:Object = typeid.GetArrayElement(arr, n);
			Local token:Byte = GetTypeToken(typeid.ElementType());
			If( token = TYPETKN_UNDEFINED Or  token = TYPETKN_OBJECT) Then
				RecursiveSerialize(fs, o);
			Else If( token = TYPETKN_ARRAY ) Then
				WriteArray(fs, o, name);
			Else
				Write(fs, typeid.ElementType().Name() + "=" + o.ToString());
			End If
		Next
		Indent :- 1;
	End Method
	Method ReadArray( fs:TStream, arr:Object)
		Local typeid:TTypeId = TTypeId.ForObject(arr);
		Local alen:Int = Int(GetVal(fs)[1]);
		Local token:Byte = GetTypeToken( typeid.ElementType() );
		For Local n:Int = 0 To alen -1
			Local o:Object = typeid.GetArrayElement(arr, n);
			If( token = TYPETKN_UNDEFINED ) Then
				typeid.SetArrayElement(arr, n, RecursiveDeserialize( fs ));
			Else If( token = TYPETKN_ARRAY ) Then
				ReadArray(fs, o);
			Else
				typeid.SetArrayElement(arr, n, GetVal(fs)[1]);
			End If
		Next
	End Method
	Method GetTypeToken:Byte(tid:TTypeId)
		Select (tid)
			Case ByteTypeId; Return TYPETKN_BYTE;
			Case ShortTypeId; Return TYPETKN_SHORT;
			Case IntTypeId; Return TYPETKN_INT;
			Case LongTypeId; Return TYPETKN_LONG;
			Case FloatTypeId; Return TYPETKN_FLOAT;
			Case DoubleTypeId; Return TYPETKN_DOUBLE;
			Case ObjectTypeId; Return TYPETKN_OBJECT;
			Case StringTypeId; Return TYPETKN_STRING;
			Case ArrayTypeId; Return TYPETKN_ARRAY;
			Default;
				If(tid.ExtendsType( ArrayTypeId )) Then
					Return TYPETKN_ARRAY;
				End If
				Return TYPETKN_UNDEFINED;
		End Select
	End Method
	Method GetVal:String[](fs:TStream)
		Local line:String = fs.ReadLine();
		Local ret:String[] = [line[..line.Find("=")], line[line.Find("=") + 1..]];
		
		For Local n:Int = 0 To ret.Length -1
			ret[n] = ret[n].Trim();
		Next

		Return ret;
	End Method
	Method Write(fs:TStream, val:String)
		Local padding:String ="";
		For Local n:Int = 0 To Indent - 1
			padding :+ "  ";
		Next
		fs.WriteLine(padding + val);
	End Method

End Type



'// DEMO.bmx
SuperStrict
Framework brl.basic
Import brl.reflection

Include "IFormatter.bmx"
Include "BinaryFormatter.bmx"
Include "AsciiFormatter.bmx"

'// Some basic Game objects
Type Player
	Field Score:Int;
	Field Name:String;
	Field Location:Float[];

	Method New()
		Score = 0;
		Name = "Unnamed Player";
		Location = New Float[2];
	End Method

	Method ToString:String()
		Return (Name + " (" + Score + ")");
	End Method
End Type

Type Game
	Field Players:Player[];
	Field RandSeed:Int;
	Field Level:Int;

	Method New()
		Level = 1;
		RandSeed = MilliSecs();
		SeedRnd(RandSeed);

		'// All array objects MUST be initialized for the binaryformatter to work properly!
		'// The reason for this is that the routine that puts the values back in these lists
		'// expects the objects to be valid instances.
		Players = New Player[2];
		Players[0] = New Player;
		Players[1] = New Player;
	End Method

	Method ToString:String()
		Return ("Level: " + Level + ", " + Players[0].ToString() + ", " + Players[1].ToString() );
	End Method

End Type


'// Define our objects : (Un)comment here to use a different formatter.
Local bf:IFormatter = New BinaryFormatter;
'Local bf:IFormatter = New AsciiFormatter;

Local file:TStream = Null;
Local savefile:String = "game.sav";
Local g:Game = New Game;

'// Setup some basic game parameters with initial values to simulate a game in progress.
g.Level = 10;
g.Players[0].Name ="Bob";
g.Players[0].Score = Rand(0, 1000);
g.Players[0].Location[0] = Rand(0, 500);
g.Players[0].Location[1] = Rand(0, 500);
g.Players[1].Name ="Mike";
g.Players[1].Score = Rand(0, 1000);
g.Players[1].Location[0] = Rand(0, 500);
g.Players[1].Location[1] = Rand(0, 500);


'// Show the Initial output
Print("--- PRESAVE TEST ----------------------------------------------");
Print(g.ToString());
Print("x: " + g.Players[1].Location[0] + " y:" + g.Players[1].Location[1] );


'// Save the entire game to a file in it's current state.
Try
	file = WriteFile(savefile);
	bf.Serialize( file, g );
Catch e:Object
	Print(e.ToString());
End Try

If(file <> Null) Then 
	file.Close();
End If


'// clear game so we can load it up fresh from a file.
g = Null;
 
Try
	file = ReadFile(savefile);
	g = Game(bf.Deserialize( file ));
Catch e:Object
	Print(e.ToString());
End Try

If(file <> Null) Then 
	file.Close();
End If

'// bog out when something went wrong.
If( g = Null ) Then End;

'// Show the new output
'// If all went well, this should be identical to the presave output.
Print("--- POSTSAVE TEST ----------------------------------------------");
Print(g.ToString());
Print("x: " + g.Players[1].Location[0] + " y:" + g.Players[1].Location[1] );

End;

Comments

None.

Code Archives Forum