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
函数。我们这里只截取当前关心的内容,其他部分先略去:
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);
}
通过上述分析,我们进一步发现初始化的核心逻辑就在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/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