UnLua源码分析(一)初始化流程
UnLua源码分析(一)初始化流程
- 接入
- 插件启动
- 注册设置
- 默认参数集
- 注册回调
- SetActive
- PostLoadMapWithWorld
- ULuaEnvLocator
- 启动Lua虚拟机
- 初始化UE相关的Lua Lib
- 创建与Lua交互的数据结构
- 注册静态导出的类,函数,枚举
- Lua层初始化
- UUnLuaManager
- 总结
- Reference
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_EDITORif (!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())
#endifSetActive(true);
#endif
}
可以看到,负责启动的入口函数还是比较简洁的,第一步是注册一些设置,第二步是创建默认的参数集,第三步会根据当前是否为编辑器环境,如果是则注册一些回调函数,来控制编辑器环境下UnLua的生命周期,如果是打包版则直接启动UnLua。我们先来看看第一个步骤,注册设置。
注册设置
RegisterSettings
负责向UE编辑器注册UnLua的配置项,并且注册了配置修改的回调,然后便从ini文件中加载读取当前的配置。
void RegisterSettings()
{
#if WITH_EDITORISettingsModule* 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();
#endifauto& 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);
}
通过上述分析,我们进一步发现初始化的核心逻辑就在ULuaEnvLocator
和UUnLuaManager
中。
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/534const auto Dir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / TEXT("Binaries/Win64"));FPlatformProcess::PushDllDirectory(*Dir);L = lua_newstate(GetLuaAllocator(), nullptr);FPlatformProcess::PopDllDirectory(*Dir);
#elseL = lua_newstate(GetLuaAllocator(), nullptr);
#endifAllEnvs.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 supportlua_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);
#endifreturn 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 classesauto ExportedNonReflectedClasses = GetExportedNonReflectedClasses();for (const auto& Pair : ExportedNonReflectedClasses)Pair.Value->Register(L);// register statically exported global functionsauto ExportedFunctions = GetExportedFunctions();for (const auto& Function : ExportedFunctions)Function->Register(L);// register statically exported enumsauto 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 thenrawset(t, k, result)return resultelset.LogWarn(string.format("failed to load module UnLua.%s\n%s", k, result))endend}))");#if UNLUA_ENABLE_FTEXTluaL_dostring(L, "UnLua.FTextEnabled = true");
#elseluaL_dostring(L, "UnLua.FTextEnabled = false");
#endif#if UNLUA_WITH_HOT_RELOADluaL_dostring(L, R"(pcall(function() _G.require = require('UnLua.HotReload').require end))");
#endifLegacySupport(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 inputsEKeys::GetAllKeys(AllKeys); // get all key inputs// get template input UFunctions for InputAction/InputAxis/InputTouch/InputVectorAxis/InputGesture/AnimNotifyUClass *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交互核心环境分析