UE5 C++ implements blueprint node wildcard parameter pins (CustomStructureParam, CustomThunk)

Preface

In the blueprint macro, when setting the pin, there is a pin type called wildcard. This type allows the parameter type to be unconstrained when used as a pin. The pin parameter type is checked (statically) at compile time. If the type does not conform to the execution logic in the macro, the compilation will report an error.

Blueprint creation macro

Wildcards are somewhat similar to generic programming settings in C++. If the parameter type is generic, the input type is not constrained. When compiling, check whether the input type is reasonable, that is, don't care about the type, only care about the logic.

Requirements Analysis

When using wildcards in blueprint macros, I had such an idea, can I dynamically determine the input type through the content of the input pin to differentiate the execution logic? Later, I thought that this is not possible. After all, the macro is the action of the static process, so the structure has been determined at runtime and such a setting cannot be completed. If you know a way, please tell me.

In C++, when exposing functions to blueprints, a design demand is encountered: write a factory method to complete object creation. The factory method provides parameter input. The parameter types are different, and the created objects will be different.

If such a demand is in C++, it can be achieved through function overloading. Overloading functions allows functions with the same name but different types. However, exposing functions to blueprints does not allow function overloading.

In fact, we can change our thinking. Although we cannot achieve the requirements through similar overloading, blueprints themselves are interpreted languages. When C++ "exposes" functions to blueprints through reflection, the types of passed parameters can be inferred at runtime. This can solve our needs, that is, different execution logic can be selected by different types of passed parameters.

How to pass parameters without limiting the type? This can be done with the help of wildcard features! Although the parameter type cannot be dynamically inferred in the blueprint macro, the parameters passed in by Unreal C++ exposed to the blueprint function are received in C++, and the type can be inferred! This can solve our problem!

Reflection parameters

UFUNCTION macro, annotated on the function, adds reflection features to the function. When you add the function specifier BlueprintCallable, this function will be exposed to the blueprint. By looking at the UHT generated intermediate gen compiled file, you can see that the parameters are converted and passed to C++.

For example: add the following function in the blueprint function library header file "UMyFunctionLibrary"

1//Sample Code
2UFUNCTION(BlueprintCallable)
3static void MyFunc(int32 Number);

The generated intermediate files can be found in the project path "\Intermediate\Build\Win64\UnrealEditor\Inc\ProtobufToturial\UHT"

  • MyFunctionLibrary.generated.h
  • MyFunctionLibrary.gen.cpp

Open the header file "MyFunctionLibrary.generated.h" to find the declaration of the corresponding reflection execution function created (excerpt)

1#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS \
2 \
3	DECLARE_FUNCTION(execMyFunc);
4
5
6#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS_NO_PURE_DECLS \
7 \
8	DECLARE_FUNCTION(execMyFunc);

Open the source file "MyFunctionLibrary.gen.cpp" and you can find the function definition code (excerpt)

1DEFINE_FUNCTION(UMyFunctionLibrary::execMyFunc)
2{
3	P_GET_PROPERTY(FIntProperty,Z_Param_Number);
4	P_FINISH;
5	P_NATIVE_BEGIN;
6	UMyFunctionLibrary::MyFunc(Z_Param_Number);
7	P_NATIVE_END;
8}

From the above code, we can see that the execMyFunc function is a reflective call function. The macros in the function parse the passed parameters, and then pass the parameters back to the C++ function. For the exec annotation function, it is generated by UHT. If we want to insert logic in the middle, we need to skip the UHT generation.

When this function is called, all parameters will imitate the function call characteristics, push the parameters into the stack, and then parse the data from the stack one by one. Check the P_GET_PROPERTY macro, and you will find that the parsed parameters get data from the stack. Refer to the following source code.

1#define P_GET_PROPERTY(PropertyType, ParamName)\
2	PropertyType::TCppType ParamName = PropertyType::GetDefaultPropertyValue();\
3	Stack.StepCompiledIn<PropertyType>(&ParamName);

Operation

First, you need to use two reflection tags in UFUNCTION (click the tag name to jump to the official description)

  • CustomThunk: Function specifier, marking the function to skip the UHT tool to generate the code, and you need to manually add the exec execution function for reflection.
  • CustomStructureParam: Function meta-specifier, expose the specified parameters as wildcards.

Write a function

1UFUNCTION(BlueprintCallable, CustomThunk, meta=(CustomStructureParam="InputValue"))
2static void WildcardFunc(const int& InputValue);

This function is declared in the header file of the custom blueprint function library. Because the function is marked with CustomThunk, it does not need to be defined.

Add exec function

In the reflection setting, when the function is marked with CustomThunk, you need to manually write the execution exec function. The following is the manually written corresponding function. The name must be the exec prefix plus the reflection function name.

Declaration in the header file

1DECLARE_FUNCTION(execWildcardFunc);

Definition in the source file

1DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
2{
3	//Write logic
4
5}

Add type detection

Since the interaction between blueprint and C++ is realized through reflection, Unreal implements the reflection mechanism, so for the data type in the blueprint, C++ completes a layer of wrapping. So at runtime, we can perform type inference by passing in parameters.

All the parameters passed into the blueprint will be pushed onto the stack in the same way as the language function call (this stack is not the stack in the computer), so all the data needs to be taken out from the stack during parsing. Since my function only has one parameter, it only needs to be taken out once. If you want to know how to take out multiple parameters, you can define a reflection function, compile it, and then check the generated intermediate gen file.

The reference code is as follows

 1DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
 2{
 3	//Move stack parameter index to the first parameter
 4	Stack.MostRecentProperty = nullptr;
 5	Stack.MostRecentPropertyAddress = nullptr;
 6	Stack.StepCompiledIn<FProperty>(nullptr);
 7
 8	FProperty* Property = Stack.MostRecentProperty;
 9	//Check type
10	if (Property->IsA(FIntProperty::StaticClass()))
11	{
12		int32* pNumber = Property->ContainerPtrToValuePtr<int32>(Stack.MostRecentPropertyContainer);
13	}
14	else if (Property->IsA(FBoolProperty::StaticClass()))
15	{
16		bool* pBool = Property->ContainerPtrToValuePtr<bool>(Stack.MostRecentPropertyContainer);
17	}
18	else if (Property->IsA(FObjectProperty::StaticClass()))
19	{
20		AActor** Object = Property->ContainerPtrToValuePtr<AActor*>(Stack.MostRecentPropertyContainer);
21	}
22	else if (Property->IsA(FArrayProperty::StaticClass()))
23	{
24		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property);
25		//Check element type
26		//ArrayProperty->Inner->IsA();
27		//Check the number of array elements
28		ArrayProperty->Inner->ElementSize;
29		//Get array content (assuming that the array stores Actors)
30		TArray<AActor*> Actors;
31		ArrayProperty->CopyCompleteValueToScriptVM(&Actors, Stack.MostRecentPropertyAddress);
32	}
33	P_FINISH;
34	P_NATIVE_BEGIN;
35	//Write local logic
36	P_NATIVE_END;
37}

In the reference case above, the analysis of arrays is actually quite special. Unreal provides a special wildcard for arrays, the meta-specifier "ArrayParm". This part will not be explained in this article, and can be discussed in another article later.

Engine version: 5.2

Translations:

Comment