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

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

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函数。我们这里只截取当前关心的内容,其他部分先略去:

C++ 复制代码
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文件中加载读取当前的配置。

C++ 复制代码
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中。

C++ 复制代码
TMap<FName, FFunctionCollection> GDefaultParamCollection;

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

#include "DefaultParamCollection.inl"
    }
}

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

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

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

C++ 复制代码
ENGINE_API bool RegisterMovementComponent(class UMovementComponent* MovementComp, float AvoidanceWeight = 0.5f);

注册回调

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

C++ 复制代码
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的逻辑,这里我们先只关心初始化的部分,一些细节也先略去:

C++ 复制代码
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层覆盖或新增的方法设置进来,这块内容留到后面再详细展开。

C++ 复制代码
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负责具体的绑定工作。

C++ 复制代码
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虚拟机。

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

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

启动Lua虚拟机

C++ 复制代码
#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

C++ 复制代码
    UELib::Open(L);

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

C++ 复制代码
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交互的数据结构

C++ 复制代码
    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++层的对象进行映射,方便调用和管理。具体细节我们等遇到了再说。

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

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中,可以找到它静态导出的代码:

C++ 复制代码
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类型的静态变量,以及它的构造函数,包含了静态导出的逻辑:

C++ 复制代码
#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启动时,就会自动调到它的构造函数,完成静态导出。

C++ 复制代码
#define IMPLEMENT_EXPORTED_CLASS(Name) \
    FExported##Name##Helper FExported##Name##Helper::StaticInstance;

Lua层初始化

C++ 复制代码
    UnLuaLib::Open(L);

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

C++ 复制代码
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相关的逻辑。

C++ 复制代码
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](https://github.com/Tencent/UnLua) \[2\] [UE4和UnLua交互核心环境分析](https://zhuanlan.zhihu.com/p/397148815)

相关推荐
慢慢沉19 小时前
Lua(数据库访问)
开发语言·数据库·lua
慢慢沉19 小时前
Lua协同程序(coroutine)
lua
慢慢沉2 天前
Lua元表(Metatable)
lua
慢慢沉2 天前
Lua(字符串)
开发语言·lua
慢慢沉2 天前
Lua(数组)
开发语言·lua
慢慢沉2 天前
Lua(迭代器)
开发语言·lua
慢慢沉2 天前
Lua基本语法
开发语言·lua
Feng.Lee3 天前
接口测试Postman工具高级使用技巧
功能测试·测试工具·lua·postman·可用性测试
三翼鸟数字化技术团队3 天前
鸿蒙平台运行Lua脚本
lua·harmonyos