Skip to main content

Engine (or pure C++) calling TypeScript

Useful when Unreal Engine needs to execute certain functionality inside of TypeScript.

Table Of Contents

Dynamic Delegates

TypeScript logic can be executed from within UObjects by binding to DYNAMIC_DELEGATE and DYNAMIC_MULTICAST_DELEGATE.

Example use-cases include:

  • Exporting TypeScript functions to C++.
  • UI actions (e.g OnPressed) and network messages that bind to TypeScript functionality.
C++
#pragma once

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

DECLARE_DYNAMIC_DELEGATE(FBasicNotify);
DECLARE_DYNAMIC_DELEGATE_OneParam(FNotifyWithRefString, FString&, InStringRef);
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(FString, FNotifyWithStringRet, FString, InString);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMulticastNotifyWithInt, int32, InInt);

UCLASS()
class ADemoActor : public AActor
{
GENERATED_BODY()

public:
virtual void BeginPlay() override {
// DECLARE_DYNAMIC_DELEGATE
BasicNotify.ExecuteIfBound();

// DECLARE_DYNAMIC_DELEGATE_OneParam
if (NotifyWithRefString.IsBound())
{
FString StringReference = "Example String...";
NotifyWithRefString.Execute(StringReference);
}

// DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam
if (NotifyWithStringRet.IsBound())
{
FString ReturnedString = NotifyWithStringRet.Execute("Example string");
UE_LOG(LogTemp, Warning, TEXT("ReturnedString: %s"), *ReturnedString); // "Example string" + " with some extra characters"
}

// DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam
MulticastNotifyWithInt.Broadcast(1337);
}

UFUNCTION()
static void PassJsFunctionAsDelegate(FBasicNotify Callback)
{
Callback.ExecuteIfBound();
}

public:
UPROPERTY()
FBasicNotify BasicNotify;

UPROPERTY()
FNotifyWithRefString NotifyWithRefString;

UPROPERTY()
FNotifyWithStringRet NotifyWithStringRet;

UPROPERTY()
FMulticastNotifyWithInt MulticastNotifyWithInt;
};

Note: UPROPERTY() exposes the delegate variable to puerts

TypeScript
// DECLARE_DYNAMIC_DELEGATE

function OnNotifyBasic(): void {
console.warn("BasicNotify Callback Executed.");
}
DemoActor.BasicNotify.Bind(OnNotifyBasic);
// DECLARE_DYNAMIC_DELEGATE_OneParam

import {$unref, $set} from 'puerts';

DemoActor.NotifyWithRefString.Bind((InStringRef) => {
console.warn("NotifyWithRefString: " + $unref(InStringRef)); // Print the value

$set(InStringRef, "Some new string!"); // Update the value by reference
});
// DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam

DemoActor.NotifyWithStringRet.Bind((InString) => {
return InString + " with some extra characters.";
});
// DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam

function OnMulticast(InInt: number): void {
console.warn("Multicast Callback 1: " + InInt);
}
DemoActor.MulticastNotifyWithInt.Add(OnMulticast);
DemoActor.MulticastNotifyWithInt.Add((InInt: number) => {
console.warn("Multicast Callback 2: " + InInt);
});

Defining Delegates From TypeScript

Instead of directly binding to DYNAMIC_DELEGATE inside of a UObject, new delegates can be constructed from within TypeScript.

Example use-cases include:

  • Passing delegates as parameters to C++
  • Manual control over delegate handles and their lifetime
TypeScript
// Delegate handle owned by UObject

import {toDelegate} from 'puerts';

function PrintHelloWorld() : void {
console.warn("Hello World!");
}

const NewDelegate = toDelegate(DemoActor, PrintHelloWorld); // toDelegate(owOwnerner: UE.Object, Func:Function)
DemoActor.PassJsFunctionAsDelegate(NewDelegate);
// Delegate handle with no owner (Manual Cleanup)

import {toManualReleaseDelegate, releaseManualReleaseDelegate} from 'puerts';

function PrintHelloWorld() : void {
console.warn("Hello World!");
}

const NewManualReleaseDelegate = toManualReleaseDelegate(PrintHelloWorld); // toManualReleaseDelegate(Func:Function)
DemoActor.PassJsFunctionAsDelegate(NewManualReleaseDelegate);

releaseManualReleaseDelegate(PrintHelloWorld); // Release to prevent memory leak

Using JsObject Types

Aside from delegates, JsObjects can be used within C++ to execute functions, access variables, e.t.c

C++
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "JsObject.h"
#include "DemoActor.generated.h"

UCLASS()
class ADemoActor : public AActor
{
GENERATED_BODY()

public:
UFUNCTION()
static void CalculateAge(FJsObject InPerson)
{
const int BirthYear = InPerson.Get<int>("BirthYear");
InPerson.Set<int>("Age", 2025 - BirthYear);
}

UFUNCTION()
static void ExecuteJsFunctionObject(FJsObject InJsFunctionObject)
{
UE_LOG(LogTemp, Warning, TEXT("ADemoActor:ExecuteJsFunctionObject() = %i"), InJsFunctionObject.Func<int>());
}
};
TypeScript
import * as UE from 'ue'

let John = {BirthYear:1999, Age: -1};
UE.DemoActor.CalculateAge(John);
console.warn("John is aged " + John.Age);

UE.DemoActor.ExecuteJsFunctionObject(() => {
return 1337;
});

Binding To std::function

Inside of a standard C++ environment, std::function is the preferred method for passing around function pointers. Puerts supports this as well.

C++
static void PassJsFunctionWithStd(std::function<int(int, int)> InFunction)
{
int ReturnValue = InFunction(88, 99);
UE_LOG(LogTemp, Warning, TEXT("ADemoActor:PassJsFunctionWithStd() | 89 + 99 = %i"), ReturnValue);
}
TypeScript
import * as UE from 'ue'

UE.DemoActor.PassJsFunctionWithStd((InA: number, InB: number) => {
return InA + InB;
});