C++ Introspection and ContainerPtrToValuePtr through FObjectProperty

On an old post (C++ Introspection and ContainerPtrToValuePtr). I’ve done a full introspection of an actor class, diging in arrays and structs to access basic properties.

Now I need to extend this introspection to go though components (ie. FObjectProperty). I’ve updated the code as following but I’ve probably missed something around pointers to get access to values inside a FObjectProperty.

Something is probably wrong below COMMENT1 and COMMENT2.

void AUnrSimulationActor::Dump() const
{
	for (TFieldIterator<FProperty> vPropIt(GetClass(), EFieldIterationFlags::IncludeSuper); vPropIt; ++vPropIt)
	{
		auto vProperty = *vPropIt;
		DumpAnyProperty(vProperty, this, GetName());
	}
}

void AUnrSimulationActor::DumpAnyProperty(const FProperty* aProperty, const void* aContainerPtr, const FString& aRelativePath, bool aAddPropertyNameToPath) const
{
	if (const FArrayProperty* vArrayProperty = CastField<const FArrayProperty>(aProperty))
	{
		auto vArrayName = vArrayProperty->GetAuthoredName();
		auto vArrayPtr = vArrayProperty->ContainerPtrToValuePtr<void>(aContainerPtr);
		FScriptArrayHelper vArrayHelper(vArrayProperty, vArrayPtr);
		auto vCount = vArrayHelper.Num();

		for (int vIndex = 0; vIndex < vCount; vIndex++)
		{
			auto vItemPath = aRelativePath + "." + vArrayName + "[" + FString::FromInt(vIndex) + "]";
			auto vArrayItemPtr = vArrayHelper.GetRawPtr(vIndex);

			DumpAnyProperty(vArrayProperty->Inner, vArrayItemPtr, vItemPath, false);
		}
	}
	else if (const FStructProperty* vStructProperty = CastField<const FStructProperty>(aProperty))
	{
		auto vStructPropertyName = vStructProperty->GetAuthoredName();
		auto vStructName = vStructProperty->Struct->GetName();
		auto vStructPath = aAddPropertyNameToPath ? aRelativePath + "." + vStructPropertyName : aRelativePath;

		UE_LOG(LogTemp, Display, TEXT("%s (%s)"), *vStructPath, *vStructName);

		DumpStructProperty(vStructProperty, aContainerPtr, vStructPath);
	}
        // COMMENT1 
	else if (const FObjectProperty* vObjectProperty = CastField<const FObjectProperty>(aProperty))
	{
		auto vObjectPropertyName = vObjectProperty->GetAuthoredName();
		auto vObjectPath = aAddPropertyNameToPath ? aRelativePath + "." + vObjectPropertyName : aRelativePath;

		DumpObjectProperty(vObjectProperty, aContainerPtr, vObjectPath);
	}
	else
	{
		DumpBasicProperty(aProperty, aContainerPtr, aRelativePath);
	}
}

// COMMENT2
void AUnrSimulationActor::DumpObjectProperty(const FObjectProperty* aObjectProperty, const void* aContainerPtr, const FString& aRelativePath) const
{
        // ERROR MAY BE  BELOW !!
	auto vObjectPtr = aObjectProperty->ContainerPtrToValuePtr<void>(aContainerPtr);

	for (TFieldIterator<FProperty> vPropIt(aObjectProperty->PropertyClass, EFieldIterationFlags::IncludeSuper); vPropIt; ++vPropIt)
	{
		auto vProperty = *vPropIt;
		DumpAnyProperty(vProperty, vObjectPtr, aRelativePath);
	}
}

void AUnrSimulationActor::DumpStructProperty(const FStructProperty* aStructProperty, const void* aContainerPtr, const FString& aRelativePath) const
{
	auto vStructPtr = aStructProperty->ContainerPtrToValuePtr<void>(aContainerPtr);

	for (TFieldIterator<FProperty> vPropIt(aStructProperty->Struct, EFieldIterationFlags::IncludeSuper); vPropIt; ++vPropIt)
	{
		auto vProperty = *vPropIt;
		DumpAnyProperty(vProperty, vStructPtr, aRelativePath);
	}
}

void AUnrSimulationActor::DumpBasicProperty(const FProperty* aProperty, const void* aContainerPtr, const FString& aRelativePath) const
{
	auto vPropertyPath = aRelativePath + "." + aProperty->GetAuthoredName();
	FString vIsNative = aProperty->IsNative() ? TEXT("CPP") : TEXT("BP");
	FString vValue = "Unknown";

	if (aProperty->GetClass() == FIntProperty::StaticClass())
	{
		auto vValuePtr = aProperty->ContainerPtrToValuePtr<int>(aContainerPtr);
		vValue = FString::FromInt(*vValuePtr);
	}
	else if (aProperty->GetClass() == FDoubleProperty::StaticClass())
	{
		auto vValuePtr = aProperty->ContainerPtrToValuePtr<double>(aContainerPtr);
		vValue = FString::SanitizeFloat(*vValuePtr);
	}
	else if (aProperty->GetClass() == FStrProperty::StaticClass())
	{
		auto vValuePtr = aProperty->ContainerPtrToValuePtr<FString>(aContainerPtr);
		vValue = *vValuePtr;
	}
	else if (aProperty->GetClass() == FBoolProperty::StaticClass())
	{
		auto vValuePtr = aProperty->ContainerPtrToValuePtr<bool>(aContainerPtr);
		vValue = *vValuePtr ? "true" : "false";
	}

	UE_LOG(LogTemp, Display, TEXT("%s %s %s"), *vPropertyPath, *vIsNative, *vValue);
}

I see you mostly copied the struct code for the object code.
Main difference between the two is that an object property is a pointer to object.
The value of the property (UObject*) may be null, so you need to check validity before iterating.
ContainerPtrToValuePtr returns a pointer to the value, so it’s an UObject**. Using <void> and auto keyword can lead to confusion in this case.

Also, not sure why you’re using aObjectProperty->PropertyClass, it will return the class-restriction of the property but not necessarily the class of the object. For example if the property is declared as APawn* Prop then propertyclass would be equal to APawn::StaticClass(), but the object assigned could be an ACharacter class (or a subclass). To get the true class of the object, use vObjectPtr->GetClass() (if not null).

UObject** vObjectValuePtr = aObjectProperty->ContainerPtrToValuePtr<UObject*>(aContainerPtr);
UObject* vObjectPtr = *vObjectValuePtr;

FString vIsNative = aObjectProperty->IsNative() ? TEXT("CPP") : TEXT("BP");
FString vValue = vObjectPtr ? vObjectPtr->GetName() : "None";
UE_LOG(LogTemp, Display, TEXT("%s %s %s"), *aRelativePath, *vIsNative, *vValue);

if (vObjectPtr) {
    for (TFieldIterator<FProperty> vPropIt(vObjectPtr->GetClass(), EFieldIterationFlags::IncludeSuper); vPropIt; ++vPropIt)
    { ... }
}

Be mindful of circular dependencies. This is most likely gonna crash, because an actor references its components, and components reference their owning actor. You’re gonna have to implement circular prevention in some way.

A simple way would be to add a TSet<UObject*> argument passed around by reference to each function, and fill it on-the-go as you are discovering new objects, and skip objects which you already visited.

I think if you want a more readable result however you should consider dumping on a per-object basis without nesting objects within each other. The code would be similar, but instead of dumping sub-objects properties in DumpObjectProperty, simply dump the object reference and fill the set. Then after dumping main object, dump referenced objects one by one.
Something like this (actually using TArray makes it easier as you can keep iterating while it is being filled)

void AUnrSimulationActor::Dump() const
{
    TArray<UObject*> ObjectsToDump;
    ObjectsToDump.Add(this); // start with this, will be filled on-the-go

    for (int i=0; i<ObjectsToDump.Num(); i++) {
        UObject* Obj = ObjectsToDump[i];
        for (TFieldIterator<FProperty> vPropIt(Obj->GetClass()); vPropIt; ++vPropIt)
            DumpAnyProperty(vPropIt, Obj, Obj->GetName(), ObjectsToDump);
    }
}

void AUnrSimulationActor::DumpObjectProperty(const FObjectProperty* aObjectProperty, const void* aContainerPtr, const FString& aRelativePath, TArray<UObject*>& ObjectsToDump) const
{
    UObject** vObjectValuePtr = aObjectProperty->ContainerPtrToValuePtr<UObject*>(aContainerPtr);
    UObject* vObjectPtr = *vObjectValuePtr;

    // Dump the reference only
    FString vIsNative = aObjectProperty->IsNative() ? TEXT("CPP") : TEXT("BP");
    FString vValue = vObjectPtr ? vObjectPtr->GetName() : "None";
    UE_LOG(LogTemp, Display, TEXT("%s %s %s"), *aRelativePath, *vIsNative, *vValue);

    // Object will be dumped later
    if (vObjectPtr)
        ObjectsToDump.AddUnique(vObjectPtr);
}

Also fixed with an older solution from @Chatouille Getting nested property values from properties? - #4 by Chatouille

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.