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;
});