Serialization Error/Bug When Using a ByteArray and readObject in an IExternalizable Class?

This post was written by Greg Jastrab. Read other posts by Greg Jastrab.

I’ve encountered some odd behavior that I would have expected to work when calling readObject() to de-serialize an array of anonymous objects. If anyone knows what’s going on here, please enlighten me. I’ve also filed a bug on Adobe’s bug tracking system if anyone wants to follow the progress at Adobe’s end.

I’m trying to read all of the bytes in the readExternal() function of a class implementing IExternalizable, so that I may use the position property to move back in stream in case I need to do so. The problem only seems to occur if I am trying to de-serialize an Array of anonymous objects. If I put plain old Strings in the Array it will work fine. I find this odd since I would expect

public function readExternal(input:IDataInput):void {
  var arr:Array = input.readObject() as Array;
}

to have the exact same behavior as:

public function readExternal(input:IDataInput):void {
  var ba:ByteArray = new ByteArray(); 
  var inputBytes:uint = input.bytesAvailable;
  input.readBytes(ba);
  var baBytes:uint = ba.bytesAvailable;
  var arr:Array = ba.readObject() as Array;
  trace("inputBytes == baBytes ?= " + (inputBytes == baBytes)); // traces "inputBytes == baBytes ?= true
}

AIR installer and code attached below…

The classes I’m trying to serialize are simple as I’m only trying to serialize an Array in this test application. (Excuse the poor code of assuming the items in the Array are Objects in the toString()

io/WorkingSerializer.as

package io {
 
	import flash.utils.IExternalizable;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
 
	public class WorkingSerializer implements IExternalizable {
 
		public var arr:Array;
 
		public function WorkingSerializer(a:Array=null) {
			arr = a ? a : [];
		}
 
		public function readExternal(input:IDataInput):void {
			arr = input.readObject() as Array;
		}
 
		public function writeExternal(output:IDataOutput):void {
			output.writeObject(arr);
		}
 
		public function toString():String {
			var s:String = "";
			for each(var obj:Object in arr) {
				s += "\n\t{ ";
				for(var p:String in obj)
					s += p + ": " + obj[p] + ",";
				s += " }\n";
			}
			return "[WorkingSerializer]\n arr: [ " + s + "]";
		}
 
	}
}

io/ProblemSerializer.as

package io {
 
	import flash.utils.ByteArray;
	import flash.utils.IExternalizable;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
 
	public class ProblemSerializer implements IExternalizable {
 
		public var arr:Array;
 
		public function ProblemSerializer(a:Array=null) {
			arr = a ? a : [];
		}
 
		public function readExternal(input:IDataInput):void {
			var ba:ByteArray = new ByteArray();
			input.readBytes(ba);
			arr = ba.readObject() as Array;
		}
 
		public function writeExternal(output:IDataOutput):void {
			output.writeObject(arr);
		}
 
		public function toString():String {
			var s:String = "";
			for each(var obj:Object in arr) {
				s += "\n\t{ ";
				for(var p:String in obj)
					s += p + ": " + obj[p] + ",";
				s += " }\n";
			}
			return "[ProblemSerializer]\n arr: [ " + s + "]";
		}
 
	}
}

And here is the main application file. I wrote it as an AIR application so install AIR first if you don’t have it.

SerializationIssue.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()"
						layout="vertical" horizontalAlign="center" width="500" height="500">
 
	<mx:Script>
		<![CDATA[
			import io.*;
 
			private const WORKING:String = "workingfile_v1";
			private const PROBLEM:String = "problemfile_v1";
 
			private function init():void {
				registerClassAlias("io.WorkingSerializer", io.WorkingSerializer);
				registerClassAlias("io.ProblemSerializer", io.ProblemSerializer);
 
				var wo:WorkingSerializer = new WorkingSerializer([{obj: "some"}, {obj: "items"}, {obj: "are"}, {obj: "here"}]);
				var s:FileStream = new FileStream();
				s.open(getFile(WORKING), FileMode.WRITE);
				s.writeObject(wo);
				s.close();
 
				var po:ProblemSerializer = new ProblemSerializer([{obj: "items"}, {obj: "in"}, {obj: "problem"}, {obj: "serializer"}]);
				s.open(getFile(PROBLEM), FileMode.WRITE);
				s.writeObject(po);
				s.close();
			}
 
			private function getFile(filename:String):File { return File.applicationStorageDirectory.resolvePath(filename); }
 
			private function readObj(readWorking:Boolean=false):void {
				var s:FileStream = new FileStream();
				s.open(getFile(readWorking ? WORKING : PROBLEM), FileMode.READ);
 
				if(readWorking) {
					var wo:WorkingSerializer = s.readObject() as WorkingSerializer;
					logger.text += wo + "\n\n";
					trace(wo);
				}
				else {
					var po:ProblemSerializer = s.readObject() as ProblemSerializer;
					logger.text += po + "\n\n";
					trace(po);
				}
				s.close();
			}
		]]>
	</mx:Script>
 
	<mx:HBox>
		<mx:Button label="Read Working Serializer" click="readObj(true)" />
		<mx:Button label="Read Problem Serializer" click="readObj()" />
	</mx:HBox>
 
	<mx:Panel title="Logging" width="100%" height="100%">
		<mx:TextArea id="logger" width="100%" height="100%" editable="false" wordWrap="true"
					 updateComplete="logger.verticalScrollPosition=logger.maxVerticalScrollPosition" />
	</mx:Panel>
 
</mx:WindowedApplication>

To test this out without having to compile it yourself, I’ve compiled it as an AIR installer: Serialization Issue AIR Application. And the source: Serialization Issue Source.

When I run the code below and click on the “Read Working Serializer” button I correctly see:

[WorkingSerializer]
 arr: [ 
	{ obj: some, }
 
	{ obj: items, }
 
	{ obj: are, }
 
	{ obj: here, }
]

get output to the console. But, if I click on the “Read Problem Serializer” button I get the following RangeError:

RangeError: Error #2006: The supplied index is out of bounds. 
at flash.utils::ByteArray/readObject() 
at io::ProblemSerializer/readExternal()[<path to environment>/SerializationIssue/src/io/ProblemSerializer.as:19] 
at flash.filesystem::FileStream/readObject() 
at SerializationIssue/readObj()[<path to environment>/SerializationIssue/src/SerializationIssue.mxml:40] 
at SerializationIssue/___SerializationIssue_Button2_click()[<path to environment>/SerializationIssue/src/SerializationIssue.mxml:51]

In the meantime I’ll find a workaround for this, but has anyone encountered anything similar to this or know why this isn’t working?

About the author: Greg Jastrab joined SmartLogic Solutions in June 2006 and leads all Flash/Flex/AIR development. Follow @gjastrab on twitter

Tags: , , ,

4 Responses to “Serialization Error/Bug When Using a ByteArray and readObject in an IExternalizable Class?”

  1. Greg Jastrab says:

    I just added a comment to the JIRA page of my bug, postulating what is happening with that code. The only thing I can think of for now, after digging through the AMF spec for a bit and playing with some manual parsing is that the byte array is not utilizing the reference tables that the buffer reading the externalizable class is maintaining.

    So I’ve been digging into the AMF spec for a bit, and have a guess at what’s going on here. I believe the reference tables used in AMF3 are not being utilized by the ByteArray after it has read all of the data.

    It seems that if the reference tables are not available to/used by the ByteArray this would explain why

    function readExternal(input:IDataInput):void {

    var arr:Array = input.readObject() as Array;
    }

    would work but

    function readExternal(input:IDataInput):void {
    var ba:ByteArray = new ByteArray();
    input.readBytes(ba);

    var arr:Array = ba.readObject() as Array;
    }

    would not work.

  2. Peter Farland says:

    Correct. I believe that ByteArray always expects AMF references to start at 0 and this is a problem for AMF data captured out of another stream (such as from an IExternalizable readObject() during deserialization of a NetConnection response).

    You could process the raw ByteArray yourself using lower level read operations, but you would need to know what state the reference table was in (and unfortunately this is not obtainable from a NetConnection response). So instead that leaves you with completely custom serialization in the readObject()/writeObject() - such as buffering the IExternalizable content in a separate ByteArray first so that you know the references start at 0.

  3. Greg Jastrab says:

    Thanks for confirming this Peter.

    I guess this makes sense that the ByteArray shouldn’t know about the reference table anyway, since it would be possible to read bytes into the ByteArray from multiple sources (different FileStreams) which would have separate reference tables.

  4. Niks says:

    hii, i like the article you wrote. i also wrote an article on Serialization here : http://kaniks.blogspot.com
    feel free to post your comments

    thanks
    cheers

Leave a Reply