Code archives/File Utilities/Object Loader

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

Download source code

Object Loader by Otus2008
Allows you to read or write an arbitrary object to a stream (eg. a file or over the network). Uses the reflection module to get runtime information about type structure (fields). May be of use when implementing saved games or multiplayer over the network.

For an example program and a module version, please download the zip file from here: http://jan.varho.org/blog/programming/blitz/blitzmax-object-loader-module/

Bug reports (and patches) appreciated!

UPDATE: Added checks for saving TList and TMap objects properly.
' Loads and saves any objects to a stream using the reflection module.
' Recursive - any and all objects referenced are also saved!
' Use the metadata key "no_save" to mark fields not to be saved.

SuperStrict


Import BRL.Reflection


'Saves an object to the stream specified
Function SaveObject(o:Object, s:TStream, tid:TTypeId = Null) 
	
	'Null?
	If o
		s.WriteByte True
	Else
		s.WriteByte False
		Return
	End If
	
	'Get type Id
	If tid = ObjectTypeId Then tid = TTypeId.ForObject(o)
	If Not tid Then tid = TTypeId.ForObject(o)
	
	'Save type name
	s.WriteInt tid.Name().length
	s.WriteString tid.Name() 
	
	'DebugLog "Saving "+tid.Name()
	
	'Primitive type?
	Select tid
	'Integers
	Case ByteTypeId
		s.WriteByte	Byte(String(o) )
		Return
	Case ShortTypeId
		s.WriteShort	Short(String(o) )
		Return
	Case IntTypeId
		s.WriteInt	Int(String(o) )
		Return
	Case LongTypeId
		s.WriteLong	Long(String(o) )
		Return
	'Floating Points
	Case FloatTypeId
		s.WriteFloat	Float(String(o) )
		Return
	Case DoubleTypeId
		s.WriteDouble	Double(String(o) )
		Return
	'Strings
	Case StringTypeId
		s.WriteInt	String(o).length
		s.WriteString	String(o)
		Return
	End Select
	
	'Array?
	If tid.Name().EndsWith("[]")
		'Save length
		s.WriteInt tid.ArrayLength(o)
		
		'Element type
		Local etid:TTypeId = TTypeId.ForName(tid.Name()[..tid.Name().length - 2]) 
		
		'Save elements
		For Local i:Int = 0 To tid.ArrayLength(o)-1
			SaveObject tid.GetArrayElement(o, i), s, etid
		Next
		Return
	End If
	
	'Map?
	If tid.Name()="TMap"
		Local m:TMap = TMap(o)
		
		'Save length
		Local l:Int = 0
		For Local key:Object = EachIn m.Keys()
			l:+1
		Next
		s.WriteInt l
		
		'Save key-value pairs
		For Local node:TNode = EachIn m
			SaveObject node.Key(), s
			SaveObject node.Value(), s
		Next
		Return
	End If
	
	'List?
	If tid.Name()="TList"
		Local l:TList = TList(o)
		
		'Save length
		s.WriteInt l.Count()
		
		'Save contents
		For Local obj:Object = EachIn l
			SaveObject obj, s
		Next
		Return
	End If
	
	'Save fields
	For Local f:TField = EachIn tid.EnumFields()
		If f.MetaData("no_save") Then Continue
		SaveObject f.Get(o), s, f.TypeId()
	Next
	
End Function

'Reads an object to the stream specified
Function LoadObject:Object(s:TStream) 
	
	'Null?
	Select s.ReadByte() 
	Case False
		Return Null
	Case True
	Default
		RuntimeError "Not an object"
	End Select
	
	'Get type Id
	Local tid:TTypeId = TTypeId.ForName(s.ReadString(s.ReadInt() ) )
	
	'DebugLog "Loading "+tid.Name()
	
	'Primitive type?
	Select tid
	'Integers
	Case ByteTypeId
		Return String.FromInt(s.ReadByte())
	Case ShortTypeId
		Return String.FromInt(s.ReadShort())
	Case IntTypeId
		Return String.FromInt(s.ReadInt())
	Case LongTypeId
		Return String.FromLong(s.ReadLong() ) 
	'Floating point numbers
	Case FloatTypeId
		Return String.FromFloat(s.ReadFloat())
	Case DoubleTypeId
		Return String.FromDouble(s.ReadDouble() ) 
	'Strings
	Case StringTypeId
		Return s.ReadString(s.ReadInt())
	End Select
	
	Local o:Object
	
	'Array?
	If tid.ElementType() 
		'Get length
		Local l:Int = s.ReadInt() 
		
		'Create array
		o = tid.NewArray(l)
		
		'Load elements
		For Local i:Int = 0 To l - 1
			Local obj:Object = LoadObject(s)
			If obj Then tid.SetArrayElement o, i, obj
		Next
		Return o
	End If
	
	'Map?
	If tid.Name()="TMap"
		Local m:TMap = New TMap
		
		'Get length
		Local l:Int = s.ReadInt()
		
		'Load key-value pairs
		For Local i:Int = 0 To l - 1
			Local key:Object = LoadObject(s)
			Local value:Object = LoadObject(s)
			m.Insert key, value
		Next
		Return m
	End If
	
	'List?
	If tid.Name()="TList"
		Local l:TList = New TList
		
		'Get length
		Local length:Int = s.ReadInt()
		
		'Load key-value pairs
		For Local i:Int = 0 To length - 1
			l.AddLast LoadObject(s)
		Next
		Return l
	End If
	
	'Create the object
	o = tid.NewObject()
	
	'Load fields
	For Local f:TField = EachIn tid.EnumFields() 
		If f.MetaData("no_save") Then Continue
		Local obj:Object = LoadObject(s) 
		If obj Then f.Set o, obj
	Next
	
	Return o
	
End Function

Comments

Macguffin2008
Hey Otus,

Thanks for the code! It works great for me, with one exception - multidimensional arrays.

I'm currently bombing out on both custom type 2d arrays, and regular 2d int arrays... not sure what the deal is yet, but wanted to post in case you want to look at it.


Otus2008
I don't think reflection supports multidimensional arrays. Could be wrong though. Have you tried arrays of arrays? That's usually the answer to multidim. array problems.


Macguffin2008
Good call - it doesn't. Looks like reflection doesn't return a multidimensional array as a TField.

In my case, I wrapped my calls for saving out the 2d arrays with a function that took the 2d array, mapped it onto a 1d array, and then did the reverse when loading it. My arrays are all equal in their x and y dimensions, so that went pretty easily. I imagine that if people need this for non-square arrays, you could imbed data into the stream with that info.


markcw2009
When trying to compile the module version of this I get an error.

Building test_module
Compiling:objload.bmx
Compile Error: Function can not return a value

Which is in this line but I don't understand what is going on, help?
'Compares two fields
Function CompareFieldNames(o1:Object, o2:Object) 
	Return TField(o1).Name().Compare(TField(o2).Name() ) ' ?
End Function



Otus2009
markcw: Sorry, it should be
Function CompareFieldNames:Int(o1:Object, o2:Object)

No idea how that slipped through... :p


markcw2009
Oh great, thanks Otus. I was able to get it compiling by rem'ing them out and replacing the lines where they are called with the code here. Also, the update for TMap and TList is not in the module code. Must have slipped through eh? :)


Code Archives Forum