Skip to content

Dispatch

Artem Podorozhko edited this page Jul 21, 2026 · 1 revision

Dispatch

CefDispatch is the WebUserInterface plugin's format-agnostic message routing module. It maps a uint32 message type to a factory, converts raw payload bytes into a typed C++ value, and optionally invokes a typed handler for that value.

Source: Source/CefDispatch

CefDispatch is intentionally independent of WebSockets, protobuf, JSON, and CEF. A transport such as CefWebSocketServer supplies the message type and bytes; CefDispatch decides how those bytes become an application value and where that value is handled.

Why use it

Use CefDispatch when a message stream contains multiple payload types and the application should not need a large switch statement or transport-specific decoding code at every call site.

It provides:

  • one registry for MessageType -> factory routing;
  • typed value wrappers for protobuf messages, structs, strings, or byte containers;
  • optional MessageType -> handler routing after decoding;
  • replace/unregister support for editor tools, tests, and controlled runtime reconfiguration;
  • thread-safe registry access using read/write locks;
  • no protobuf dependency in the core module.

Use it with CefWebSocketServer when browser messages need typed routing. Use it without WebSockets when another transport already provides a message type and payload bytes.

When to use it

CefDispatch is a good fit when:

  • message types are stable numeric IDs;
  • each type has a distinct decoder or application value;
  • multiple systems need to consume the same decoded message;
  • transport and business logic should remain separate;
  • the project uses a mixture of protobuf and custom binary/text payloads.

It may be unnecessary for a very small protocol with only one or two messages. In that case, decoding directly can be simpler. Do not treat the registry as a security boundary: validate message sizes, source permissions, schema fields, and authorization before invoking gameplay behavior.

Core flow

message type + raw bytes
          |
          v
FCefDispatchRegistry::Decode
          |
          v
FCefDispatchFactory -> TUniquePtr<ICefDispatchValue>
          |
          +--> typed value read by the caller
          |
          +--> FCefDispatchHandlerRegistry::Handle / Dispatch
                         |
                         v
                  typed application handler

The factory owns decoding. The returned ICefDispatchValue owns the decoded value until the caller releases its TUniquePtr.

Message type design

Message types are uint32 values. Define one authoritative mapping for the project and keep it shared by the sender and receiver. Avoid reusing an ID for a different schema without a protocol-version change.

Message type Meaning Decoded value
1001 UI text command FString
1002 Settings update project struct
2001 protobuf event generated protobuf message

The module does not assign IDs or persist a schema. That responsibility belongs to the application protocol.

Register a factory manually

Get the module registry and register a factory that returns a typed dispatch value:

#include "CefDispatch.h"

TSharedPtr<FCefDispatchRegistry> registry =
    FCefDispatchModule::Get().GetRegistry();

registry->RegisterFactory(
    1001,
    [](uint32 messageType, const TArray<uint8>& payload, FString& error)
        -> TUniquePtr<ICefDispatchValue>
    {
        if (payload.Num() > 4096)
        {
            error = TEXT("Text message is too large");
            return nullptr;
        }

        FUTF8ToTCHAR converter(
            reinterpret_cast<const ANSICHAR*>(payload.GetData()),
            payload.Num());
        FString text(converter.Length(), converter.Get());
        return MakeCefDispatchValue(MoveTemp(text));
    });

Factories can return any value type that is copyable or movable into TCefDispatchValue<T>\), including FString, a project struct, a protobuf-generated message, a raw byte wrapper, or another application-owned C++ type. Return nullptrand setOutError` when validation or decoding fails.

Decode and read a typed value

Call Decode when the caller needs to inspect or pass on the decoded value:

TUniquePtr<ICefDispatchValue> value;
FString error;

const ECefDispatchFactoryResult result =
    registry->Decode(messageType, bytes, value, error);

if (result == ECefDispatchFactoryResult::Ok && value.IsValid())
{
    if (const TCefDispatchValue<FString>* textValue =
            CefDispatchTryGetValue<FString>(*value))
    {
        UE_LOG(LogTemp, Log, TEXT("Text: %s"), *textValue->GetValue());
    }
}
else
{
    UE_LOG(LogTemp, Warning, TEXT("Dispatch failed: %s"), *error);
}

CefDispatchTryGetValue<T> checks the runtime type token before the cast. It returns nullptr for a type mismatch, so callers should not assume that a route's value is a particular type unless the registration contract guarantees it.

Factory results

FCefDispatchRegistry::Decode returns:

Result Meaning
Ok A factory ran and returned a valid typed value.
RouteNotFound No factory is registered for the message type.
FactoryFailed The factory returned an invalid/null value.
InvalidFactory A registered factory is not callable.

Factories should provide a useful error string for malformed or unsupported payloads.

Register a typed handler

The handler registry can decode a payload and invoke a handler in one call:

TSharedPtr<FCefDispatchHandlerRegistry> handlers =
    FCefDispatchModule::Get().GetHandlerRegistry();

handlers->RegisterTypedHandler<FString>(
    1001,
    [](const FString& text)
    {
        UE_LOG(LogTemp, Log, TEXT("Handled text: %s"), *text);
    });

FString error;
const ECefDispatchHandlerResult result =
    handlers->Dispatch(1001, bytes, error);

Typed handlers may use these callable forms:

void(const T&)
bool(const T&)
void(uint32, const T&)
bool(uint32, const T&)
void(const T&, FString&)
bool(const T&, FString&)
void(uint32, const T&, FString&)
bool(uint32, const T&, FString&)

Returning bool lets the handler report failure. When an error string is accepted, write a useful message before returning false. Use Handle instead of Dispatch when the value has already been decoded.

Handler results

FCefDispatchHandlerRegistry::Dispatch and Handle return:

Result Meaning
Ok Decode and handler invocation succeeded.
DecodeRegistryUnavailable The handler registry has no decode registry.
DecodeRouteNotFound No factory exists for the message type.
DecodeFailed Factory decoding failed or returned no value.
HandlerNotFound No handler exists for the message type.
HandlerTypeMismatch The decoded value type differs from the registered handler type.
HandlerFailed The handler returned false or otherwise failed.
InvalidHandler The registered handler is not callable.

Static/deferred registration macros

For route declarations that should register automatically, use the macros from CefDispatchRegistration.h:

#include "CefDispatch.h"

CEF_DISPATCH_REGISTER_FACTORY(
    2001,
    [](uint32, const TArray<uint8>& payload, FString& error)
        -> TUniquePtr<ICefDispatchValue>
    {
        FMyPayload decoded;
        if (!DecodeMyPayload(payload, decoded, error))
        {
            return nullptr;
        }
        return MakeCefDispatchValue(MoveTemp(decoded));
    });

CEF_DISPATCH_REGISTER_TYPED_HANDLER(
    2001,
    FMyPayload,
    [](const FMyPayload& value)
    {
        ConsumeMyPayload(value);
    });

Available forms include:

  • CEF_DISPATCH_REGISTER_FACTORY;
  • CEF_DISPATCH_REGISTER_FACTORY_REPLACE;
  • CEF_DISPATCH_REGISTER_HANDLER;
  • CEF_DISPATCH_REGISTER_HANDLER_REPLACE;
  • CEF_DISPATCH_REGISTER_TYPED_HANDLER;
  • CEF_DISPATCH_REGISTER_TYPED_HANDLER_REPLACE.

These macros create static registrar objects. If CefDispatch has not started yet, registration is deferred and applied during StartupModule. This makes static registration usable from modules that load before CefDispatch. Prefer the non-replace macros by default; duplicate message types are rejected unless replacement is explicitly allowed.

Protobuf integration

CefDispatch does not depend on protobuf. A protobuf-enabled module can register a factory that parses its payload and wraps the generated message:

CEF_DISPATCH_REGISTER_FACTORY(
    2001,
    [](uint32, const TArray<uint8>& payload, FString& error)
        -> TUniquePtr<ICefDispatchValue>
    {
        MyProto::Message message;
        if (!message.ParseFromArray(payload.GetData(), payload.Num()))
        {
            error = TEXT("Invalid MyProto::Message payload");
            return nullptr;
        }

        return MakeCefDispatchValue(MoveTemp(message));
    });

The transport can remain binary and generic while the factory owns schema parsing. Pair this with CefProtobuf when using the project's protobuf runtime.

Registration and lifecycle rules

  • FCefDispatchModule::Get() loads the module and returns its singleton module instance.
  • FCefDispatchModule::IsAvailable() checks whether the module is already loaded.
  • GetRegistry() returns the factory registry.
  • GetHandlerRegistry() returns the handler registry sharing the same decode registry.
  • Registering a duplicate factory or handler fails unless bAllowReplace is true.
  • UnregisterFactory and UnregisterHandler remove a route and report whether anything was removed.
  • Registry counts are available through GetFactoryCount and GetHandlerCount.
  • The registries are cleared when the module shuts down.

If a plugin can load before CefDispatch, use the registration macros or RegisterDeferredFactory/RegisterDeferredHandler rather than assuming the registry pointer is already initialized.

Threading guidance

Registry lookups and modifications are protected internally, so concurrent registration and decode operations are supported at the registry level. Your factories and handlers are still responsible for their own shared state.

Keep decoding and handlers deterministic and lightweight. If a handler needs to modify game-thread-only Unreal state, enqueue that work to the game thread. Do not assume that a dispatch call made from a WebSocket worker is running on the game thread.

Common mistakes

  • Reusing a message ID for a different payload without a protocol version change.
  • Registering a factory but forgetting to register a handler before calling Dispatch.
  • Returning nullptr from a factory without setting OutError.
  • Registering a handler for FString while the factory returns a project struct.
  • Using replace registration unintentionally and hiding a duplicate route.
  • Passing unvalidated network bytes directly into protobuf or custom decoders.
  • Calling gameplay APIs from a worker-thread handler.