UStruct() refcounting: early garbage collection causing a crash

You can try this out yourself – make a new native Actor with the following code, place it in your game, and it will crash in about 1 minute after you start the game (once garbage collection happens)

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CrashyActor.generated.h"

UCLASS()
class UArrayIdx : public UObject
{
	GENERATED_BODY()
public:
	UArrayIdx() {}
	~UArrayIdx() { mArrayIdx = 999999; }

	int mArrayIdx;
};

USTRUCT(BlueprintInternalUseOnly)
struct FArrayIdxHolder
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(Transient)
	TObjectPtr<UArrayIdx> mInner;
};

UCLASS()
class ACrashyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	ACrashyActor() {
		mMyArray.AddDefaulted(100);
		PrimaryActorTick.bCanEverTick = true;
	}

	virtual void Tick(float DeltaTime) override {
		if (!mArrayIdx.mInner) {
			mArrayIdx.mInner = NewObject<UArrayIdx>();

			mpArrayIdx = &mArrayIdx; // this should hold a strong reference to the struct
			FObjectPtr(&mArrayIdx).Get()->AddToRoot(); // this protect the struct from garbage collection
			
			mArrayIdx.mInner->mArrayIdx = 10;
		}
		check(!mArrayIdx.mInner->GetMaskedFlags(RF_BeginDestroyed | RF_FinishDestroyed));

		mMyArray[mArrayIdx.mInner->mArrayIdx] += DeltaTime;
	}

	FArrayIdxHolder mArrayIdx;

	UPROPERTY(Transient, VisibleAnywhere)
	TArray<float> mMyArray;

	TObjectPtr<FArrayIdxHolder> mpArrayIdx;
};

My question is: does anyone know a good way to fix this code, using -only- logic in the AActor class, and not touching any of the code in the UObject or UClass?

Context:

In my case, the USTRUCT is actually FAnimNode_RetargetPoseFromMesh, and the UObject is its member variable, TObjectPtr<UIKRetargetProcessor> Processor. My game is crashing when the Processor object gets garbage collected, even though my AnimNode is still alive and ticking.

As these are both engine classes that I don’t want to fork, I’m looking for a solution that doesn’t involve modifying either of these classes

To further complicate these matters, the AnimNode is stored inside of ANOTHER UStruct(), FAnimInstanceProxy, and I can’t convert my derived class to a UCLASS()

Background:

I’ve been trying to make an AnimInstance (also known as AnimBlueprint) in Native code, so that I can reuse some generic logic across multiple skeletons.

When trying to make my AnimGraph (implemention of AnimNodes) in native code, I followed the pattern of some of the logic in AnimPreviewInstance. (In particular, check out UE_5.0\Engine\Source\Editor\AnimGraph\Public\AnimPreviewInstance.h, line 147)

Here’s some code that still crashes, and is more accurate towards the case I’m looking at with regard to my usage of FAnimNode_RetargetPoseFromMesh.


#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CrashyActor.generated.h"

// pretend that this is defined in an engine file that you can't modify
UCLASS()
class UMyIdxClass : public UObject
{
	GENERATED_BODY()
public:
	int mIdx;
};

// pretend that this is defined in an engine file that you can't modify,
// and you have to use it because it implements some complicated code that you don't want to fork
USTRUCT(BlueprintInternalUseOnly)
struct FArrayIdxHelper
{
	GENERATED_USTRUCT_BODY()

	void FindLastIdx(const TArray<float>& array) { GetLastIdxVal().mIdx = array.Num() - 1; }
	int GetLastIdx() { return GetLastIdxVal().mIdx; }

private:
	UMyIdxClass& GetLastIdxVal() {
		if (!mLastIdxVal) {
			mLastIdxVal = NewObject<UMyIdxClass>();
		}
		check(!mLastIdxVal->GetMaskedFlags(RF_BeginDestroyed | RF_FinishDestroyed));
		return *mLastIdxVal;
	}

	UPROPERTY(Transient)
	TObjectPtr<UMyIdxClass> mLastIdxVal;
};

UCLASS()
class ACrashyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	ACrashyActor() {
		mMyArray.AddDefaulted(100);
		PrimaryActorTick.bCanEverTick = true;
	}

	virtual void Tick(float DeltaTime) override {
		mArrayHelper.FindLastIdx(mMyArray);
		mMyArray[mArrayHelper.GetLastIdx()] += DeltaTime;
	}

	FArrayIdxHelper mArrayHelper;

	UPROPERTY(Transient, VisibleAnywhere)
	TArray<float> mMyArray;
};

Here’s one solution that I found, but it would be nice to know if there’s a better one that doesn’t involve using a hardcoded string.

(I can’t use GET_MEMBER_NAME_CHECKED here because the macro requires the variable to be visible from the function where it’s being used)


#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CrashyActor.generated.h"

// pretend that this is defined in an engine file that you can't modify
UCLASS()
class UMyIdxClass : public UObject
{
	GENERATED_BODY()
public:
	int mIdx;
};

// pretend that this is defined in an engine file that you can't modify,
// and you have to use it because it implements some complicated code that you don't want to fork
USTRUCT(BlueprintInternalUseOnly)
struct FArrayIdxHelper
{
	GENERATED_USTRUCT_BODY()

	void FindLastIdx(const TArray<float>& array) { GetLastIdxVal().mIdx = array.Num() - 1; }
	int GetLastIdx() { return GetLastIdxVal().mIdx; }

private:
	UMyIdxClass& GetLastIdxVal() {
		if (!mLastIdxVal) {
			mLastIdxVal = NewObject<UMyIdxClass>();
		}
		check(!mLastIdxVal->GetMaskedFlags(RF_BeginDestroyed | RF_FinishDestroyed));
		return *mLastIdxVal;
	}

	UPROPERTY(Transient)
	TObjectPtr<UMyIdxClass> mLastIdxVal;
};

UCLASS()
class ACrashyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	ACrashyActor() {
		mMyArray.AddDefaulted(100);
		PrimaryActorTick.bCanEverTick = true;
		HackReferenceHolder = nullptr;
	}

	virtual void Tick(float DeltaTime) override {
		mArrayHelper.FindLastIdx(mMyArray);
		mMyArray[mArrayHelper.GetLastIdx()] += DeltaTime;

		if (!HackReferenceHolder) {
			FProperty* prop = mArrayHelper.StaticStruct()->FindPropertyByName("mLastIdxVal");
			if (FObjectPtrProperty* objPtrProp = Cast<FObjectPtrProperty>(prop)) {
				HackReferenceHolder = objPtrProp->GetObjectPropertyValue(&mArrayHelper);
			}
		}
	}

	FArrayIdxHelper mArrayHelper;

	UPROPERTY(Transient, VisibleAnywhere)
	TArray<float> mMyArray;

	UPROPERTY(Transient, VisibleAnywhere)
	TObjectPtr<UObject> HackReferenceHolder;
};