前言

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 │
└──────────────────────────────────────────────────────┘

1
2
3
4
5
6
7
8
9
10
flowchart TD
A([Lua Call]) --> B[Class_CallUFunc]
B --> C[FFunctionDesc::CallUE]
C --> D1[PreCall: Lua → C++ 参数转换]
D1 --> D2[UObject::ProcessEvent]
D2 --> D3[PostCall: C++ → Lua 返回值]

style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style C fill:#fff3e0,stroke:#e65100,stroke-width:2px
style D2 fill:#f3e5f5,stroke:#4a148c,stroke-width:2px

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) │
└──────────────────────────────────────────────────────┘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A([ProcessEvent]) --> B[FLuaOverrider Hook]
B --> C{是否有<br/>Lua 覆写?}
C -->|是| D[FFunctionDesc::CallLua]
C -->|否| E[Super::ProcessEvent]

D --> D1[查找 Lua 函数]
D1 --> D2[C++ → Lua 参数转换]
D2 --> D3[lua_pcall]
D3 --> D4[Lua → C++ 返回值]

style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px
style C fill:#fff9c4,stroke:#f57f17,stroke-width:2px
style D fill:#ffccbc,stroke:#bf360c,stroke-width:2px

绑定机制

基于这篇文章,我来详细介绍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

GC

FObjectReferencer GC 引用计数管理、防悬空指针

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
┌───────────────────────────────────────────────────────────────┐
│ 双重 GC 协同机制 │
└───────────────────────────────────────────────────────────────┘

UE 端 ←→ Lua 端:
┌──────────────────┐ ┌──────────────────┐
│ UObject │ │ Userdata │
│ ├─ RefCount │ │ ├─ void** Ptr │ ───┐
│ └─ RF_Flags │ │ └─ BIT_FLAGS │ │
└────────┬─────────┘ └──────────────────┘ │
│ │ │
│ ◄───────────────────────────────────┘ │
│ (双向引用) │
▼ │
┌─────────────────────────────────────┐ │
│ FObjectReferencer │ ◄─────────────────────┘
│ (防止 UE GC 误回收被 Lua 引用的对象) │
│ ┌─────────────────────────────┐ │
│ │ TSet<UObject*> │ │
│ │ AutoObjectReference │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘


┌─────────────────────────────────────┐
│ UE GC System │
│ ├─ MarkAsGarbage │
│ ├─ NotifyObjectDestroyed ─────────┼─► FObjectRegistry::Unbind()
│ └─ ... │ └─► 标记 Userdata 为 0xDEAD
└─────────────────────────────────────┘

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
// LuaCore.cpp:345
void* NewUserdataWithPadding(lua_State* L, int32 Size, int32 Alignment)
{
// ★ 计算需要的内存大小
const int32 PaddingSize = (Alignment - sizeof(FUserdataDesc) % Alignment) % Alignment;
const int32 TotalSize = sizeof(FUserdataDesc) + PaddingSize + Size;

// ★ API讲解: lua_newuserdata
// 功能: 分配指定大小的内存并压栈
// 栈变化: [+1, 压入userdata]
// 返回值: 指向分配内存的指针
void* Userdata = lua_newuserdata(L, TotalSize);

// ★ 初始化描述符
FUserdataDesc* Desc = (FUserdataDesc*)Userdata;
Desc->magic = 0x1688; // 魔数验证
Desc->tag = 0;
Desc->padding = PaddingSize;

// ★ 返回数据区指针 (跳过描述符和填充)
return (uint8*)Userdata + sizeof(FUserdataDesc) + PaddingSize;
}

// ObjectRegistry.cpp:87
void FObjectRegistry::PushObjectCore(lua_State* L, UObject* Object)
{
// ★ 步骤1: 分配 Userdata (存储二级指针)
void** Userdata = (void**)NewUserdataWithPadding(L, sizeof(void*), 8);

// ★ 步骤2: 存储 UObject 指针
*Userdata = Object; // userdata 内容 = Object的地址

// ★ 步骤3: 设置元表
// API: luaL_setmetatable(L, name)
// 功能: 从registry获取元表并设置给栈顶userdata
// 等价于: luaL_getmetatable(L, name); lua_setmetatable(L, -2);
luaL_setmetatable(L, "UObject");

// 栈: [userdata] (已关联UObject元表)
}

GMP泄漏

FObjectReferencer GC 引用计数管理、防悬空指针

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

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

可以转成weak指针

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

UnLua 的 Lua / UE 双端 GC 是怎么实现的

UnLua 的 GC 问题本质上是:Lua GC 只认识 Lua 对象,UE GC 只认识 UObject 引用链。所以 UnLua 不能简单把 UObject* 塞进 Lua,也不能让 Lua 表随便被回收;它需要在两套 GC 之间建立一层“引用桥”,让双方都能在对象还被另一边使用时活着,并在任意一边释放时安全解绑。

1. Lua 侧怎么持有 UE 对象

UObject 被 Push 到 Lua 时,UnLua 会创建一个 userdata

1
2
3
void** Userdata = (void**)NewUserdataWithPadding(L, sizeof(void*), 8);
*Userdata = Object;
luaL_setmetatable(L, "UObject");

这里注意两个点:

  • userdata 里保存的是 void** 二级指针,不是直接裸 UObject*
  • 二级指针的好处是:当 UE 侧对象已经销毁时,UnLua 可以把 *Userdata 改成 ReleasedPtr,常见分析里会写成 0xDEAD,之后 Lua 再访问时可以检查出来并报错,而不是继续访问野指针。

绑定 Actor / Widget 这类对象时,UnLua 不只是创建 userdata,还会创建 Lua 侧实例表:

1
2
3
local INSTANCE = {}
INSTANCE.Object = RAW_UOBJECT
setmetatable(INSTANCE, REQUIRED_MODULE)

REQUIRED_MODULE / INSTANCE / 函数表等对象会通过 luaL_ref(L, LUA_REGISTRYINDEX) 放进 Lua 注册表。Lua 注册表是强引用表,只要引用没有 luaL_unref,Lua GC 就不会回收这些表。也就是说:被 UnLua 正式绑定的 Lua 对象,生命周期主要由 UE 对象的解绑来驱动,而不是单纯等 Lua GC 自动回收

2. UE 侧怎么知道 Lua 还引用着 UObject

UE GC 只会沿着 UPROPERTYAddReferencedObjects 等 UE 引用系统标记对象。Lua 里的引用 UE 默认看不见,所以 UnLua 使用 FObjectReferencer 这类对象接入 UE GC。

核心形式类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FObjectReferencer : public FGCObject
{
public:
void Add(UObject* Object)
{
if (Object)
ReferencedObjects.Add(Object);
}

void Remove(UObject* Object)
{
if (Object)
ReferencedObjects.Remove(Object);
}

virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObjects(ReferencedObjects);
}

private:
TSet<UObject*> ReferencedObjects;
};

FGCObject::AddReferencedObjects 会在 UE GC 标记阶段被调用。只要对象还在 ReferencedObjects 里,UE GC 就认为它仍然可达,不会回收。UnLua 中常见有两类引用:

  • ManualObjectReference:手动引用,典型入口是 UnLua.Ref(Object) / UnLua.Unref 或引用句柄被 Lua GC 回收。
  • AutoObjectReference:自动引用,常用于一些需要跨 Lua / UE 生命周期的辅助对象,例如 Delegate Handler,防止 Lua 回调仍可能触发时 UE 对象先被 GC。

所以 Lua 想“强持有”一个 UE 对象时,不是靠 Lua 表本身让 UE 看见,而是通过 FObjectReferencer 把这个 UObject 补进 UE 的引用收集器。

3. Lua GC 发生时做什么

Lua 侧的 userdata / 引用代理对象会设置 __gc 元方法。Lua GC 真正回收这些对象时,会回调到 C++,典型逻辑是:

1
2
3
4
5
6
7
8
9
10
11
static int32 UObject_Delete(lua_State* L)
{
UObject* Object = GetUObjectFromUserdata(L, 1);

// 1. 从 ObjectMap / Lua Registry 中解除映射
// 2. 从 ManualObjectReference / AutoObjectReference 中移除 UE 强引用
// 3. 必要时 luaL_unref,释放 Lua 注册表强引用
// 4. 将 userdata 指针标记为 ReleasedPtr

return 0;
}

这一步解决的是:Lua 不再需要这个对象时,要通知 UE 侧引用桥撤掉引用。否则 FObjectReferencer 还一直持有 UObject,UE GC 就永远回收不了,常见表现就是 UserWidget、Delegate、Callback 泄漏。

4. UE GC / UObject 销毁时做什么

另一条路径是 UE 先销毁对象。UnLua 模块会通过 GUObjectArray 监听 UObject 创建和销毁:

1
2
GUObjectArray.AddUObjectCreateListener(this);
GUObjectArray.AddUObjectDeleteListener(this);

当 UE 侧对象进入销毁流程时,UnLua 会收到删除通知,然后做解绑:

1
2
3
4
5
6
7
void FObjectRegistry::Unbind(UObject* Object)
{
// 1. 从 ObjectMap 中找到 UObject 对应的 Lua userdata / table
// 2. luaL_unref 释放 INSTANCE、Module Table、FunctionRef 等注册表引用
// 3. 从 FObjectReferencer 中移除 Object
// 4. 把 userdata 里的指针写成 ReleasedPtr
}

这样即使 Lua 代码里还残留旧表或旧 userdata,后续访问时也会因为 ReleasedPtr 检查失败而报错,不会直接崩溃。

5. 双端 GC 的完整时序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TD
A[UE 创建 UObject] --> B[UnLua Bind]
B --> C[创建 Lua userdata]
B --> D[创建 INSTANCE 表]
C --> E[ObjectMap 记录 UObject 到 Lua 对象]
D --> F[luaL_ref 放入 Lua Registry]

G[Lua 需要强持有 UObject] --> H[FObjectReferencer.Add]
H --> I[UE GC AddReferencedObjects 标记 UObject]

J[Lua GC 回收 userdata / RefHandle] --> K[__gc / UObject_Delete]
K --> L[luaL_unref + Referencer.Remove]
L --> M[UE GC 后续可回收 UObject]

N[UE 先销毁 UObject] --> O[NotifyUObjectDeleted]
O --> P[FObjectRegistry::Unbind]
P --> Q[释放 Lua Registry 引用]
P --> R[userdata 指针标记 ReleasedPtr]

6. 关键结论

  • Lua 侧防回收:靠 luaL_ref(L, LUA_REGISTRYINDEX) 把实例表、模块表、函数引用放进 Lua 注册表,形成 Lua 强引用。
  • UE 侧防回收:靠 FObjectReferencer : FGCObject,在 AddReferencedObjects 中把 Lua 需要保活的 UObject 加给 UE 的 FReferenceCollector
  • 防悬空指针:Lua userdata 使用二级指针,UE 对象销毁后写成 ReleasedPtr / 0xDEAD,访问前检查。
  • Lua 释放 UE 引用:Lua GC 触发 __gc,调用 UObject_Delete / ReleaseManualRef,移除 FObjectReferencer 中的对象并 luaL_unref
  • UE 释放 Lua 引用:UE 删除 UObject 时触发 NotifyUObjectDeleted,UnLua 执行 Unbind,释放 Lua 注册表引用并标记 userdata。
  • 泄漏高发点Add / Remove 不成对、Delegate 回调没释放、UnLua.Ref 后没有清理、Lua 注册表引用没有 luaL_unref,都会导致 Lua 表或 UObject 无法被对应 GC 回收。

双端互调

UnLua 的 C++ 调用 Lua 与 Lua 调用 C++

UnLua 的双向调用可以分成两条链路:

  • **Lua 调用 C++**:Lua 访问 UE 表、UObjectUClassUFunction,最后通过反射走到 UObject::ProcessEvent 或静态导出函数。
  • C++ 调用 Lua:UE 事件或 C++ 主动调用进入 ProcessEvent / CallLua,UnLua 从 Lua 注册表取出 Lua 函数和 self,再用 lua_pcall 执行。

1. Lua 调用 C++ 的整体流程

Lua 侧代码通常长这样:

1
2
local actor = UE.UGameplayStatics.GetPlayerPawn(self, 0)
actor:SetActorLocation(UE.FVector(100, 200, 300), false, nil, true)

看起来像普通 Lua 方法调用,实际会经过 UnLua 的元表和反射系统:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A[Lua: actor:SetActorLocation] --> B[查找 actor 的 metatable.__index]
B --> C[FObjectRegistry / FClassRegistry 找到 ClassDesc]
C --> D[按函数名查找或创建 FFunctionDesc]
D --> E[生成 C Closure 并缓存到 Lua 表]
E --> F[执行 Class_CallUFunction]
F --> G[FFunctionDesc::CallUE]
G --> H[PreCall: Lua 参数写入 UE 参数内存]
H --> I[UObject::ProcessEvent]
I --> J[PostCall: 返回值 / Out 参数写回 Lua 栈]

核心点是:Lua 并不直接知道 C++ 函数地址。它通过 __index 找到一个 C Closure,这个 Closure 的 upvalue 里保存了 FFunctionDesc,真正调用时再用 UFunction 反射完成参数打包和调用。

2. Lua 调用 C++ 的关键步骤

2.1 字段查找:__index

当 Lua 执行:

1
actor:SetActorLocation(...)

Lua 会先查 actor 这个 userdata / instance table 上有没有 SetActorLocation。如果没有,就触发元表的 __index。UnLua 的 __index 大致会按这个顺序找:

  1. Lua 实例表 INSTANCE 上是否有同名字段。
  2. Lua 模块表 REQUIRED_MODULE 上是否有同名函数。
  3. C++ 反射类型里是否有同名 UFunction / FProperty
  4. 父类 Super 链上是否存在。

找到 C++ 函数后,会生成一个 Lua C Closure:

1
2
3
lua_pushlightuserdata(L, FunctionDesc);
lua_pushcclosure(L, Class_CallUFunction, 1);
lua_rawset(L, CacheIndex);

后续再次访问同名函数时,直接从缓存取 Closure,避免每次都反射查找。

2.2 参数转换:Lua 栈到 UE 参数内存

UE 反射调用需要一块连续参数内存:

1
2
void* Params = FMemory_Alloca(Function->ParmsSize);
FMemory::Memzero(Params, Function->ParmsSize);

UnLua 会遍历 UFunctionFProperty

1
2
3
4
5
for (TFieldIterator<FProperty> It(Function); It; ++It)
{
FProperty* Property = *It;
PropertyDesc->WriteValue(L, Params, LuaStackIndex);
}

常见转换关系:

  • Lua number -> int32 / float / double
  • Lua string -> FString / FName / FText
  • Lua boolean -> bool
  • Lua table / userdata -> UStruct,例如 FVectorFRotator
  • Lua userdata -> UObject*
  • Lua function -> Delegate / Callback

如果是 Out 参数或返回值,调用前需要准备内存,调用后再从 Params 里读回 Lua 栈。

2.3 反射调用:ProcessEvent

对象成员函数最终通常走:

1
Object->ProcessEvent(Function, Params);

静态导出函数或非反射导出的 C++ 函数,则可能走 UnLua 静态导出系统保存的函数指针,例如:

1
2
3
BEGIN_EXPORT_CLASS(FMyMathLib)
ADD_STATIC_FUNCTION(Add)
END_EXPORT_CLASS()

Lua 调用:

1
local Result = UE.FMyMathLib.Add(10, 20)

这类静态导出不一定经过 UObject::ProcessEvent,而是由导出注册表直接分发到 C++ 函数指针。

2.4 返回值和 Out 参数

ProcessEvent 执行完成后,UnLua 会把返回值和 Out 参数压回 Lua 栈:

1
2
3
4
5
6
7
8
9
if (ReturnProperty)
{
ReturnPropertyDesc->ReadValue(L, Params);
}

for (FProperty* OutProperty : OutProperties)
{
OutPropertyDesc->ReadValue(L, Params);
}

所以 Lua 侧可以这样接:

1
local ok, hit = UE.UKismetSystemLibrary.LineTraceSingle(...)

实际返回数量由 ReturnValue + OutParams 决定。

3. C++ 调用 Lua 的整体流程

C++ 调 Lua 分两种常见场景:

  • UE 事件被 Lua 覆写:例如 ReceiveBeginPlayReceiveTick、蓝图事件、RPC 等。
  • C++ 主动调用 Lua 函数:例如拿到某个 Lua 对象的 LuaRef,从注册表取函数并 lua_pcall

事件覆写的主链路如下:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A[UE 调用 UFunction] --> B[UObject::ProcessEvent]
B --> C[UnLua ProcessEvent Hook / FLuaOverrider]
C --> D{Lua 是否覆写该函数}
D -->|否| E[走原始 C++ / 蓝图逻辑]
D -->|是| F[从 Lua Registry 取 INSTANCE]
F --> G[查找 Lua 函数]
G --> H[CallLua: C++ 参数压入 Lua 栈]
H --> I[lua_pcall]
I --> J[返回值 / Out 参数写回 UE 参数内存]

4. C++ 调用 Lua:事件覆写

Lua 模块中常见写法:

1
2
3
4
5
6
7
8
9
10
11
local M = Class()

function M:ReceiveBeginPlay()
print("BeginPlay from Lua", self)
end

function M:ReceiveTick(DeltaSeconds)
-- 每帧逻辑
end

return M

UE 侧 Actor 触发 BeginPlay 时,底层会进入 ProcessEvent。UnLua Hook 后会判断这个 UFunction 是否有 Lua 覆写:

1
2
3
4
5
6
7
8
9
bool bOverridden = LuaOverrides->IsOverridden(Function);
if (bOverridden)
{
FunctionDesc->CallLua(L, FunctionRef, SelfRef, Stack, RESULT_PARAM);
}
else
{
Super::ProcessEvent(Function, Params);
}

SelfRefFunctionRef 一般都是 Lua 注册表引用:

1
2
lua_rawgeti(L, LUA_REGISTRYINDEX, FunctionRef); // push Lua function
lua_rawgeti(L, LUA_REGISTRYINDEX, SelfRef); // push self

然后 UnLua 把 C++ 参数按 FProperty 逐个压栈:

1
2
3
4
for (FProperty* Property : InputProperties)
{
PropertyDesc->ReadValue(L, Params);
}

最后执行:

1
int32 Status = lua_pcall(L, NumParams + 1, NumResults, ErrorHandlerIndex);

这里的 +1 是因为 Lua 面向对象调用需要把 self 作为第一个参数传入。

5. C++ 主动调用 Lua 函数

如果不是 UE 事件覆写,而是 C++ 想主动调用 Lua,可以抽象成四步:

  1. 拿到 lua_State*
  2. 从 Lua 注册表或全局表取 Lua 函数。
  3. 压入 self 和参数。
  4. lua_pcall 并读取返回值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool CallLuaFunction(lua_State* L, int32 FunctionRef, int32 SelfRef, float DeltaSeconds)
{
const int32 Top = lua_gettop(L);

lua_rawgeti(L, LUA_REGISTRYINDEX, FunctionRef); // function
lua_rawgeti(L, LUA_REGISTRYINDEX, SelfRef); // self
lua_pushnumber(L, DeltaSeconds); // param

const int32 Status = lua_pcall(L, 2, 0, 0);
if (Status != LUA_OK)
{
const char* Error = lua_tostring(L, -1);
UE_LOG(LogTemp, Error, TEXT("Lua error: %s"), UTF8_TO_TCHAR(Error));
lua_pop(L, 1);
lua_settop(L, Top);
return false;
}

lua_settop(L, Top);
return true;
}

实际 UnLua 会额外做:

  • 错误栈处理和 traceback。
  • 参数类型检查。
  • UObject 生命周期检查。
  • Out 参数和返回值写回。
  • Lua 栈平衡校验。

6. C++ / Lua 双向调用中的 self

Lua 中的冒号调用:

1
actor:SetActorLocation(Location)

等价于:

1
actor.SetActorLocation(actor, Location)

所以 Lua 调 C++ 时,actor 会作为第一个隐式参数参与调用。UnLua 的 Class_CallUFunction 会从这个 self 中取出真实 UObject*

C++ 调 Lua 也一样,如果调用的是 Lua 对象方法,必须先压函数,再压 self

1
2
3
lua_rawgeti(L, LUA_REGISTRYINDEX, FunctionRef); // function
lua_rawgeti(L, LUA_REGISTRYINDEX, SelfRef); // self
lua_pcall(L, 1, 0, ErrorHandler);

如果少压 self,Lua 方法里的 self 就会错位,表现为访问成员变量是 nil,或者第一个业务参数被当成 self

7. 调用链对比

方向 入口 查找方式 参数转换 真正执行 返回处理
Lua -> C++ UObject actor:Func(...) __index -> FFunctionDesc Lua 栈 -> FProperty 参数内存 UObject::ProcessEvent 参数内存 -> Lua 栈
Lua -> C++ 静态导出 UE.Foo.Bar(...) 静态导出注册表 Lua 栈 -> C++ 类型 导出函数指针 C++ 返回值 -> Lua 栈
C++ -> Lua 覆写事件 ProcessEvent FLuaOverrider / FunctionRef FProperty 参数内存 -> Lua 栈 lua_pcall Lua 栈 -> 返回值 / Out 参数
C++ -> Lua 主动调用 C++ 持有 LuaRef lua_rawgeti C++ 值手动压栈 lua_pcall 手动读取 Lua 栈

8. 常见问题

  • Lua 调 C++ 找不到函数:检查函数是否是 UFUNCTION、名字是否正确、对象是否已经绑定、静态导出宏是否注册。
  • 参数类型不匹配:重点看 FProperty 类型和 Lua 实参类型,尤其是 FName / FStringTArrayUStruct、Delegate。
  • C++ 调 Lua 没进函数:检查 Lua 模块是否 require 成功、覆写函数名是否和 UE 事件名一致、FunctionRef 是否有效。
  • self 是 nil 或参数错位:检查 C++ 调 Lua 时是否压入了 self,Lua 调 C++ 时是否用 : 而不是 .
  • 调用后随机崩溃:检查 Lua 栈是否平衡、UObject 是否已经被释放、userdata 是否命中 ReleasedPtr
  • 返回值不对:检查返回值数量、Out 参数顺序,以及 lua_pcallNumResults 是否和读取逻辑一致。

原生 C++ 与 Lua 互调

如果不考虑 UE / UnLua,只看原生 C++ 和 Lua 的交互,本质上全部围绕一个对象:lua_State* L。它既代表 Lua 虚拟机状态,也代表当前调用栈。C++ 和 Lua 互相传值,不是直接传 C++ 变量,而是通过 Lua 栈完成。

1. 最小运行环境

原生 C++ 嵌入 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
extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}

int main()
{
lua_State* L = luaL_newstate();
if (!L)
return -1;

luaL_openlibs(L);

if (luaL_dofile(L, "main.lua") != LUA_OK)
{
const char* Error = lua_tostring(L, -1);
printf("Lua error: %s\n", Error);
lua_pop(L, 1);
}

lua_close(L);
return 0;
}

这段代码里最关键的是:

  • luaL_newstate:创建 Lua VM。
  • luaL_openlibs:打开 Lua 标准库,例如 printtablestring
  • luaL_dofile:加载并执行 Lua 文件。
  • lua_close:销毁 Lua VM,触发 Lua 侧对象清理。

2. Lua 栈模型

Lua C API 的核心是栈。C++ 通过 lua_push* 把值压入 Lua 栈,通过 lua_to* 从栈上读取值。

1
2
3
4
5
6
7
lua_pushnumber(L, 3.14);       // 栈: [3.14]
lua_pushstring(L, "hello"); // 栈: [3.14, "hello"]

double A = lua_tonumber(L, -2);
const char* B = lua_tostring(L, -1);

lua_pop(L, 2); // 栈清空

栈索引有两种:

  • 正数索引:从栈底开始,1 是第一个参数。
  • 负数索引:从栈顶开始,-1 是栈顶,-2 是栈顶下面一个。

Lua 调 C 函数时,Lua 传入的参数会从 1 开始放在栈上。C 函数返回值则由 C++ 压栈,并通过返回整数告诉 Lua 返回了几个值。

3. Lua 调用 C++ 函数

原生 Lua 调 C++ 的关键是注册一个 lua_CFunction

1
2
3
4
5
6
7
8
static int Add(lua_State* L)
{
double A = luaL_checknumber(L, 1);
double B = luaL_checknumber(L, 2);

lua_pushnumber(L, A + B);
return 1;
}

函数签名必须是:

1
int Func(lua_State* L);

注册到 Lua 全局表:

1
lua_register(L, "Add", Add);

Lua 侧调用:

1
2
local result = Add(10, 20)
print(result) -- 30

完整调用链:

1
2
3
4
5
6
7
flowchart TD
A[Lua: Add 10 20] --> B[Lua VM 查找全局函数 Add]
B --> C[进入 C 函数 Add lua_State* L]
C --> D[C++ 从栈索引 1 / 2 读取参数]
D --> E[C++ 计算结果]
E --> F[lua_pushnumber 压入返回值]
F --> G[return 1 告诉 Lua 有 1 个返回值]

4. C++ 调用 Lua 全局函数

假设 Lua 文件里有:

1
2
3
function Mul(a, b)
return a * b
end

C++ 调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool CallLuaMul(lua_State* L)
{
lua_getglobal(L, "Mul"); // push function
lua_pushnumber(L, 6); // arg1
lua_pushnumber(L, 7); // arg2

if (lua_pcall(L, 2, 1, 0) != LUA_OK)
{
const char* Error = lua_tostring(L, -1);
printf("Lua error: %s\n", Error);
lua_pop(L, 1);
return false;
}

double Result = lua_tonumber(L, -1);
lua_pop(L, 1);

printf("Result = %.0f\n", Result);
return true;
}

lua_pcall(L, 2, 1, 0) 的含义:

  • 第一个参数 2:传给 Lua 函数的参数数量。
  • 第二个参数 1:期望 Lua 函数返回的结果数量。
  • 第三个参数 0:错误处理函数在栈上的索引,0 表示不用自定义错误处理函数。

调用前栈形态:

1
[function Mul, 6, 7]

调用后栈形态:

1
[42]

5. C++ 调用 Lua 表里的函数

Lua 侧:

1
2
3
4
5
6
7
Player = {
Name = "Tom",
AddHp = function(self, value)
self.Hp = (self.Hp or 0) + value
return self.Hp
end
}

C++ 调用 Player:AddHp(10)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool CallPlayerAddHp(lua_State* L)
{
lua_getglobal(L, "Player"); // stack: Player
lua_getfield(L, -1, "AddHp"); // stack: Player, AddHp
lua_pushvalue(L, -2); // stack: Player, AddHp, Player(self)
lua_pushinteger(L, 10); // stack: Player, AddHp, Player, 10

if (lua_pcall(L, 2, 1, 0) != LUA_OK)
{
const char* Error = lua_tostring(L, -1);
printf("Lua error: %s\n", Error);
lua_pop(L, 1);
return false;
}

int Hp = (int)lua_tointeger(L, -1);
lua_pop(L, 2); // pop return value and Player table
return true;
}

这里最容易错的是 self。Lua 的冒号调用:

1
Player:AddHp(10)

等价于:

1
Player.AddHp(Player, 10)

所以 C++ 手动调用表方法时,也必须把表本身再压一次作为 self

6. C++ 暴露对象给 Lua

最简单的方式是 lightuserdata,直接把 C++ 指针塞给 Lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct FNativePlayer
{
int Hp = 100;
};

static int GetHp(lua_State* L)
{
FNativePlayer* Player = (FNativePlayer*)lua_touserdata(L, 1);
lua_pushinteger(L, Player->Hp);
return 1;
}

FNativePlayer Player;
lua_pushlightuserdata(L, &Player);
lua_setglobal(L, "NativePlayer");
lua_register(L, "GetHp", GetHp);

Lua 侧:

1
print(GetHp(NativePlayer))

lightuserdata 的特点:

  • 只是一个裸指针,不由 Lua 管理生命周期。
  • 没有独立 metatable,适合临时传递指针。
  • 如果 C++ 对象先释放,Lua 侧还拿着指针,就会变成悬空指针。

更安全的方式是 full userdata,由 Lua 分配一块内存保存指针或对象,并可以设置 __gc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int PlayerGC(lua_State* L)
{
FNativePlayer** Userdata = (FNativePlayer**)luaL_checkudata(L, 1, "NativePlayerMT");
delete *Userdata;
*Userdata = nullptr;
return 0;
}

static void PushPlayer(lua_State* L)
{
FNativePlayer** Userdata = (FNativePlayer**)lua_newuserdata(L, sizeof(FNativePlayer*));
*Userdata = new FNativePlayer();

if (luaL_newmetatable(L, "NativePlayerMT"))
{
lua_pushcfunction(L, PlayerGC);
lua_setfield(L, -2, "__gc");
}

lua_setmetatable(L, -2);
}

full userdata 的好处是可以绑定元表、__index__gc,更接近 UnLua 里 UObject userdata 的设计。

7. 用元表实现 Lua 面向对象调用 C++

可以给 userdata 设置方法表,让 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
27
28
29
static int PlayerGetHp(lua_State* L)
{
FNativePlayer** Userdata = (FNativePlayer**)luaL_checkudata(L, 1, "NativePlayerMT");
lua_pushinteger(L, (*Userdata)->Hp);
return 1;
}

static int PlayerAddHp(lua_State* L)
{
FNativePlayer** Userdata = (FNativePlayer**)luaL_checkudata(L, 1, "NativePlayerMT");
int Value = (int)luaL_checkinteger(L, 2);
(*Userdata)->Hp += Value;
lua_pushinteger(L, (*Userdata)->Hp);
return 1;
}

static void RegisterPlayer(lua_State* L)
{
luaL_newmetatable(L, "NativePlayerMT");

lua_newtable(L);
lua_pushcfunction(L, PlayerGetHp);
lua_setfield(L, -2, "GetHp");
lua_pushcfunction(L, PlayerAddHp);
lua_setfield(L, -2, "AddHp");

lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}

Lua 侧就可以:

1
2
3
local player = CreatePlayer()
print(player:GetHp())
print(player:AddHp(20))

这就是很多绑定库的基础:userdata 保存 C++ 对象,metatable 提供方法查找,__gc 负责释放或解绑生命周期

8. C++ 保存 Lua 函数回调

如果 C++ 想保存 Lua 函数,不能直接保存栈索引,因为栈会变化。正确做法是把函数放进 Lua 注册表,保存一个引用 id:

1
2
3
4
5
6
7
8
9
10
11
int CallbackRef = LUA_NOREF;

static int SetCallback(lua_State* L)
{
luaL_checktype(L, 1, LUA_TFUNCTION);

lua_pushvalue(L, 1);
CallbackRef = luaL_ref(L, LUA_REGISTRYINDEX);

return 0;
}

C++ 之后触发回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void FireCallback(lua_State* L, int Value)
{
if (CallbackRef == LUA_NOREF)
return;

lua_rawgeti(L, LUA_REGISTRYINDEX, CallbackRef);
lua_pushinteger(L, Value);

if (lua_pcall(L, 1, 0, 0) != LUA_OK)
{
const char* Error = lua_tostring(L, -1);
printf("Lua callback error: %s\n", Error);
lua_pop(L, 1);
}
}

不再需要时释放引用:

1
2
luaL_unref(L, LUA_REGISTRYINDEX, CallbackRef);
CallbackRef = LUA_NOREF;

这点和 UnLua 里的 FunctionRefSelfRef 很像:都不是保存“栈位置”,而是保存 Lua 注册表中的强引用。

9. 错误处理和栈平衡

原生互调最常见的问题是栈不平衡。建议每个 C++ 调 Lua 的函数都记录进入前的栈顶:

1
2
3
4
5
int Top = lua_gettop(L);

// push function, push args, lua_pcall, read results...

lua_settop(L, Top);

这样即使中途出错,也能恢复栈。更完整的错误处理会额外压入 traceback 函数:

1
2
3
4
5
6
7
8
9
static int Traceback(lua_State* L)
{
const char* Msg = lua_tostring(L, 1);
if (Msg)
luaL_traceback(L, L, Msg, 1);
else
lua_pushliteral(L, "(no error message)");
return 1;
}

调用时:

1
2
3
4
5
6
7
8
9
10
11
12
13
int Base = lua_gettop(L);
lua_pushcfunction(L, Traceback);
int ErrorFunc = lua_gettop(L);

lua_getglobal(L, "Main");
lua_pushinteger(L, 123);

if (lua_pcall(L, 1, 0, ErrorFunc) != LUA_OK)
{
printf("%s\n", lua_tostring(L, -1));
}

lua_settop(L, Base);

10. 原生互调和 UnLua 的关系

原生 C++ / Lua UnLua / UE
lua_State* 管理 Lua VM FLuaEnv 管理 Lua VM
lua_register 注册 C 函数 静态导出宏 / 反射注册 C++ 函数
lua_push* / lua_to* 手动转类型 FPropertyDesc 自动处理类型转换
lua_pcall 调 Lua 函数 FFunctionDesc::CallLua 封装 lua_pcall
userdata 保存 C++ 指针 userdata 保存 UObject* / 二级指针
metatable 提供 __index / __gc UnLua 用元表连接 Lua 表和 UE 反射
luaL_ref 保存 Lua 回调 FunctionRef / SelfRef / ObjectRef
C++ 自己管理对象生命周期 UE GC + Lua GC + FObjectReferencer 协同

所以 UnLua 并没有绕开 Lua C API。它是在原生互调机制上,额外加了 UE 反射、UObject 生命周期、自动参数转换、ProcessEvent Hook、双端 GC 管理。

参考

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

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

Unlua代码分析 | FixCode Blog