UnLua源码分析(一)初始化流程

发布于:2025-05-19 ⋅ 阅读:(24) ⋅ 点赞:(0)

UnLua是适用于UE的一个高度优化的Lua脚本解决方案。我们今天先来分析一下它的初始化流程。本文基于UE 5.5的环境,分析的UnLua源码版本为是最新的Devlop分支

接入

首先是去下载Develop分支的源码,这个最新分支修复了UE 5.4版本的编译问题。不过很不幸,它不能在5.5版本下编译通过,主要原因也是UE 5.5版本的某些API发生了变化。接入时可以参考GitHub上的相关issue。编译通过之后,就可以参考UnLua官方给的新手教程,进行Lua开发了。打开官方的TPS工程,在Tutorial目录下也有若干展示UnLua特性的例子。

插件启动

UnLua是以插件的形式加载到UE的,那么我们很容易找到它的启动入口,位于UnLuaModule.cpp中的FUnLuaModule::StartupModule函数。我们这里只截取当前关心的内容,其他部分先略去:

virtual void StartupModule() override
{
    RegisterSettings();

    FCoreUObjectDelegates::PostLoadMapWithWorld.AddRaw(this, &FUnLuaModule::PostLoadMapWithWorld);

    CreateDefaultParamCollection();

#if AUTO_UNLUA_STARTUP
#if WITH_EDITOR
    if (!IsRunningGame())
    {
        FEditorDelegates::PreBeginPIE.AddRaw(this, &FUnLuaModule::OnPreBeginPIE);
        FEditorDelegates::PostPIEStarted.AddRaw(this, &FUnLuaModule::OnPostPIEStarted);
        FEditorDelegates::EndPIE.AddRaw(this, &FUnLuaModule::OnEndPIE);
        FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(this, &FUnLuaModule::OnEndPlayMap);
    }

    if (IsRunningGame() || IsRunningDedicatedServer())
#endif
        SetActive(true);
#endif
}

可以看到,负责启动的入口函数还是比较简洁的,第一步是注册一些设置,第二步是创建默认的参数集,第三步会根据当前是否为编辑器环境,如果是则注册一些回调函数,来控制编辑器环境下UnLua的生命周期,如果是打包版则直接启动UnLua。我们先来看看第一个步骤,注册设置。

注册设置

RegisterSettings负责向UE编辑器注册UnLua的配置项,并且注册了配置修改的回调,然后便从ini文件中加载读取当前的配置。

void RegisterSettings()
{
#if WITH_EDITOR
    ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
    if (!SettingsModule)
        return;

    const auto Section = SettingsModule->RegisterSettings("Project", "Plugins", "UnLua",
                                                            LOCTEXT("UnLuaEditorSettingsName", "UnLua"),
                                                            LOCTEXT("UnLuaEditorSettingsDescription", "UnLua Runtime Settings"),
                                                            GetMutableDefault<UUnLuaSettings>());
    Section->OnModified().BindRaw(this, &FUnLuaModule::OnSettingsModified);
#endif

#if ENGINE_MAJOR_VERSION >=5 && !WITH_EDITOR
    // UE5下打包后没有从{PROJECT}/Config/DefaultUnLua.ini加载,这里强制刷新一下
    FString UnLuaIni = TEXT("UnLua");
    GConfig->LoadGlobalIniFile(UnLuaIni, *UnLuaIni, nullptr, true);
    UUnLuaSettings::StaticClass()->GetDefaultObject()->ReloadConfig();
#endif

    auto& Settings = *GetDefault<UUnLuaSettings>();
    bPrintLuaStackOnSystemError = Settings.bPrintLuaStackOnSystemError;
}

在Project Settings/Plugins目录下,可以看到UnLua的配置项,我们暂时不去关心这些配置的具体用途。
在这里插入图片描述

默认参数集

CreateDefaultParamCollection会从一个UBT自动生成的inl文件中,读取UE中包含默认参数的函数,加入到一个名为GDefaultParamCollection的全局Map中。

TMap<FName, FFunctionCollection> GDefaultParamCollection;

void CreateDefaultParamCollection()
{
    static bool CollectionCreated = false;
    if (!CollectionCreated)
    {
        CollectionCreated = true;

#include "DefaultParamCollection.inl"
    }
}

打开DefaultParamCollection.inl文件可以看到大量的函数名称和参数名称,例如:

FC = &GDefaultParamCollection.Add(TEXT("UAvoidanceManager"));
PC = &FC->Functions.Add(TEXT("RegisterMovementComponent"));
PC->Parameters.Add(TEXT("AvoidanceWeight"), new FFloatParamValue(0.500000f));

对照引擎代码,的确可以在UAvoidanceManager中找到函数的定义:

ENGINE_API bool RegisterMovementComponent(class UMovementComponent* MovementComp, float AvoidanceWeight = 0.5f);

注册回调

编辑器环境下,会去监听当前是否处于PIE模式。可以看到UnLua的初始化逻辑分为两块,一部分在进入PIE模式之前执行,一部分则在进入PIE模式之后再执行。

void OnPreBeginPIE(bool bIsSimulating)
{
    SetActive(true);
}

void OnPostPIEStarted(bool bIsSimulating)
{
    UEditorEngine* EditorEngine = Cast<UEditorEngine>(GEngine);
    if (EditorEngine)
        PostLoadMapWithWorld(EditorEngine->PlayWorld);
}

打包版同样也会先调用SetActive,然后在加载地图时调用PostLoadMapWithWorld。显然这两个函数就是UnLua初始化的核心函数了。

SetActive

SetActive接受一个bool类型的参数,说明它同时负责启动和销毁UnLua的逻辑,这里我们先只关心初始化的部分,一些细节也先略去:

virtual void SetActive(const bool bActive) override
{
    if (bIsActive == bActive)
        return;

    if (bActive)
    {
        GUObjectArray.AddUObjectCreateListener(this);
        GUObjectArray.AddUObjectDeleteListener(this);

        const auto& Settings = *GetMutableDefault<UUnLuaSettings>();
        const auto EnvLocatorClass = *Settings.EnvLocatorClass == nullptr ? ULuaEnvLocator::StaticClass() : *Settings.EnvLocatorClass;
        EnvLocator = NewObject<ULuaEnvLocator>(GetTransientPackage(), EnvLocatorClass);
        EnvLocator->AddToRoot();

        for (const auto Class : TObjectRange<UClass>())
        {
            for (const auto& ClassPath : Settings.PreBindClasses)
            {
                if (!ClassPath.IsValid())
                    continue;

                const auto TargetClass = ClassPath.ResolveClass();
                if (!TargetClass)
                    continue;

                if (Class->IsChildOf(TargetClass))
                {
                    const auto Env = EnvLocator->Locate(Class);
                    Env->TryBind(Class);
                    break;
                }
            }
        }
    }
    bIsActive = bActive;
}

主要也是三件事情,首先是对UObject的创建和销毁进行了监听,这个很自然,因为UnLua需要为UObject绑定相关的Lua信息,实现Lua层与C++层之间的交互;第二是创建了一个ULuaEnvLocator类型的对象,通过类的定义可知它主要负责从上层管理Lua虚拟机环境,这个类型还支持通过配置进行修改;最后是如果配置项中存在需要预先绑定的类,则在此时尝试进行绑定。这里绑定的概念是双向的,意味着会把C++层的方法暴露给Lua层,同时也把Lua层覆盖或新增的方法设置进来,这块内容留到后面再详细展开。

UCLASS()
class UNLUA_API ULuaEnvLocator : public UObject
{
    GENERATED_BODY()
public:
    virtual UnLua::FLuaEnv* Locate(const UObject* Object);

    virtual void HotReload();

    virtual void Reset();

    TSharedPtr<UnLua::FLuaEnv, ESPMode::ThreadSafe> Env;
};

默认配置下UnLua有3个需要提前绑定的类:
在这里插入图片描述

PostLoadMapWithWorld

相较之下,PostLoadMapWithWorld就比较简单了,它主要就是创建出UUnLuaManager类型的对象了,这个manager负责具体的绑定工作。

void PostLoadMapWithWorld(UWorld* World) const
{
    if (!World || !bIsActive)
        return;

    const auto Env = EnvLocator->Locate(World);
    if (!Env)
        return;

    const auto Manager = Env->GetManager();
    if (!Manager)
        return;

    Manager->OnMapLoaded(World);
}

通过上述分析,我们进一步发现初始化的核心逻辑就在ULuaEnvLocatorUUnLuaManager中。

ULuaEnvLocator

ULuaEnvLocator提供了一个Locate函数,负责返回一个FLuaEnv类型的对象。这个对象是UnLua的核心对象,负责管理Lua虚拟机。

UnLua::FLuaEnv* ULuaEnvLocator::Locate(const UObject* Object)
{
    if (!Env)
    {
        Env = MakeShared<UnLua::FLuaEnv, ESPMode::ThreadSafe>();
        Env->Start();
    }
    return Env.Get();
}

接下来对FLuaEnv的构造函数进行逐步分析。

启动Lua虚拟机

#if PLATFORM_WINDOWS
    // 防止类似AppleProResMedia插件忘了恢复Dll查找目录
    // https://github.com/Tencent/UnLua/issues/534
    const auto Dir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / TEXT("Binaries/Win64"));
    FPlatformProcess::PushDllDirectory(*Dir);
    L = lua_newstate(GetLuaAllocator(), nullptr);
    FPlatformProcess::PopDllDirectory(*Dir);
#else
    L = lua_newstate(GetLuaAllocator(), nullptr);
#endif

    AllEnvs.Add(L, this);

    luaL_openlibs(L);

    AddSearcher(LoadFromCustomLoader, 2);
    AddSearcher(LoadFromFileSystem, 3);
    AddSearcher(LoadFromBuiltinLibs, 4);

此时Lua虚拟机已创建完成,并且Lua的标准库也都加载进来了。此外,UnLua还调整了Lua文件的搜索路径,使得Lua虚拟机可以读取到UE工程目录下的源文件。当然,我们也可以自定义自己的Loader。

初始化UE相关的Lua Lib

    UELib::Open(L);

Open函数会向Lua的全局环境中注册UE的表,表中包含几个基本的UE库函数,同时还设置了__index元方法,这样Lua层在访问UE.XXX时就会触发这里的逻辑。

static constexpr luaL_Reg UE_Functions[] = {
    {"LoadObject", UObject_Load},
    {"LoadClass", UClass_Load},
    {"NewObject", Global_NewObject},
    {NULL, NULL}
};

int UnLua::UELib::Open(lua_State* L)
{
    lua_newtable(L);
    lua_pushstring(L, "__index");
    lua_pushcfunction(L, UE_Index);
    lua_rawset(L, -3);

    lua_pushvalue(L, -1);
    lua_setmetatable(L, -2);

    lua_pushvalue(L, -1);
    lua_pushstring(L, REGISTRY_KEY);
    lua_rawset(L, LUA_REGISTRYINDEX);

    luaL_setfuncs(L, UE_Functions, 0);
    lua_setglobal(L, NAMESPACE_NAME);

    // global access for legacy support
    lua_getglobal(L, LUA_GNAME);
    luaL_setfuncs(L, UE_Functions, 0);
    lua_pop(L, 1);

#if WITH_UE4_NAMESPACE == 1
    // 兼容UE4访问
    lua_getglobal(L, NAMESPACE_NAME);
    lua_setglobal(L, "UE4");
#elif WITH_UE4_NAMESPACE == 0
    // 兼容无UE4全局访问
    lua_getglobal(L, LUA_GNAME);
    lua_newtable(L);
    lua_pushstring(L, "__index");
    lua_getglobal(L, NAMESPACE_NAME);
    lua_rawset(L, -3);
    lua_setmetatable(L, -2);
#endif

    return 1;
}

创建与Lua交互的数据结构

    ObjectRegistry = new FObjectRegistry(this);
    ClassRegistry = new FClassRegistry(this);
    ClassRegistry->Initialize();

    FunctionRegistry = new FFunctionRegistry(this);
    DelegateRegistry = new FDelegateRegistry(this);
    ContainerRegistry = new FContainerRegistry(this);
    PropertyRegistry = new FPropertyRegistry(this);
    EnumRegistry = new FEnumRegistry(this);
    EnumRegistry->Initialize();

    lua_pushstring(L, "StructMap"); // create weak table 'StructMap'
    LowLevel::CreateWeakValueTable(L);
    lua_rawset(L, LUA_REGISTRYINDEX);

    lua_pushstring(L, "ArrayMap"); // create weak table 'ArrayMap'
    LowLevel::CreateWeakValueTable(L);
    lua_rawset(L, LUA_REGISTRYINDEX);

通过名字就能得知,这里创建了保存与Lua交互信息的Object、Class、Container、Struct、Array等注册表。它们的主要作用是将Lua层的对象与C++层的对象进行映射,方便调用和管理。具体细节我们等遇到了再说。

注册静态导出的类,函数,枚举

    // register statically exported classes
    auto ExportedNonReflectedClasses = GetExportedNonReflectedClasses();
    for (const auto& Pair : ExportedNonReflectedClasses)
        Pair.Value->Register(L);

    // register statically exported global functions
    auto ExportedFunctions = GetExportedFunctions();
    for (const auto& Function : ExportedFunctions)
        Function->Register(L);

    // register statically exported enums
    auto ExportedEnums = GetExportedEnums();
    for (const auto& Enum : ExportedEnums)
        Enum->Register(L);

所谓的静态导出,就是在UnLua加载时,利用静态变量初始化的方式,预先导出给Lua的类,函数和枚举。比如TArray,我们在LuaLib_Array.cpp中,可以找到它静态导出的代码:

static const luaL_Reg TArrayLib[] =
{
    {"Length", TArray_Length},
    {"Num", TArray_Length},
    {"Add", TArray_Add},
    {"AddUnique", TArray_AddUnique},
    {"Find", TArray_Find},
    {"Insert", TArray_Insert},
    {"Remove", TArray_Remove},
    {"RemoveItem", TArray_RemoveItem},
    {"Clear", TArray_Clear},
    {"Reserve", TArray_Reserve},
    {"Resize", TArray_Resize},
    {"GetData", TArray_GetData},
    {"Get", TArray_Get},
    {"GetRef", TArray_GetRef},
    {"Set", TArray_Set},
    {"Swap", TArray_Swap},
    {"Shuffle", TArray_Shuffle},
    {"LastIndex", TArray_LastIndex},
    {"IsValidIndex", TArray_IsValidIndex},
    {"Contains", TArray_Contains},
    {"Append", TArray_Append},
    {"ToTable", TArray_ToTable},
    {"__gc", TArray_Delete},
    {"__call", TArray_New},
    {"__pairs", TArray_Pairs},
    {"__index", TArray_Index},
    {"__newindex", TArray_NewIndex},
    {nullptr, nullptr}
};

EXPORT_UNTYPED_CLASS(TArray, false, TArrayLib)

IMPLEMENT_EXPORTED_CLASS(TArray)

EXPORT_UNTYPED_CLASS是一个宏,它定义了一个struct,和该struct类型的静态变量,以及它的构造函数,包含了静态导出的逻辑:

#define EXPORT_UNTYPED_CLASS(Name, bIsReflected, Lib) \
    struct FExported##Name##Helper \
    { \
        static FExported##Name##Helper StaticInstance; \
        FExported##Name##Helper() \
            : ExportedClass(nullptr) \
        { \
            UnLua::IExportedClass *Class = UnLua::FindExportedClass(#Name); \
            if (!Class) \
            { \
                ExportedClass = new UnLua::TExportedClassBase<bIsReflected>(#Name); \
                UnLua::ExportClass(ExportedClass); \
                Class = ExportedClass; \
            } \
            Class->AddLib(Lib); \
        } \
        ~FExported##Name##Helper() \
        { \
            delete ExportedClass; \
        } \
        UnLua::IExportedClass *ExportedClass; \
    };

IMPLEMENT_EXPORTED_CLASS宏就是对该静态变量进行初始化,这样在UnLua启动时,就会自动调到它的构造函数,完成静态导出。

#define IMPLEMENT_EXPORTED_CLASS(Name) \
    FExported##Name##Helper FExported##Name##Helper::StaticInstance;

Lua层初始化

    UnLuaLib::Open(L);

在UnLua完成C++层面的构造之后,UnLua会再执行一段Lua逻辑,完成最后的初始化工作。

int Open(lua_State* L)
{
    lua_register(L, "print", LogInfo);
    luaL_requiref(L, "UnLua", LuaOpen, 1);
    luaL_dostring(L, R"(
        setmetatable(UnLua, {
            __index = function(t, k)
                local ok, result = pcall(require, "UnLua." .. tostring(k))
                if ok then
                    rawset(t, k, result)
                    return result
                else
                    t.LogWarn(string.format("failed to load module UnLua.%s\n%s", k, result))
                end
            end
        })
    )");

#if UNLUA_ENABLE_FTEXT
    luaL_dostring(L, "UnLua.FTextEnabled = true");
#else
    luaL_dostring(L, "UnLua.FTextEnabled = false");
#endif

#if UNLUA_WITH_HOT_RELOAD
    luaL_dostring(L, R"(
        pcall(function() _G.require = require('UnLua.HotReload').require end)
    )");
#endif

    LegacySupport(L);
    lua_pop(L, 1);
    return 1;
}

可以看到,UnLua在全局环境中定义了UnLua表,访问UnLua.XXX时,会直接去加载UnLua.XXX.lua文件,另外UnLua重写了require函数,改用HotReload模块,用于热重载的支持。

UUnLuaManager

UUnLuaManager构造函数则主要初始化UE Input相关的逻辑。

UUnLuaManager::UUnLuaManager()
    : InputActionFunc(nullptr), InputAxisFunc(nullptr), InputTouchFunc(nullptr), InputVectorAxisFunc(nullptr), InputGestureFunc(nullptr), AnimNotifyFunc(nullptr)
{
    if (HasAnyFlags(RF_ClassDefaultObject))
    {
        return;
    }

    GetDefaultInputs();             // get all Axis/Action inputs
    EKeys::GetAllKeys(AllKeys);     // get all key inputs

    // get template input UFunctions for InputAction/InputAxis/InputTouch/InputVectorAxis/InputGesture/AnimNotify
    UClass *Class = GetClass();
    InputActionFunc = Class->FindFunctionByName(FName("InputAction"));
    InputAxisFunc = Class->FindFunctionByName(FName("InputAxis"));
    InputTouchFunc = Class->FindFunctionByName(FName("InputTouch"));
    InputVectorAxisFunc = Class->FindFunctionByName(FName("InputVectorAxis"));
    InputGestureFunc = Class->FindFunctionByName(FName("InputGesture"));
    AnimNotifyFunc = Class->FindFunctionByName(FName("TriggerAnimNotify"));
}

总结

自此我们梳理了UnLua的整个初始化流程,UnLua的初始化主要分为两个部分,一部分是C++层的初始化,另一部分是Lua层的初始化。C++层主要完成了Lua虚拟机的创建和UE相关的注册表的创建,Lua层则完成了最后的注册和热重载支持。UnLua的设计思路还是比较清晰的,后续我们会继续分析UnLua与UE交互的一些细节。

Reference

[1] UnLua GitHub

[2] UE4和UnLua交互核心环境分析


网站公告

今日签到

点亮在社区的每一天
去签到