Redirecting arbitrary UClasses

A hack which lets you replace any UClass implementation with any other implementation (without CoreRedirects).
June Rhodes posted on Oct 29, 2020

Sometimes you really need to redirect a UClass implementation, and CoreRedirects aren’t enough. In my case, I needed to redirect the UOnlineEngineInterface implementation, even though the path is hard coded within the engine.

What is UOnlineEngineInterface?

First a bit of context. What even is this interface? Well this interface contains an assortment of methods relating to online functionality where the calling code doesn’t just use the online subsystem directly. The header is actually defined in the engine itself under Engine/Public/Net/OnlineEngineInterface.h, unlike the rest of the online code which tends to sit in plugins.

It contains everything from obtaining the online subsystem instance name for a particular world (used during play-in-editor with multiple instances running), to trying to automatically log in players in the base AGameSession implementation. That last case caused issues in the EOS Online Subsystem plugin, because calling AutoLogin with a user that is already signed in can be treated as a “please link an Epic Games account” and that would pop up a web browser:

bool AGameSession::ProcessAutoLogin()
{
    UWorld* World = GetWorld();

    FOnlineAutoLoginComplete CompletionDelegate = FOnlineAutoLoginComplete::CreateUObject(this, &ThisClass::OnAutoLoginComplete);
    if (UOnlineEngineInterface::Get()->AutoLogin(World, 0, CompletionDelegate))
    {
        // Async login started
        return true;
    }

    // Not waiting for async login
    return false;
}

Note: This code is from the Unreal Engine source code and is not MIT licensed.

bool AGameSession::ProcessAutoLogin()
{
    UWorld* World = GetWorld();

    FOnlineAutoLoginComplete CompletionDelegate = FOnlineAutoLoginComplete::CreateUObject(this, &ThisClass::OnAutoLoginComplete);
    if (UOnlineEngineInterface::Get()->AutoLogin(World, 0, CompletionDelegate))
    {
        // Async login started
        return true;
    }

    // Not waiting for async login
    return false;
}

In order to fix the issue of AGameSession causing a web browser to open, we basically need to turn this AutoLogin call into a no-op, instead of allowing the call to propagate to the online subsystem.

How does UOnlineEngineInterface get resolved?

When we look at the implementation of Get, we find that the engine loads the UClass implementation from a hard coded path:

UOnlineEngineInterface* UOnlineEngineInterface::Get()
{
    if (!Singleton)
    {
        // Proper interface class hard coded here to emphasize the fact that this is not expected to change much, any need to do so should go through the OGS team first
        UClass* OnlineEngineInterfaceClass = StaticLoadClass(UOnlineEngineInterface::StaticClass(), NULLTEXT("/Script/OnlineSubsystemUtils.OnlineEngineInterfaceImpl"), NULL, LOAD_Quiet, NULL);
        if (!OnlineEngineInterfaceClass)
        {
            // Default to the no op class if necessary
            OnlineEngineInterfaceClass = UOnlineEngineInterface::StaticClass();
        }

        Singleton = NewObject<UOnlineEngineInterface>(GetTransientPackage(), OnlineEngineInterfaceClass);
        Singleton->AddToRoot();
    }

    return Singleton;
}

Note: This code is from the Unreal Engine source code and is not MIT licensed.

UOnlineEngineInterface* UOnlineEngineInterface::Get()
{
    if (!Singleton)
    {
        // Proper interface class hard coded here to emphasize the fact that this is not expected to change much, any need to do so should go through the OGS team first
        UClass* OnlineEngineInterfaceClass = StaticLoadClass(UOnlineEngineInterface::StaticClass(), NULL, TEXT("/Script/OnlineSubsystemUtils.OnlineEngineInterfaceImpl"), NULL, LOAD_Quiet, NULL);
        if (!OnlineEngineInterfaceClass)
        {
            // Default to the no op class if necessary
            OnlineEngineInterfaceClass = UOnlineEngineInterface::StaticClass();
        }

        Singleton = NewObject<UOnlineEngineInterface>(GetTransientPackage(), OnlineEngineInterfaceClass);
        Singleton->AddToRoot();
    }

    return Singleton;
}

This is… not great. I don’t know about you, but I can’t exactly reach out to the OGS team. If you’re using a custom version of Unreal Engine, you can easily change the path yourself, but in my case I’m shipping a plugin that has to work with pre-built versions of Unreal Engine, so that’s not an option.

But hey, it’s a UClass path, so we should be able to use [CoreRedirects] to point the reference somewhere else right?

Not quite. When we dig into the implementation StaticLoadClass and debug it, we find that the only redirects that apply to this kind of call are Package Redirects, not Class Redirects. The problem is that the package it’s located in is OnlineSubsystemUtils. This package also contains the implementation of UIpNetDriver, making it impractical to copy or redirect the package as a whole.

So with [CoreRedirects] eliminated, we need to find a different strategy.

Ok, but how does any UObject get resolved?

Unreal Engine contains a bunch of hashtables that map string paths to UObject instances. If you’re interested in how this happens, you can take a look at HashObject inside UObjectHash.cpp.

If we could somehow register our implementation with the OnlineSubsystemUtils.OnlineEngineInterfaceImpl name in the hashtables, then when Unreal Engine goes to run StaticLoadClass, it will point at our code instead.

It will still think it’s loading the original class, but the hashtables have been pointed elsewhere, and that’s good enough for us.

So how do we modify the hashtables?

Well it turns out, this is actually pretty tricky. Most of the functions that surface modifying or interacting with these hashtables are private or not declared with CORE_API, meaning we can’t access them from our plugin code.

Interestingly enough though, UObjectBase has a LowLevelRename function. It’s declared protected which means we can access it on derived implementations of UObjectBase (which all UObjects are).

The problem is we’re not trying to rename the path to our UObject instance, we’re trying to rename the path to our UObject class. These are different. If we just call LowLevelRename inside a function on our UObject instance, we’d be redirecting a UClass into a UWhateverMyInstanceIs, and those types aren’t comparable at all.

The ‘protected’ keyword is really just a guideline

But the thing is, once you can get the pointer to a function, you can call it. We know that both UClass and UWhateverOurOverridingClassIs both inherit from UObjectBase. When we check UClass, we can see that at no point does it or any of it’s parents override LowLevelRename. Which means that if we grab the address of LowLevelRename from inside UWhateverOurOverridingClassIs and then we call it on our UClass, we’ll be able to effectively call LowLevelRename on the UClass instance without the limitations of protected getting in our way.

Doing that, looks like this:

void UOnlineEngineInterfaceEOS::DoHack()
{
    UPackage *OnlineSubsystemUtils = FindPackage(nullptrTEXT("/Script/OnlineSubsystemUtils"));

    (UOnlineEngineInterfaceEOS::StaticClass()->*&UOnlineEngineInterfaceEOS::LowLevelRename)(
        TEXT("OnlineEngineInterfaceImpl"),
        OnlineSubsystemUtils);
}
void UOnlineEngineInterfaceEOS::DoHack()
{
    UPackage *OnlineSubsystemUtils = FindPackage(nullptr, TEXT("/Script/OnlineSubsystemUtils"));

    (UOnlineEngineInterfaceEOS::StaticClass()->*&UOnlineEngineInterfaceEOS::LowLevelRename)(
        TEXT("OnlineEngineInterfaceImpl"),
        OnlineSubsystemUtils);
}

Now this code might look a bit weird, so let’s break it down for a moment.

We get the pointer to the LowLevelRename function with &UOnlineEngineInterfaceEOS::LowLevelRename. We get a pointer to our UClass instance with UOnlineEngineInterfaceEOS::StaticClass(). Then we use the ->* pointer-to-member operator to be able to invoke LowLevelRename in the context of the UClass pointer.

With our function call now operating in the context of our UClass, we call it and rename our UClass instance over the top of the one already registered by OnlineSubsystemUtils. Later, when StaticLoadClass runs, it looks up the hashtables as normal and loads our implementation instead.

Now it’s important to note, you must do this after the target module you are overriding has registered it’s UObject classes, but before any code has tried to actually get an instance with StaticLoadClass. Otherwise you can end up with either your override not working at all, or only parts of the runtime referring to your override.

For completeness, this is how you should invoke DoHack, within your StartupModule function:

void FOnlineSubsystemEOSUtilsModule::StartupModule()
{
    UClass *OnlineEngineInterfaceClass = StaticLoadClass(
        UOnlineEngineInterfaceEOS::StaticClass(),
        NULL,
        TEXT("/Script/OnlineSubsystemEOSUtils.OnlineEngineInterfaceEOS"),
        NULL,
        LOAD_Quiet,
        NULL);
    if (OnlineEngineInterfaceClass)
    {
        UOnlineEngineInterfaceEOS *Obj =
            NewObject<UOnlineEngineInterfaceEOS>(GetTransientPackage(), OnlineEngineInterfaceClass);
        Obj->DoHack();
    }
}
void FOnlineSubsystemEOSUtilsModule::StartupModule()
{
    UClass *OnlineEngineInterfaceClass = StaticLoadClass(
        UOnlineEngineInterfaceEOS::StaticClass(),
        NULL,
        TEXT("/Script/OnlineSubsystemEOSUtils.OnlineEngineInterfaceEOS"),
        NULL,
        LOAD_Quiet,
        NULL);
    if (OnlineEngineInterfaceClass)
    {
        UOnlineEngineInterfaceEOS *Obj =
            NewObject<UOnlineEngineInterfaceEOS>(GetTransientPackage(), OnlineEngineInterfaceClass);
        Obj->DoHack();
    }
}

Please don’t actually do this

It should go without saying, but modifying the UObject hashtables with pointer magic like this is a pretty big hack. You should try everything else - CoreRedirects, forking the engine, anything - before you go down this path. I would not be surprised if a future version of Unreal Engine breaks this code.

But at least for now, when you’re all out of options, you can have one last trick up your sleeve to really make the engine do what you want.

If you want to keep up to date with new blog posts, join our Discord. Feel free to discuss or ask questions about this blog post in the #gamedev channel!

All code examples are MIT licensed unless otherwise specified.