Generic BP Nodes
Let’s say you wanted to implement a Blueprint node that can return the first element of a list (if the list is non-empty), with two exec pins for valid and invalid, very similar to say the cast node. Tackling first the valid and invalid exec pins, we can use a very useful UFUNCTION
meta specifier ExpandEnumAsExecs
.
UENUM(BlueprintType)
enum class EArrayHeadResult : uint8 {
Valid UMETA(DisplayName="Valid"),
Invalid UMETA(DisplayName="Invalid")
};
Once we have this enum, we can use it in our Blueprint function to find the head element of array of say FString
.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable,
meta=(DisplayName="Head", CompactNodeTitle="HEAD", ExpandEnumAsExecs="Result"))
static void Array_Head(const TArray<FString> &Array, FString &Head, EArrayHeadResult &Result);
};
The body of the function is simple; we just check if the Array
is not empty, we grab the first element, assign it to Head and also assign Result
to EArrayHeadResult::Valid
. Just a few lines; once we have it, we can use it in Blueprint.
Now, what if we wanted to use anything other than FString
, we would need to make another version of this method; we cannot use templates, because template is a compile-time C++ construct. When the compiler encounters [the first] usage of template <typename T>
, it generates a copy of the function, replacing all T
s for whatever the concrete type is. By the time we get to Blueprints, the code is compiled, and so it is not possible to use templates in Blueprint-exposed functions.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, meta=(...))
template <typename T>
static void Array_Head(const TArray<T> &Array, T &Head, EArrayHeadResult &Result);
};
Now what? If we were in pure C++ land, as a last resort we could specialise the template explicitly; that is to get the compiler to generate the appropriate instance of the template even without any implicit instantiations (think simply calls).
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, meta=(...))
template <typename T>
static void Array_Head(const TArray<T> &Array, T &Head, EArrayHeadResult &Result);
};
template<>
void UDreamCommonBPLibrary::ArrayHead(const TArray<FString>&, FString&, EArrayHeadResult&);
template<>
void UDreamCommonBPLibrary::ArrayHead(const TArray<int>&, int&, EArrayHeadResult&);
...
Not only is this almost as clumsy, but the UFUNCTION
macro is on the definition, not on the implementation; this means that the UFUNCTION
macro cannot be aware of the specialised versions. And so, template is not a solution. If templates are a no-go, is there a generic solution?
Generic Blueprint Node
The good news is that there must be a way; after all, we can use what seem to be able to use nodes like get (copy) and get (ref) that seems to be generic. Let’s see how we can achieve the same result; a Blueprint generic code without C++ templates.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, CustomThunk, meta=(ArrayTypeDependentParams="Head", ...))
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result);
};
Together with ArrayTypeDependentParams="Head"
, we can use int32 as a “reference” to a Blueprint-managed value; think of it as a int32
→ void*
mapping. the ArrayTypeDependentParams="Head"
indicates to the Blueprint runtime that we don’t mean int32
to be the concrete type, but rather the key that can be resolved to some underluing Blueprint-managed reference. It is now tempting to naïvely implement the function.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable,
meta=(DisplayName="Head", CompactNodeTitle="HEAD",
ArrayParm="Array", ArrayTypeDependentParams="Head",
ExpandEnumAsExecs="Result"))
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result) {
if (A.IsEmpty()) {
Result = EArrayHeadResult::Invalid;
} else {
Result = EArrayHeadResult::Valid;
Head = A[0];
}
}
}
The C++ code compiles, and we can use it in the Blueprint editor; the node can accept array of any type, and the type of the head element follows the type of the array element.
But when we run our game, it segfautls. The problem is that we are dealing with the int32
→ void*
Blueprint-managed references, but we are treating them as if they were plain integer values. (Here, we we have a double-free: we didn’t actually copy the underlying data, but had two objects that attempted to free the same underlying data. The first object is the first element of TArray<FString>
and the second is the copied FString. At the end of the execution, they both get destroyed, both freeing the same characters that make up the string, but only the first free can succeed.)
This breaks the Blueprint memory management, hence the segmentation fault. We are using fairly low-level Blueprint concepts, hence we need to break out low-level Blueprint code.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, CustomThunk, meta=(ArrayTypeDependentParams="Head", ...))
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result);
private:
DECLARE_FUNCTION(execArray_Head) {
...
}
static void genericArray_Head(
const void *Array, const FArrayProperty *ArrayProperty,
void *Head, const FProperty *HeadProperty,
void *Result, const FEnumProperty *ResultProperty) {
}
}
We break the Array_Head into three parts:
- the
Array_Head
declaration with theUFUNCTION
macro withCustomThunk
, - the
execArray_Head
DECLARE_FUNCTION
which provides the interface to the Blueprint runtime, - the
genericArray_Head
that implements the actual head algorithm.
The only good news is that the body of Array_Head
remains empty; we are left to write the Blueprint thunk on our own. The general pattern is to use the Stack to pull function parameters [just like you would in any other language]. For each parameter we pull its address and relevant FProperty
subclass.
Stack.StepCompiledIn<PropertyType>(nullptr);
void *Param = Stack.MostRecentPropertyAddress;
auto *ParamProperty = CastField<PropertyType>(Stack.MostRecentProperty);
if (!ParamProperty) {
Stack.bArrayContextFailed = true;
return;
}
Where PropertyType must satisfy the concept TIsDerivedFrom<T, FProperty>::IsDerived
(this is the Unreal standard library version of the std::derived_from
concept). In human-speak, we replace the PropertyType
for the concrete type; for example FArrayProperty
for arrays, FEnumProperty
for enums, FIntProperty
for integers, and so on. If the type is not known, the only thing that’s left is the most generic FProperty
. Using this code, for every parameter, we can obtain a pointer to it, and a descriptor that will allow us to manipulate the pointer in a safe‡ way. For example, for the first parameter const TArray<int32>&
, we’d have a const void*
and const FArrayProperty*
; the first being the pointer to the array and the second being an accessor / manipulator object we can use to handle the pointer.
‡ Terms and conditions apply.
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void *Array = Stack.MostRecentPropertyAddress;
const auto *ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
With this knowledge, we can implement the execArray_Head
thunk. Even though in the implementation, only the top-level name execArray_Head
is given, I still recommend that you use the same variable names as the parameter names (here Array
, Head
, and Result
); and append Property
for the FProperty
accessors. It is, of course, not required at all: stack is still a stack of nameless elements, but it helps to keep the code understandable.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, CustomThunk, ...)
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result);
private:
DECLARE_FUNCTION(execArray_Head) {
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void *Array = Stack.MostRecentPropertyAddress;
const auto *ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty) {
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
void *Head = Stack.MostRecentPropertyAddress;
const auto *HeadProperty = CastField<FProperty>(Stack.MostRecentProperty);
if (!HeadProperty) {
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FEnumProperty>(nullptr);
void *Result = Stack.MostRecentPropertyAddress;
const auto ResultProperty = CastField<FEnumProperty>(Stack.MostRecentProperty);
if (!ResultProperty) {
Stack.bArrayContextFailed = true;
return;
}
P_FINISH
P_NATIVE_BEGIN
genericArray_Head(
Array, ArrayProperty,
Head, HeadProperty,
Result, ResultProperty);
P_NATIVE_END
}
static void genericArray_Head(
const void *Array, const FArrayProperty *ArrayProperty,
void *Head, const FProperty *HeadProperty,
void *Result, const FEnumProperty *ResultProperty) {
}
}
The thunk is in two parts: the first part is from its first line to P_FINISH
which provides the inter-op layer between Blueprint runtime and C++, and then P_NATIVE_BEGIN
to P_NATIVE_END
, which is contains the native code. The P_*
macros are not doing anything too magical, they only ensure the right stack access, implement performance counters, and add scoping. Now that we have all this, we have handled the UFUNCTION
and its thunk; the only thing that remains is the implementation of the head algorithm.
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, CustomThunk, ...)
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result);
private:
DECLARE_FUNCTION(execArray_Head) {
...
}
static void genericArray_Head(
const void *Array, const FArrayProperty *ArrayProperty,
void *Head, const FProperty *HeadProperty,
void *Result, const FEnumProperty *ResultProperty) {
if (Array && Head && Result) {
FScriptArrayHelper ArrayHelper(ArrayProperty, Array);
if (ArrayHelper.Num() == 0) {
ResultProperty->
GetUnderlyingProperty()->
SetIntPropertyValue(Result, static_cast<uint64>(EArrayHeadResult::Invalid));
} else {
ResultProperty->
GetUnderlyingProperty()->
SetIntPropertyValue(Result, static_cast<uint64>(EArrayHeadResult::Valid));
HeadProperty->InitializeValue(Head);
HeadProperty->CopyCompleteValue(Head, ArrayHelper.GetElementPtr(0));
}
}
}
}
The only slight complication is that the algorithm–if we can use such a lofty name for first element of array or nothing–needs to be implemented using the Blueprint APIs, we don’t have any of the creature comforts of the direct C++ code; we need to go through the Blueprint layer. The thing worth noting is the “happy path”, namely where we copy the value to the Head
reference. We first need to invoke its constructor, using HeadProperty->InitializeValue(Head)
and then we can copy the value from ArrayHelper.GetElementPtr(0)
into Head
using HeadProperty->CopyCompleteValue(...)
. Think of this as calling the mathing C++ copy constructor or operator=.
Summary
In this post, we outlined the way to implement a Blueprint generic functions, and explored the Blueprint custom thunks and “generic” functions. You can use this approach to implement your own “generic” Blueprint functions in C++. Below is the complete listing for your copy-and-paste pleasure.
/*
Copyright (c) 2023, Dream on a Stick Limited. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must
display the following acknowledgement:
This product includes software developed by Dream on a Stick.
4. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DreamCommonBPLibrary.generated.h"
UENUM(BlueprintType)
enum class EArrayHeadResult : uint8 {
Valid,
Invalid
};
UCLASS()
class UDreamCommonBPLibrary : public UBlueprintFunctionLibrary {
GENERATED_UCLASS_BODY()
UFUNCTION(BlueprintCallable, CustomThunk,
meta = (DisplayName = "Head", CompactNodeTitle = "HEAD",
ArrayParm = "Array", ArrayTypeDependentParams = "Head", ExpandEnumAsExecs="Result"),
Category = "Dream|Utilities")
static void Array_Head(const TArray<int32> &Array, int32 &Head, EArrayHeadResult &Result);
private:
DECLARE_FUNCTION(execArray_Head) {
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void *Array = Stack.MostRecentPropertyAddress;
const auto *ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty) {
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
void *Head = Stack.MostRecentPropertyAddress;
const auto *HeadProperty = CastField<FProperty>(Stack.MostRecentProperty);
if (!HeadProperty) {
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FEnumProperty>(nullptr);
void *Result = Stack.MostRecentPropertyAddress;
const auto ResultProperty = CastField<FEnumProperty>(Stack.MostRecentProperty);
if (!ResultProperty) {
Stack.bArrayContextFailed = true;
return;
}
P_FINISH
P_NATIVE_BEGIN
genericArray_Head(
Array, ArrayProperty,
Head, HeadProperty,
Result, ResultProperty);
P_NATIVE_END
}
static void genericArray_Head(
const void *Array, const FArrayProperty *ArrayProperty,
void *Head, const FProperty *HeadProperty,
void *Result, const FEnumProperty *ResultProperty
) {
if (Array && Head && Result) {
FScriptArrayHelper ArrayHelper(ArrayProperty, Array);
if (ArrayHelper.Num() == 0) {
ResultProperty->
GetUnderlyingProperty()->
SetIntPropertyValue(Result, static_cast<uint64>(EArrayHeadResult::Invalid));
} else {
ResultProperty->
GetUnderlyingProperty()->
SetIntPropertyValue(Result, static_cast<uint64>(EArrayHeadResult::Valid));
HeadProperty->InitializeValue(Head);
HeadProperty->CopyCompleteValue(Head, ArrayHelper.GetElementPtr(0));
}
}
}
};
Leave a Reply