Disappearing JSON object memory?

I’m trying to parse JSON with the following general structure:

{
    "dialogue": [
        {
            "name": "Test 1",
            "image": "Qyvnily_WildFlower",
            "lines": [
                "Howie doin'?"
            ] 
        },
        {
            "name": "Test 2",
            "image": "Maya_Confused",
            "lines": [
                "Hi! Um. I'm Maya. Very happy to meet you!"
            ],
            "choices": [
                {
                    "text": "Test Question 1 Prompt?...",
                    "dialogue": [
                        {
                            "name": "Test Question 1",
                            "image": "Maya_Eldritch",
                            "lines": [
                                "So... testing 1?"
                            ]
                        },
                        {
                            "name": "Test Answer 1",
                            "image": "Qyvnily_WildFlower",
                            "lines": [
                                "Answering 1, certainly."
                            ]
                        },
                        {
                            "name": "Test Answer 1 Response",
                            "image": "Maya_Embarrassed",
                            "lines": [
                                "Cool, cool. ANyway..."
                            ],
                            "thoughts": [
                                "Sweet crumpets"
                            ]    
                        }
                    ]
                },
                {
                    "text": "Test Question 2 Prompt?...",
                    "dialogue": [
                        {
                            "name": "Test Question 2",
                            "image": "Maya_Happy",
                            "lines": [
                                "Hey how come test question 2?"
                            ]
                        },
                        {
                            "name": "Test Answer 2",
                            "image": "Qyvnily_WildFlower",
                            "lines": [
                                "You know the answer is test answer 2."
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

The idea is that characters are having a conversation and the conversation script supports conversation subtree processing in response to the player picking an option from the choices array (which is currently rendered as a list of UButtons in the UI).

The first pass works fine, rendering the initial conversation and the choice set, but when I try to render a dialogue subtree I run into a serious problem – the memory hosting the inflated JSON from FJsonSerializer::Deserialize() appears to have been freed if I try to access it from the context of a lambda function bound to the button OnClicked delegate! I get variably a SIGBUS or SIGSEGV crash citing non-zero memory address if I try to access references to the nested JSON objects. If I create a new shared pointer to the nested dialogue array I can get the correct number of entries reported when the lambda function executes, but trying to dereference any of them results in a segmentation fault crash.

My call flow looks like:

1. GenerateConversationUI() creates widgets to host the conversation content ->
2. ParseConversationScript() deserializes the JSON string and inflates it into JSON objects, storing a shared pointer to the result in a class member field CurrentScriptJsonObject ->
3. ParseDialogue() extracts the dialogue array of JSON objects from CurrentDialogueJsonObject and steps through each to generate UI elements representing them, adding these to the parent container widget. For the choices array, it generates UButtons whose OnClicked delegate gets bound to a lambda function; this lambda moves the current subdialogue array to a separate shared pointer class member, CurrentDialogueJsonObject, and calls ParseDialogue() again. Upon entering ParseDialogue() from the lambda exec context the extracted dialogue array has the correct number of elements, but dereferencing any causes a crash.

UUserWidget* UConversationStarter::GenerateConversationUI(const FString& Script)
{
    UUserWidget* ConvoWidget = CreateWidget<UUserWidget>(GetWorld(), ConvoBaseWidgetClass);
    UScrollBox* ScrollBox = Cast<UScrollBox>(ConvoWidget->WidgetTree->FindWidget(TEXT("DialogueScrollBox")));
    ParseConversationScript(Script);
    if (CurrentScriptJsonObject)
    {
        auto DialogueElementsArray = CurrentScriptJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
        UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; dialogue array has %d elements"), DialogueElementsArray.Num());

        // todo: set current dialogue json object in a desperate bid to keep the ■■■■ nested crap alive through stack pops
        CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
        CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, std::move(DialogueElementsArray));

        ParseDialogue(ConvoWidget, ScrollBox);
    }
    else 
    {
        UE_LOG(LogTemp, Error, TEXT("GenerateConvoUI; failed to extract top level script JSON object"));
    }
    return ConvoWidget;
}

void UConversationStarter::ParseConversationScript(const FString& Script)
{
    TSharedPtr<FJsonObject> ScriptJsonObject = MakeShareable(new FJsonObject());;
    auto Reader = TJsonReaderFactory<>::Create(Script);
    if (FJsonSerializer::Deserialize(Reader, ScriptJsonObject))
    {
        UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; deserialized json"));
        CurrentScriptJsonObject = ScriptJsonObject;
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; deserializing json failed"));
    }
}

void UConversationStarter::ParseDialogue(UUserWidget* ConvoWidget, UPanelWidget* Container)
{
    auto DialogueElementsArrayExperiment = CurrentDialogueJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
    UE_LOG(LogTemp, Warning, TEXT("ParseDialogue; current dialogue object has dialogue array of %d elements"), DialogueElementsArrayExperiment.Num());
    for (auto DialogueElement : DialogueElementsArrayExperiment)
    {
        const TSharedPtr<FJsonObject>* DialogueObject;
        if (DialogueElement->TryGetObject(DialogueObject))
        {
            ...
            
            const TArray<TSharedPtr<FJsonValue>>* ChoicesArray;
            if ((*DialogueObject)->TryGetArrayField(KEY_ARRAY_CHOICES, ChoicesArray))
            {
                ...
                
                // populate choiceswidget with buttons hosting the choices array text
                for (auto Choice : *ChoicesArray)
                {
                    const TSharedPtr<FJsonObject>* ChoiceObject;
                    if (Choice->TryGetObject(ChoiceObject))
                    {
                        auto* ChoiceButton = ChoicesWidget->WidgetTree->ConstructWidget<ULambdaButton>();
                        ...
                        const TArray<TSharedPtr<FJsonValue>>* SubDialogueElements;
                        ...
						if (Choice->AsObject()->TryGetArrayField(KEY_ARRAY_DIALOGUE, SubDialogueElements))
						{
                            // todo: set current dialogue json object in a desperate bid to keep the ■■■■ nested crap alive through stack pops
                            CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
                            CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, std::move(DialogueElementsArray));
							// install subdialogue elements to OnClick lambda event
							ChoiceButton->LambdaEvent.BindLambda([&]() 
							{
								ParseDialogue(ConvoWidget, Container);
							});
						}
                    }
                    else
                    {
                        UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; choice jsonval could not be converted to object"));
                    }
                }
            }
            else
            {
                UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; no choices found"));
            }
            ...
        }
    }
}

At first I assumed the problem was that the shared pointers created by FJsonSerializer::Deserialize() must be dying off when the scope in which FJsonSerializer::Deserialize() was called is popped off the call stack, but I tried manually placing the Deserialize() call in its own scope and analyzing right after and it came up with the correct dialogue elements count, and allows me to dereference them:

void UConversationStarter::ParseConversationScript(const FString& Script)
{
    {
        TSharedPtr<FJsonObject> ScriptJsonObject = MakeShareable(new FJsonObject());;
        auto Reader = TJsonReaderFactory<>::Create(Script);
        if (FJsonSerializer::Deserialize(Reader, ScriptJsonObject))
        {
            UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; deserialized json"));
            CurrentScriptJsonObject = ScriptJsonObject;
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("ParseConvoScript; deserializing json failed"));
        }
    }
    // NOTE: dialogue elements are still alive at this point, so it isn't simply leaving scope that causes the problem.
    auto DialogueElementsArrayExperiment = CurrentScriptJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
    UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current script object has dialogue array of %d elements"), DialogueElementsArrayExperiment.Num());
    
    const TSharedPtr<FJsonObject>* DialogueObject;
    if (DialogueElementsArrayExperiment[0]->TryGetObject(DialogueObject))
    {
    UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current script object's dialogue array first element has image named %s"), *(*DialogueObject)->GetStringField(KEY_STRING_IMAGE));
    }

    CurrentDialogueJsonObject = MakeShareable(new FJsonObject());
    CurrentDialogueJsonObject->SetArrayField(KEY_ARRAY_DIALOGUE, DialogueElementsArrayExperiment);
    auto DialogueElementsArrayStat = CurrentDialogueJsonObject->GetArrayField(KEY_ARRAY_DIALOGUE);
    UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current dialogue object has dialogue array of %d elements"), DialogueElementsArrayStat.Num());
    const TSharedPtr<FJsonObject>* DialogueObject2;
    if (DialogueElementsArrayStat[0]->TryGetObject(DialogueObject2))
    {
        UE_LOG(LogTemp, Warning, TEXT("ParseConvoScript; out scope of deserialize, current dialogue object's dialogue array first element has image named %s"), *(*DialogueObject2)->GetStringField(KEY_STRING_IMAGE));
    }
}

So I’m not really sure where that leaves me… how can I keep a handle to the entire inflated JSON DOM, including nested objects, regardless of scope and call stack?

Full source: ryddelmyst/Ryddelmyst_5.4/Source/Ryddelmyst/Private/ConversationStarter.cpp at 31-add-lore-to-megyle-demo · mysterymagination/ryddelmyst · GitHub

Could moving TSharedPtr<FJsonObject> ScriptJsonObject higher up in the scope fix this? TRy moving it to a UPROPERTY in the header and then recycle it during the ParseConversationScript.

1 Like

I made both CurrentScriptJsonObject and CurrentDialogJsonObject class members of the ConversationStarter class that runs these functions, so that should have done the trick. I can’t make it a UPROPERTY since TSharedPtr<> doesn’t support reflection (or something, it gives me the “X is not a class, enum etc. name” compile error if I try to make it a UPROPERTY. The ConversationStarter itself is UObject subclass that is a UPROPERTY of an ActorComponent I have initiating the conversation.

EDIT: also tried removing CurrentScriptJsonObject and CurrentDialogJsonObject from the equation and just keeping the ScriptJsonObject shared pointer as a class member field so I’m only ever accessing the same JsonObject that I Deserialize() into; with this setup I still get a “SIGBUS: invalid attempt to access memory at address ” crash if I extract the dialogue subtree array and pass a reference to it to the lambda exec call to ParseDialogue(). However, if I extract the dialogue array afresh from ScriptJsonObject in ParseDialogue() every time the access succeeds and I don’t crash. Unfortunately this isn’t very helpful since it only allows me to unpack the same 1st level dialogue array over and over, but it is interesting! Now that I have the original guy sticking around maybe I can successfully create shared pointers to subtrees within him? We shall see. Maybe. I might also nope out of navigating the Unreal JSON memory labyrinth and just convert the entire JSON DOM into one or more USTRUCTs on the first pass so I can dodge the whole darn thing.

Take home point: for some reason it seems key to maintain the same JsonObject instance I populated with Deserialize(); even making new shared pointers to it seems to somehow allow nested JsonObjects to be deallocated.

This may not be the issue, but in your recursive method you are setting global variable value from within the for loop, outside the lambda, so it cannot work as expected there.

In the for loop, you should probably declare it as a local variable (shared ptr), capture it (by value) in the lambda, and assign it to the global variable once in the lambda, like so

auto NextDialogueJsonObject = MakeShareable(...);
ChoiceButton->LambdaEvent.BindLambda([this, NextDialogueJsonObject]() {
    this->CurrentDialogueJsonObject = NextDialogueJsonObject;
    this->ParseDialogue(this->ConvoWidget, this->Container);
});

Alternatively, passing the shared ptr as a function parameter should also do the job, without requiring a global var, unless you also need it somewhere else.

Also regarding the issue, I’d start by removing those std::move statements.

1 Like

the line

CurrentDialogueJsonObject.Get()->SetArrayField(KEY_ARRAY_DIALOGUE, std::move(DialogueElementsArray));

would cause the invalidation of DialogueElementsArray causing the later to be empty when passed on to ParseDialogue.

After removing that part it is now populated during the DialogueElementsArray for itteration.
ConversationStarter.cpp (26.6 KB)

As for further fixes it’s a bit hard to narrow down what else needs fixing as many parts of the code is commented out, so I’m not sure it these parts are meant to be executed.

What mechanism triggers the conversations in game? I’ve tried triggering interact on actors with Conversational components but nothing pops up besides a short descriptor.

StartConversation_Implementation doesn’t seem to be triggered in game, is it called anywhere?

1 Like

yeah I know the logic is a mess at the moment; I was tossing intended functionality out the window in favor of trying to find a way to keep the inflated JSON alive. I’m pretty sure I tried what you suggest, but I’ll give it another (cleaner) shot and report back.

StartConversation_Implementation() is called by RyddelmystCharacter.cpp in Interact() when the interacted-with Actor has the InteractCapability::TALKABLE interact capability. That call flow catches the generated UUserWidget and send it to RyddelmystHUD for display.

Mmmokay, so my findings are a bit confusing; firstly I had a separate memory error that was masking the progress I should have been making with the JSON objects. Once I moved the top level ConvoWidget and Container widget into class member UPROPs the error I was seeing inside the for loop went away. However, I still got segmentation fault crashes if I tried something like:

for (auto Choice : *ChoicesArray)
{
  const TSharedPtr<FJsonObject>* ChoiceObject;
  if (Choice->TryGetObject(ChoiceObject))
  {
      ...
      const TArray<TSharedPtr<FJsonValue>>* SubDialogueElements;
      if (ChoiceObject->TryGetArrayField(KEY_ARRAY_DIALOGUE, SubDialogueElements))
      {						
         ChoiceButton->LambdaEvent.BindLambda([&]() 
         {
            ParseDialogue(*SubDialogueElements);
         });
      }
  } 
}

As soon as I start iterating over the Tarray of subdialogueelements passed to ParseDialogue() from the lambda exec, the game crashes. Still not sure why that would be unless the subdialogueelements and/or the TArray itself are allocated on-demand (such that they would go out of scope before lambda execution) and the inflated JSON lives in some other memory structure internally.

Anyway, I changed my API so ParseDialogue() accepts a TSharedPtr<JsonObject> and then pass the dereferenced ChoiceObject into the lambda capture-by-value and from there into ParseDialogue(); this way I can get a handle to the nested JSON objects and have them persist as the shared pointer ref count stays gt 0. What an odd journey. This is why I prefer managed memory XD

Here’s the working rig:

void UConversationStarter::ParseDialogue(TSharedPtr<FJsonObject> DialogueObject)
...
const TSharedPtr<FJsonObject>* ChoiceObject;
if (Choice->TryGetObject(ChoiceObject))
{
  auto ChoiceJsonObject = *ChoiceObject;
  ...
  // install subdialogue elements to OnClick lambda event   
  ChoiceButton->LambdaEvent.BindLambda([this, ChoiceJsonObject]() 
  {
    ParseDialogue(ChoiceJsonObject);
  });
}

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