How do you serialize and deserialize a UObject to/from an FArchive file?

Please consider the following code:

UMyObject* MyObject1 = NewObject<UMyObject>();
UMyObject* MyObject2 = NewObject<UMyObject>();

MyObject1->MyField = "Hello";
MyObject2->MyField = "Goodbye";

{
	auto Archive = std::unique_ptr<FArchive>(IFileManager::Get().CreateFileWriter(TEXT("myarchive.bin")));
	MyObject1->Serialize(*Archive); // 1
}

{
	auto Archive = std::unique_ptr<FArchive>(IFileManager::Get().CreateFileReader(TEXT("myarchive.bin")));
	MyObject2->Serialize(*Archive); // 2
}

On the line marked 1 it creates a file with the following content:

    00000000: 0a00 0000 0000 0000 0006 0000 0048 656c  .............Hel
    00000010: 6c6f 0004 0000 0000 0000 0000 0100 0000  lo..............
    00000020: 0000 0000                                ....

but on the line marked 2 I get the error:

LogSerialization: Error: Invalid boolean encountered while reading archive myarchive.bin - stream is most likely corrupted

What am I doing wrong? Do FArchive files need to have some sort of preamble written to them?

4 Likes

It looks like the error is coming from PossiblySerializeObjectGuid. The call chain is as follows:

FLazyObjectPtr::PossiblySerializeObjectGuid(this, Record);

calls:

Ar.Formatter.TryEnterField(Name, bEnterWhenWriting)

calls:

bool bValue = bEnterWhenWriting;
Inner << bValue;

So it’s trying to deserialize the bool bEnterWhenWriting, but gets a value of 10 (which is not 0 or 1) so it fails. I guess when the object was written it didn’t serialize bEnterWhenWriting for some reason?

(I guess I should figure out what a GUID is.)

What is FArchive for? Is there a benefit using it over the USaveGame?
UGameplayStatics library has some methods to write UObjects to a USaveGame easily.

UCLASS()
class UYourOwnSaveGame : public USaveGame
{
	GENERATED_BODY()

public:

	UPROPERTY()
		FString SomeInformation = "";

};
UYourOwnSaveGame* YourOwnSaveGame = Cast<UYourOwnSaveGame>(UGameplayStatics::CreateSaveGameObject(UYourOwnSaveGame::StaticClass()));

if (IsValid(YourOwnSaveGame )) {
  YourOwnSaveGame->SomeInformation = "Testing";

  if (UGameplayStatics::SaveGameToSlot(YourOwnSaveGame , TEXT("SomeSaveName"), 0)) {
	// success
  }
}
1 Like

Thanks @Sedal45, that’s certainly interesting and I’ll take a look at UGameplayStatics and USaveGame - but I’d really like to understand how to use the Core serialization library (ie Engine/Source/Runtime/Core/Public/Serialization) directly and how it works.

I’ve found that if I wrap the FArchive in a FObjectAndNameAsStringProxyArchive then it works as expected and the UObject is serialized and deserializes correctly with this different format:

00000000: 0800 0000 436f 6d6d 656e 7400 0c00 0000  ....Comment.....
00000010: 5374 7250 726f 7065 7274 7900 0a00 0000  StrProperty.....
00000020: 0000 0000 0006 0000 0048 656c 6c6f 0003  .........Hello..
00000030: 0000 0069 6400 0c00 0000 496e 7450 726f  ...id.....IntPro
00000040: 7065 7274 7900 0400 0000 0000 0000 0001  perty........... 
00000050: 0000 0005 0000 004e 6f6e 6500 0000 0000  .......None.....

As you can see each UPROPERTYs is seralized as name, type and value with run-length encoded strings. (I’m not sure what the “None” is on the end.)

But looking at the code for FObjectAndNameAsStringProxyArchive I’m not sure I get why it works, whereas using FArchive (FFileReader / FFileWriter) directly doesn’t. It’s functions don’t seem to be called.

I’ve walked through the working loading call (MyObject2->Serialize through FObjectAndNameAsStringProxyArchive ) in the debugger and I can’t quite figure out where the fields are actually read into the object.

I’ll just add the definition of UMyObject so you can see the two properties and their correspondance to the binary data:

UCLASS()
class TANGOCORE_API UMyObject : public UObject
{
	GENERATED_BODY()
public:
	UMyObject();

	UPROPERTY()
	FString Comment;

	UPROPERTY()
	int32 id;

    /*...*/
};

Ok, so I’ve figured it out… Here is the story:

UObject::Serialize calls operator<<(FArchive, FName) to serialize property names, and uses FName “None” as a “null-terminator” to indicate the end of the property list. That’s why in the correctly serialized file above “None” appears at the end of the property list to indicate it’s end. The property list is followed by an optional GUID which, when empty, is a 32-bit bool all zeros. That’s why there is 4 bytes of zeros after the None property in the corretly serialized version, to indicate no object GUID is present.

The reason that it doesn’t work with FArchive is that the default implementation of operator<<(FArchive, FName) is a no-op:

virtual FArchive& FArchive::operator<<(FName& Value)
{
	return *this;
}

consequently, when Saving, FArchive is told the property names and goes “yep got it”, and then places nothing in the file for the property names.

And now the fun part: Then later when Loading from FArchive, UObject::Serialize goes to load the first property and asks to deserialize the FName of the first property, passing in a default-constructed FName which is also the value of “None” (0). FArchive doesn’t touch the FName and returns (again, its a no-op - same operator<< function shown above). UObject::Serialize then takes this to mean it has read FName None from the archive indicating the end of the property list.

Thinking it has successfully read the (seemingly empty) property list, UObject::Serialize then goes onto read the optional GUID bool (with the file position at the start of the file), and it gets something other than 0 or 1. (It gets 10 which is the value of the first four bytes of the file little-endian: 0x0A) and then croaks with “Invalid boolean encountered while reading archive” in the Guid reading code.

The solution is the use FNameAsStringProxyArchive which is basically a tiny wrapper around FArchive that implements operator<<(FName) Rather than a no-op it writes it out a string:

FArchive& FNameAsStringProxyArchive::operator<<( class FName& N )
{
	if (IsLoading())
	{
		FString LoadedString;
		InnerArchive << LoadedString;
		N = FName(*LoadedString);
	}
	else
	{
		FString SavedString(N.ToString());
		InnerArchive << SavedString;
	}
	return *this;
}

I consider it a bug that UObject::Serialize is interpretting the default no-op implementation of FArchive::operator<<(FName) returning None indicating the end-of-the-property list and continuing on. They should be distinct paths, and it should provoke a proper error message.

8 Likes

Shouldn’t the FArchive operator functions be marked as pure virtual to force users to derive and implement or uses a derived class as you did? Or, if that’s not workable, at least throw a “not implemented” exception.

Thanks for posting your research results