前言

Unlua绑定UE底层原理,正在更新

图表分析

图表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
graph TB
subgraph Lua层["🎯 Lua 层"]
LS[Lua Scripts]
LM[Lua Modules]
LT[Lua Tables]
end

subgraph 绑定层["🔗 绑定层"]
SE["静态导出<br/>📌 编译期"]
DB["动态绑定<br/>⚡ 运行时"]
RB["反射绑定<br/>🔄 按需加载"]
end

subgraph 注册层["📋 注册层"]
RS[Registry System]
CR[Class Registry]
OR[Object Registry]
FR[Function Registry]
end

subgraph 核心层["⚙️ 核心层"]
LE["LuaEnv<br/>Lua VM 管理器"]
GC[GC 管理]
REF[对象引用]
MEM[内存分配]
end

LS --> SE
LM --> DB
LT --> RB

SE --> RS
DB --> RS
RB --> RS

RS --> CR
RS --> OR
RS --> FR

CR --> LE
OR --> LE
FR --> LE

LE --> GC
LE --> REF
LE --> MEM

classDef luaLayer fill:#e1f5ff,stroke:#01579b,stroke-width:2px
classDef bindLayer fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef regLayer fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef coreLayer fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px

class LS,LM,LT luaLayer
class SE,DB,RB bindLayer
class RS,CR,OR,FR regLayer
class LE,GC,REF,MEM coreLayer

Lua2CPP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
actor:SetActorLocation(FVector(100, 200, 300))

┌──────────────────────────────────────────────────────┐
│ 1. Lua 函数查找 │
│ actor:SetActorLocation │
│ └─► actor 的 metatable.__index │
│ └─► Class_Index │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 2. 获取 FFunctionDesc │
│ GetField(L) │
│ └─► ClassDesc->RegisterField("SetActorLocation")│
│ └─► 创建 FFunctionDesc (首次) 或缓存 │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 3. 推送 Closure │
│ PushField(L, Function) │
│ └─► lua_pushcclosure(L, Class_CallUFunction, 1) │
│ └─► 缓存到 metatable │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 4. 执行 Closure │
│ Class_CallUFunction(L) │
│ └─► FFunctionDesc::CallUE(L, NumParams) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 5. 参数转换 │
│ PreCall(L, NumParams, Params) │
│ ├─► FVector::WriteValue(L, Params) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 6. 调用 UE 函数 │
│ Object->ProcessEvent(Function, Params) │
│ └─► AActor::SetActorLocation(FVector) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 7. 返回值处理 │
│ PostCall(L, Params) │
│ └─► 无返回值,返回 0 │
└──────────────────────────────────────────────────────┘

Lua 调用 → 2. UnLua 拦截 → 3. 参数打包 → 4. 反射调用 → 5. 结果返回

CPP2Lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
蓝图节点: Event BeginPlay
┌──────────────────────────────────────────────────────┐
│ 1. 蓝图虚拟机执行 │
│ UK2Node_Event::Execute() │
│ └─► UObject::ProcessEvent(BeginPlay, nullptr) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 2. 检查 Lua 覆写 │
│ FLuaOverrider::ProcessEvent() │
│ ├─► 查找 ObjectRefs[this] │
│ │ └─► LuaTableRef (Lua 实例表引用) │
│ │ │
│ ├─► lua_rawgeti(L, LUA_REGISTRYINDEX, Ref) │
│ │ └─► 获取 INSTANCE │
│ │ │
│ └─► lua_getfield(L, -1, "ReceiveBeginPlay") │
│ └─► 查找 Lua 函数 │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 3. 准备 Lua 调用 │
│ FFunctionDesc::CallLua(L, FunctionRef, SelfRef, Stack)│
│ ├─► lua_rawgeti(L, LUA_REGISTRYINDEX, FunctionRef)│
│ └─► lua_rawgeti(L, LUA_REGISTRYINDEX, SelfRef) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 4. 参数转换 │
│ for (Property: Function->Properties) │
│ Property->ReadValue(L, Params, false) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 5. 执行 Lua 函数 │
│ lua_pcall(L, NumParams, NumResults, ErrorHandler) │
│ └─► 执行 MyActor.lua:ReceiveBeginPlay(self) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 6. 返回值处理 │
│ if (HasReturnValue) │
│ ReturnProperty->WriteValue(L, RetAddress, -1) │
└────────────┬─────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│ 7. Out 参数回写 │
│ for (OutProperty: OutProperties) │
│ OutProperty->WriteValue(L, Params) │
└──────────────────────────────────────────────────────┘

绑定机制

基于这篇文章,我来详细介绍UnLua的绑定机制实现原理。UnLua的绑定机制主要分为静态导出和动态绑定两种方式,它们共同构成了完整的C++/Lua交互体系。

一、整体架构设计

UnLua采用四层架构实现绑定机制:

⚙️ 核心层 → 📋 注册层 → 🔗 绑定层 → 🎯 Lua层

核心组件职责:
• FClassRegistry:管理UStruct到Lua Metatable的映射

• FObjectRegistry:管理UObject实例到Lua Userdata的映射

• FLuaEnv:Lua虚拟机管理和环境隔离

• FFunctionDesc:UFunction描述符和调用分发

二、静态导出机制(编译期绑定)

核心原理

静态导出利用C++全局构造函数在程序启动前完成类型注册,零运行时开销。

实现机制

  1. 宏定义展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 用户代码
BEGIN_EXPORT_CLASS(FMyMathLib)
ADD_STATIC_FUNCTION(Add)
END_EXPORT_CLASS()

// 展开后
struct FExportedFMyMathLibHelper {
static struct FExportedFMyMathLib : public UnLua::FExportedClass<FMyMathLib> {
FExportedFMyMathLib() : FExportedClass("FMyMathLib") {
AddFunction("Add", &FMyMathLib::Add); // 编译期注册
}
} Exported;
};
// 全局静态对象触发构造函数
FExportedFMyMathLibHelper::FExportedFMyMathLib FExportedFMyMathLibHelper::Exported;
  1. 全局构造函数触发流程
1
2
3
4
5
6
7
8
9
10
11
12
13
操作系统加载程序

加载所有动态库(DLL/SO)

初始化全局变量区

执行全局对象构造函数(按编译顺序)

FExportedFMyMathLib::FExportedFMyMathLib() → ExportClass(this)

注册到全局容器GExportedClasses

程序启动后FLuaEnv::Initialize()遍历容器注册到Lua
  1. Lua注册完整流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void FExportedClass::Register(lua_State* L) {
// 1. 创建命名元表
luaL_newmetatable(L, ClassName.Get());

// 2. 设置继承关系
if (!SuperClassName.IsEmpty()) {
lua_pushstring(L, "Super");
luaL_getmetatable(L, TCHAR_TO_UTF8(*SuperClassName));
lua_rawset(L, -3); // metatable.Super = SuperMetatable
}

// 3. 设置__index元方法
lua_pushstring(L, "__index");
lua_pushvalue(L, -2);
lua_pushcclosure(L, UnLua::Index, 1);
lua_rawset(L, -3);

// 4. 注册到全局UE表
lua_getglobal(L, "UE");
lua_pushstring(L, ClassName.Get());
lua_pushvalue(L, -3);
lua_rawset(L, -3); // UE["FMyMathLib"] = metatable
}
  1. 函数调用流程

– Lua调用
local result = UE.FMyMathLib.Add(10, 20)

– 执行过程:
– 1. UE[“FMyMathLib”][“Add”] → 触发__index,返回Closure
– 2. Closure(10, 20) → InvokeFunction
– 3. lua_upvalueindex(1) → 获取FExportedFunction*
– 4. FExportedFunction::Invoke → 调用FMyMathLib::Add
– 5. 返回30

三、动态绑定机制(运行时绑定)

工作流程

1️⃣ Actor实例化 → 2️⃣ 加载Lua模块 → 3️⃣ 创建绑定实例 → 4️⃣ 函数覆写准备

绑定触发机制(按优先级)

优先级 绑定方式 触发条件 配置位置

1 IUnLuaInterface 类实现接口 C++代码

2 UnLuaBind组件 添加组件 蓝图/实例

3 全局配置 匹配类名规则 Project Settings

4 手动绑定 调用API Lua代码

对象绑定完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int FObjectRegistry::Bind(UObject* Object) {
// 1. 获取弱引用表UnLua_ObjectMap
lua_getfield(L, LUA_REGISTRYINDEX, "UnLua_ObjectMap");
lua_pushlightuserdata(L, Object); // 使用对象指针作为key

// 2. 创建INSTANCE表
lua_newtable(L); // 栈: [ObjectMap, Object指针, INSTANCE]

// 3. 创建Userdata绑定UObject
PushObjectCore(L, Object); // 栈: [ObjectMap, Object指针, INSTANCE, Userdata]

// 4. INSTANCE.Object = Userdata
lua_pushstring(L, "Object");
lua_pushvalue(L, -2); // 复制Userdata
lua_rawset(L, -4); // INSTANCE["Object"] = Userdata

// 5. 获取Lua Module表
int32 ClassBoundRef = Env->GetManager()->GetBoundRef(Class);
lua_rawgeti(L, LUA_REGISTRYINDEX, ClassBoundRef); // 栈: [..., MODULE]

// 6. 设置元表继承链
lua_getmetatable(L, -2); // 获取Userdata的Metatable
lua_setmetatable(L, -2); // MODULE.metatable = METATABLE_UOBJECT
lua_setmetatable(L, -3); // INSTANCE.metatable = MODULE

// 7. 创建引用并缓存
lua_pushvalue(L, -1); // 复制INSTANCE
const auto Ref = luaL_ref(L, LUA_REGISTRYINDEX); // 创建引用
ObjectRefs.Add(Object, Ref); // C++端保存映射

// 8. 缓存到弱引用表
lua_rawset(L, -3); // ObjectMap[Object指针] = INSTANCE
return Ref;
}

元表继承链结构

1
2
3
4
5
6
7
8
9
10
11
12
INSTANCE {                    # 对象实例表
Object = <Userdata>, # 绑定的UObject
metatable = MODULE { # Lua模块表
ReceiveBeginPlay = function(...) end,
metatable = METATABLE_UOBJECT { # C++反射元表
__index = Class_Index,
__newindex = Class_NewIndex,
__gc = UObject_Delete,
...
}
}
}

四、函数覆写原理

核心机制:ProcessEvent Hook

UnLua通过拦截UObject::ProcessEvent实现函数覆写:
// Hook流程
原始调用:AActor::BeginPlay → UFunction::Invoke → UObject::ProcessEvent
Hook后:AActor::BeginPlay → UFunction::Invoke → FLuaOverrider::ProcessEvent

覆写检测流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool FLuaOverrides::IsOverridden(UFunction* Function) const {
// 1. 检查是否有Lua覆写
if (LuaFunctions.Contains(Function->GetFName())) {
return true;
}

// 2. 检查父类
UClass* SuperClass = Function->GetOwnerClass()->GetSuperClass();
if (SuperClass) {
return IsOverriddenInClass(SuperClass, Function);
}

return false;
}

Lua调用C++函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void FFunctionDesc::CallLua(lua_State* L, lua_Integer FunctionRef, 
lua_Integer SelfRef, FFrame& Stack, RESULT_DECL) {
// 1. 从registry获取Lua函数
lua_rawgeti(L, LUA_REGISTRYINDEX, FunctionRef); // [function]
lua_rawgeti(L, LUA_REGISTRYINDEX, SelfRef); // [function, self]

// 2. 参数转换:C++ → Lua
for (int i = 0; i < Properties.Num(); ++i) {
if (!Properties[i]->IsReturnParameter()) {
Properties[i]->ReadValue_InContainer(L, Params, false);
}
}

// 3. 调用Lua函数
if (lua_pcall(L, NumParams, NumResults, ErrorHandler) != LUA_OK) {
const char* ErrorMsg = lua_tostring(L, -1);
UE_LOG(LogUnLua, Error, TEXT("Lua Error: %s"), UTF8_TO_TCHAR(ErrorMsg));
lua_pop(L, 1);
return;
}

// 4. 返回值处理:Lua → C++
if (HasReturnProperty()) {
RetProp->WriteValue(L, RetAddress, -NumResults);
}
}

五、两种绑定机制对比

特性 静态导出 动态绑定
注册时机 编译期(程序启动前) 运行时(对象创建时)
类型安全 编译期检查 运行时检查
调用开销 ~1.5μs(直接调用) ~2.5μs(反射查询)
支持反射 否(不依赖UE反射) 是(基于UE反射)
热更新 否(需重新编译) 是(可替换Lua文件)
适用场景 数学库、工具函数、第三方库封装 Actor/Component、蓝图类、需要热更新的逻辑

六、关键数据结构

  1. 注册表(Registry)使用
1
2
3
4
5
6
7
8
9
// Lua registry内部结构(概念模型)
LUA_REGISTRYINDEX = {
["_G"] = _G, // Lua全局环境
["_LOADED"] = package.loaded, // 已加载模块
["UnLua_ObjectMap"] = {...}, // UnLua对象缓存
["UObject"] = {...}, // 命名元表
[1] = {...}, // luaL_ref创建的引用
[2] = function() ... end, // 引用ID=2
}
  1. 引用系统管理
1
2
3
4
5
6
7
8
9
// 创建引用
lua_newtable(L); // 栈: [table]
int ref = luaL_ref(L, LUA_REGISTRYINDEX); // 栈: [],table存入registry

// 使用引用
lua_rawgeti(L, LUA_REGISTRYINDEX, ref); // 栈: [table]

// 释放引用
luaL_unref(L, LUA_REGISTRYINDEX, ref); // registry[ref] = nil

七、性能优化要点

  1. 缓存策略:频繁访问的元表和函数引用进行缓存
  2. 批量操作:减少Lua栈操作次数,使用批量参数传递
  3. 引用计数:合理使用luaL_ref/luaL_unref管理对象生命周期
  4. 避免GC压力:使用light userdata存储指针,减少内存分配

总结

UnLua的绑定机制通过静态导出和动态绑定两种方式,实现了C++与Lua的高效交互:

  1. 静态导出适用于性能敏感、类型固定的场景,通过编译期模板元编程实现零开销绑定
  2. 动态绑定适用于需要灵活性、热更新的场景,通过运行时反射和元表继承实现
  3. 双层注册架构(ClassRegistry + ObjectRegistry)分离了类型注册和实例绑定
  4. 函数覆写通过Hook ProcessEvent实现,支持Lua函数覆盖C++/蓝图函数
  5. 完整的GC管理通过双重引用系统确保内存安全

这种设计既保证了高性能,又提供了足够的灵活性,是UnLua能够在大型游戏项目中广泛应用的关键原因。

四个核心技术点

基于您提供的文档内容,这篇文章对UnLua框架的四个核心技术点——静态导出、动态绑定、元表和GC管理——进行了深入剖析。以下是这四部分的核心内容总结:

1. 静态导出

  • 核心思想:在编译期完成绑定,实现零运行时开销。它不依赖UE的运行时反射系统。
  • 实现机制:利用C++的全局对象构造函数main()函数执行前自动触发注册逻辑。通过模板元编程和特定的宏(如BEGIN_EXPORT_CLASS),将C++类型、函数的信息和调用入口注册到Lua环境中。
  • 关键流程
    1. 程序启动时,所有通过宏定义的导出类会执行其全局构造函数,将自身注册到一个全局容器中。
    2. Lua虚拟机初始化时(FLuaEnv::Initialize),会遍历这个容器,为每个导出的类创建对应的Lua命名元表,并将函数包装为Lua闭包注册进去。
  • 特点与场景:性能极高(~1.5μs),类型安全,但不支持热更新。适合导出无状态的工具类、数学库、第三方C++库等。

2. 动态绑定

  • 核心思想:在运行时根据UE的反射信息动态地将Lua模块绑定到UObject实例上,支持热更新。
  • 实现机制:基于UE的反射系统,在对象实例化时触发绑定流程。核心是FObjectRegistry::Bind函数,它会为每个UObject实例创建一个Lua端的INSTANCE表,并与对应的Lua模块、C++元表关联,形成多层查找链。
  • 关键流程
    1. Actor等对象实例化后,通过接口、组件或配置触发绑定。
    2. 加载对应的Lua模块文件,执行后得到一个模块表(MODULE)。
    3. 创建INSTANCE表,内部通过Userdata引用C++对象,并将其元表设置为MODULE
    4. MODULE的元表又指向C++反射生成的UObject元表,从而建立起INSTANCE -> MODULE -> C++反射的继承链。
  • 特点与场景:灵活,支持热更,但有一定调用开销(~2.5μs)。适合绑定Actor、Component等需要热更新逻辑的蓝图或C++类。

3. 元表

  • 核心作用Lua中实现面向对象和操作符重载的基石,在UnLua中扮演了连接Lua与C++的“桥接协议”角色。
  • 关键元方法
    • __index: 当访问INSTANCE的一个字段(如函数名)时触发。UnLua的Class_Index函数会沿着INSTANCE -> MODULE -> C++元表这条链查找,找到后返回一个可调用闭包,并将其缓存以提升后续访问性能。
    • __newindex: 当向INSTANCE写入属性时触发,用于将Lua端的赋值操作映射到C++对象属性的Set函数。
    • __gc: 当Lua端的Userdata被垃圾回收时触发,用于通知UnLua的C++端解除对该UObject的自动引用,允许UE GC回收。
  • 实现要点:UnLua通过luaL_newmetatable为每个C++类型创建了全局唯一的命名元表,存储在Lua注册表中,确保了类型系统的一致性。

4. GC管理

  • 核心挑战:需要协调Lua GCUE GC两套独立的垃圾回收系统,防止对象被任意一方提前回收或产生悬空指针。
  • 双重GC协同机制
    1. 防UE回收:当Lua通过PushObjectCore引用一个UObject时,会将其加入AutoObjectReference集合。这个集合会阻止UE GC回收这些被Lua引用的对象。
    2. 防悬空指针
      • 二级指针:Lua Userdata内部存储的是指向UObject指针的指针(void**),而非指针本身。
      • 释放标记:当UObject被UE销毁时,UnLua会收到通知,并将Userdata内的二级指针指向一个特殊的0xDEAD地址作为“已释放”标记。
    3. 访问安全:每次通过Userdata访问C++对象前,都会检查指针是否为0xDEAD。如果是,则抛出Lua错误,从而安全地阻止了访问已释放内存。
    4. Lua GC回调:当Lua端的Userdata被回收时,其__gc元方法会调用UObject_Delete,从AutoObjectReference集合中移除对应UObject,允许UE GC最终回收它。

总结

这四个部分紧密协作,构成了UnLua高性能绑定的基础:静态导出与动态绑定提供了两种互补的接入方式;元表是实现属性、函数透明访问的协议层;而GC管理则是确保整个系统内存安全、稳定运行的守护者。它们共同使得在Unreal Engine中使用Lua脚本开发既高效又安全。

代码样例

EveLuaManager.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Copyright Night Gamer. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"

// 添加 Lua 头文件包含
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

/**
* EveLuaManager 类,用于管理 Lua 状态和执行 Lua 脚本。
*/
class EveLuaManager
{
public:
// 构造函数
EveLuaManager();
// 析构函数
~EveLuaManager();

/**
* 初始化 Lua 状态。
* @return 初始化成功返回 true,失败返回 false
*/
bool Initialize();
/**
* 执行 Lua 脚本。
* @param Script 要执行的 Lua 脚本字符串
* @return 执行成功返回 true,失败返回 false
*/
bool ExecuteLuaScript(const char* Script);
/**
* 关闭 Lua 状态。
*/
void Shutdown();

/**
* 获取 Lua 状态。
* @return Lua 状态指针
*/
lua_State* GetLuaState() const
{
return LuaState;
}

/**
* Lua 的 Print 函数,用于在 UE 日志中输出信息。
* @param L Lua 状态指针
* @return 0
*/
static int Lua_Print(lua_State* L);
/**
* Lua 的 CallMemberFunction 函数,用于调用 C++ 对象的成员函数。
* @param L Lua 状态指针
* @return 压入 Lua 栈的返回值数量
*/
static int Lua_CallMemberFunction(lua_State* L);
/**
* Lua 的 SpawnActor 函数,用于在 UE 世界中生成 Actor。
* @param L Lua 状态指针
* @return 压入 Lua 栈的返回值数量
*/
static int Lua_SpawnActor(lua_State* L);

private:
// Lua 状态指针
lua_State* LuaState;
};

EveLuaManager.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Copyright Night Gamer. All Rights Reserved.

#include "EveLuaManager.h"
#include "EveLuaActor.h"
#include "Kismet/GameplayStatics.h"

// 构造函数
EveLuaManager::EveLuaManager()
: LuaState(nullptr)
{
}

// 析构函数
EveLuaManager::~EveLuaManager()
{
// 关闭 Lua 状态
Shutdown();
}

// 初始化 Lua 状态
bool EveLuaManager::Initialize()
{
// 创建新的 Lua 状态
LuaState = luaL_newstate();
if (!LuaState)
{
// 创建失败返回 false
return false;
}
// 打开 Lua 标准库
luaL_openlibs(LuaState);

// 重定向 Lua 的 print 函数
lua_register(LuaState, "Print", Lua_Print);
lua_register(LuaState, "CallMemberFunction", Lua_CallMemberFunction);
lua_register(LuaState, "SpawnActor", Lua_SpawnActor);

return true;
}

// 执行 Lua 脚本
bool EveLuaManager::ExecuteLuaScript(const char* Script)
{
if (!LuaState)
{
// Lua 状态未初始化,返回 false
return false;
}

// 执行 Lua 脚本
int result = luaL_dostring(LuaState, Script);
if (result != LUA_OK)
{
// 获取错误信息
const char* errorMsg = lua_tostring(LuaState, -1);
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Lua error: %s"), UTF8_TO_TCHAR(errorMsg));
// 弹出错误信息
lua_pop(LuaState, 1);
return false;
}
return true;
}

// 关闭 Lua 状态
void EveLuaManager::Shutdown()
{
if (LuaState)
{
// 关闭 Lua 状态
lua_close(LuaState);
LuaState = nullptr;
}
}

// Lua 的 Print 函数实现
int EveLuaManager::Lua_Print(lua_State* L)
{
// 获取栈上参数的数量
int n = lua_gettop(L);
FString Output;
for (int i = 1; i <= n; i++)
{
if (i > 1) Output += " ";
if (lua_isstring(L, i))
{
// 获取字符串参数
const char* str = lua_tostring(L, i);
Output += UTF8_TO_TCHAR(str);
}
else if (lua_isnumber(L, i))
{
// 获取数字参数
lua_Number num = lua_tonumber(L, i);
Output += FString::SanitizeFloat(num);
}
else if (lua_isboolean(L, i))
{
// 获取布尔参数
Output += lua_toboolean(L, i) ? "true" : "false";
}
else
{
// 获取参数类型名称
Output += lua_typename(L, lua_type(L, i));
}
}
// 输出日志
UE_LOG(LogTemp, Log, TEXT("%s"), *Output);
return 0;
}

// Lua 的 CallMemberFunction 函数实现
int EveLuaManager::Lua_CallMemberFunction(lua_State* L)
{
// 获取 Lua 栈上的参数
void* Object = lua_touserdata(L, 1); // 第一个参数是对象实例指针
if (!Object)
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Invalid object pointer passed to CallMemberFunction"));
// 返回 nil 表示错误
lua_pushnil(L);
return 1;
}

const char* FunctionNameStr = lua_tostring(L, 2); // 第二个参数是函数名
int NumParams = lua_gettop(L) - 2; // 剩余的参数是函数调用的参数

// 获取对象的类
AActor* Actor = static_cast<AActor*>(Object);
if (!Actor)
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Failed to cast object pointer to AActor"));
// 返回 nil 表示错误
lua_pushnil(L);
return 1;
}

UClass* Class = Actor->GetClass();

// 查找函数
UFunction* Function = Class->FindFunctionByName(FName(FunctionNameStr));
if (!Function)
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Function %s not found in class %s"), UTF8_TO_TCHAR(FunctionNameStr), *Class->GetName());
// 返回 nil 表示函数未找到
lua_pushnil(L);
return 1;
}

// 分配参数内存(确保对齐)
void* Params = FMemory::Malloc(Function->ParmsSize, Function->GetMinAlignment());
FMemory::Memzero(Params, Function->ParmsSize);

// 填充参数
int Index = 0;
for (TFieldIterator<FProperty> It(Function); It; ++It)
{
FProperty* Param = *It;

// 跳过返回值
if (Param == Function->GetReturnProperty())
{
continue;
}

// 只处理输入参数
if (Param->IsA(FIntProperty::StaticClass()) && Index < NumParams)
{
int32 Value = lua_tointeger(L, Index + 3);
*Param->ContainerPtrToValuePtr<int32>(Params) = Value;
UE_LOG(LogTemp, Log, TEXT("[Eve-Log-Cpp] Param %d: %d"), Index, *Param->ContainerPtrToValuePtr<int32>(Params));
Index++;
}
}

// 调用函数
UE_LOG(LogTemp, Log, TEXT("[Eve-Log-Cpp] Calling Function: %s, NumParams: %d"), *Function->GetName(), NumParams);
Actor->ProcessEvent(Function, Params);

// 处理返回值
FProperty* ReturnProp = Function->GetReturnProperty();
if (ReturnProp)
{
if (ReturnProp->IsA(FIntProperty::StaticClass()))
{
int32 ReturnValue = *ReturnProp->ContainerPtrToValuePtr<int32>(Params);
// 将返回值压入 Lua 栈
lua_pushinteger(L, ReturnValue);
}
else
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Unsupported return type: %s"), *ReturnProp->GetClass()->GetName());
// 不支持的返回值类型,返回 nil
lua_pushnil(L);
}
}
else
{
// 函数没有返回值,返回 nil
lua_pushnil(L);
}

// 释放参数内存
FMemory::Free(Params);

return 1; // 返回 1 表示有一个返回值压入了 Lua 栈
}

// Lua 的 SpawnActor 函数实现
int EveLuaManager::Lua_SpawnActor(lua_State* L)
{
UWorld* World = GWorld;
if (!World)
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] World is null"));
lua_pushnil(L);
return 1;
}

// 生成 AEveLuaActor
AActor* NewActor = World->SpawnActor<AEveLuaActor>();
if (!NewActor)
{
UE_LOG(LogTemp, Error, TEXT("[Eve-Log-Cpp] Failed to spawn AEveLuaActor"));
lua_pushnil(L);
return 1;
}

// 将 Actor 指针压入 Lua 栈
lua_pushlightuserdata(L, NewActor);
return 1;
}

EveLua.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- Copyright Night Gamer. All Rights Reserved.

-- 示例1
-- 调用 Lua 的 Print 函数,在 UE 日志中输出信息
Print("[Eve-Log-Lua] Hello from Lua!")

-- 示例2
-- 调用 Lua 的 SpawnActor 函数,生成一个 AEveLuaActor 实例
local actor = SpawnActor()
if actor then
-- 调用 Lua 的 CallMemberFunction 函数,调用 AEveLuaActor 的 Add 函数
local result = CallMemberFunction(actor, "Add", 2, 3)
if result then
-- 输出 Add 函数的返回值
Print("[Eve-Log-Lua] Add result: " .. result)
else
-- 调用失败,输出错误信息
Print("[Eve-Log-Lua] CallMemberFunction returned nil.")
end
else
-- 生成 Actor 失败,输出错误信息
Print("[Eve-Log-Lua] Failed to spawn actor.")
end

GMP泄漏

Lua层的FGMPCallback 实现,将U对象进行了AddReferencedObject操作

如果没有成对remove,会导致U对象(尤其是 UserWidget)无法被GC,造成内存泄露

其实根治方法也有(最新代码已按这个修改)

可以转成weak指针

GMP dispatch时,如果IsStale 为true,则不执行,并标记GMPSignal 自释放及清理队列

参考

(99+ 封私信 / 80 条消息) UnLua原理详解 - 知乎

[UE5 UnLua 脚本方案原理 | Yuerer’s Blog](https://www.yuerer.com/UE5 UnLua 脚本方案原理/)

Unlua代码分析 | FixCode Blog