前言 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 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 2 3 4 5 6 7 8 9 10 11 12 13 操作系统加载程序 ↓ 加载所有动态库(DLL/SO) ↓ 初始化全局变量区 ↓ 执行全局对象构造函数(按编译顺序) ↓ FExportedFMyMathLib::FExportedFMyMathLib() → ExportClass(this) ↓ 注册到全局容器GExportedClasses ↓ 程序启动后FLuaEnv::Initialize()遍历容器注册到Lua
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) { luaL_newmetatable (L, ClassName.Get ()); if (!SuperClassName.IsEmpty ()) { lua_pushstring (L, "Super" ); luaL_getmetatable (L, TCHAR_TO_UTF8 (*SuperClassName)); lua_rawset (L, -3 ); } lua_pushstring (L, "__index" ); lua_pushvalue (L, -2 ); lua_pushcclosure (L, UnLua::Index, 1 ); lua_rawset (L, -3 ); lua_getglobal (L, "UE" ); lua_pushstring (L, ClassName.Get ()); lua_pushvalue (L, -3 ); lua_rawset (L, -3 ); }
函数调用流程
– 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) { lua_getfield (L, LUA_REGISTRYINDEX, "UnLua_ObjectMap" ); lua_pushlightuserdata (L, Object); lua_newtable (L); PushObjectCore (L, Object); lua_pushstring (L, "Object" ); lua_pushvalue (L, -2 ); lua_rawset (L, -4 ); int32 ClassBoundRef = Env->GetManager ()->GetBoundRef (Class); lua_rawgeti (L, LUA_REGISTRYINDEX, ClassBoundRef); lua_getmetatable (L, -2 ); lua_setmetatable (L, -2 ); lua_setmetatable (L, -3 ); lua_pushvalue (L, -1 ); const auto Ref = luaL_ref (L, LUA_REGISTRYINDEX); ObjectRefs.Add (Object, Ref); lua_rawset (L, -3 ); 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 { if (LuaFunctions.Contains (Function->GetFName ())) { return true ; } 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) { lua_rawgeti (L, LUA_REGISTRYINDEX, FunctionRef); lua_rawgeti (L, LUA_REGISTRYINDEX, SelfRef); for (int i = 0 ; i < Properties.Num (); ++i) { if (!Properties[i]->IsReturnParameter ()) { Properties[i]->ReadValue_InContainer (L, Params, false ); } } 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 ; } if (HasReturnProperty ()) { RetProp->WriteValue (L, RetAddress, -NumResults); } }
五、两种绑定机制对比
特性
静态导出
动态绑定
注册时机
编译期(程序启动前)
运行时(对象创建时)
类型安全
编译期检查
运行时检查
调用开销
~1.5μs(直接调用)
~2.5μs(反射查询)
支持反射
否(不依赖UE反射)
是(基于UE反射)
热更新
否(需重新编译)
是(可替换Lua文件)
适用场景
数学库、工具函数、第三方库封装
Actor/Component、蓝图类、需要热更新的逻辑
六、关键数据结构
注册表(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 2 3 4 5 6 7 8 9 lua_newtable (L); int ref = luaL_ref (L, LUA_REGISTRYINDEX); lua_rawgeti (L, LUA_REGISTRYINDEX, ref); luaL_unref (L, LUA_REGISTRYINDEX, ref);
七、性能优化要点
缓存策略:频繁访问的元表和函数引用进行缓存
批量操作:减少Lua栈操作次数,使用批量参数传递
引用计数:合理使用luaL_ref/luaL_unref管理对象生命周期
避免GC压力:使用light userdata存储指针,减少内存分配
总结
UnLua的绑定机制通过静态导出和动态绑定两种方式,实现了C++与Lua的高效交互:
静态导出适用于性能敏感、类型固定的场景,通过编译期模板元编程实现零开销绑定
动态绑定适用于需要灵活性、热更新的场景,通过运行时反射和元表继承实现
双层注册架构(ClassRegistry + ObjectRegistry)分离了类型注册和实例绑定
函数覆写通过Hook ProcessEvent实现,支持Lua函数覆盖C++/蓝图函数
完整的GC管理通过双重引用系统确保内存安全
这种设计既保证了高性能,又提供了足够的灵活性,是UnLua能够在大型游戏项目中广泛应用的关键原因。
四个核心技术点 基于您提供的文档内容,这篇文章对UnLua框架的四个核心技术点——静态导出、动态绑定、元表和GC管理——进行了深入剖析。以下是这四部分的核心内容总结:
1. 静态导出
核心思想 :在编译期 完成绑定,实现零运行时开销 。它不依赖UE的运行时反射系统。
实现机制 :利用C++的全局对象构造函数 在main()函数执行前自动触发注册逻辑。通过模板元编程和特定的宏(如BEGIN_EXPORT_CLASS),将C++类型、函数的信息和调用入口注册到Lua环境中。
关键流程 :
程序启动时,所有通过宏定义的导出类会执行其全局构造函数,将自身注册到一个全局容器中。
Lua虚拟机初始化时(FLuaEnv::Initialize),会遍历这个容器,为每个导出的类创建对应的Lua命名元表,并将函数包装为Lua闭包注册进去。
特点与场景 :性能极高(~1.5μs),类型安全,但不支持热更新。适合 导出无状态的工具类、数学库、第三方C++库等。
2. 动态绑定
核心思想 :在运行时 根据UE的反射信息动态地将Lua模块绑定到UObject实例上,支持热更新。
实现机制 :基于UE的反射系统,在对象实例化时触发绑定流程。核心是FObjectRegistry::Bind函数,它会为每个UObject实例创建一个Lua端的INSTANCE表,并与对应的Lua模块、C++元表关联,形成多层查找链。
关键流程 :
Actor等对象实例化后,通过接口、组件或配置触发绑定。
加载对应的Lua模块文件,执行后得到一个模块表(MODULE)。
创建INSTANCE表,内部通过Userdata引用C++对象,并将其元表设置为MODULE。
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 GC 和UE GC 两套独立的垃圾回收系统,防止对象被任意一方提前回收或产生悬空指针。
双重GC协同机制 :
防UE回收 :当Lua通过PushObjectCore引用一个UObject时,会将其加入AutoObjectReference集合。这个集合会阻止UE GC回收这些被Lua引用的对象。
防悬空指针 :
二级指针 :Lua Userdata内部存储的是指向UObject指针的指针(void**),而非指针本身。
释放标记 :当UObject被UE销毁时,UnLua会收到通知,并将Userdata内的二级指针指向一个特殊的0xDEAD地址作为“已释放”标记。
访问安全 :每次通过Userdata访问C++对象前,都会检查指针是否为0xDEAD。如果是,则抛出Lua错误,从而安全地阻止了访问已释放内存。
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 #pragma once #include "CoreMinimal.h" extern "C" {#include "lua.h" #include "lualib.h" #include "lauxlib.h" } class EveLuaManager { public : EveLuaManager (); ~EveLuaManager (); bool Initialize () ; bool ExecuteLuaScript (const char * Script) ; void Shutdown () ; lua_State* GetLuaState () const { return LuaState; } static int Lua_Print (lua_State* L) ; static int Lua_CallMemberFunction (lua_State* L) ; static int Lua_SpawnActor (lua_State* L) ; private : 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 #include "EveLuaManager.h" #include "EveLuaActor.h" #include "Kismet/GameplayStatics.h" EveLuaManager::EveLuaManager () : LuaState (nullptr ) { } EveLuaManager::~EveLuaManager () { Shutdown (); } bool EveLuaManager::Initialize () { LuaState = luaL_newstate (); if (!LuaState) { return false ; } luaL_openlibs (LuaState); lua_register (LuaState, "Print" , Lua_Print); lua_register (LuaState, "CallMemberFunction" , Lua_CallMemberFunction); lua_register (LuaState, "SpawnActor" , Lua_SpawnActor); return true ; } bool EveLuaManager::ExecuteLuaScript (const char * Script) { if (!LuaState) { return false ; } 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 ; } void EveLuaManager::Shutdown () { if (LuaState) { lua_close (LuaState); LuaState = nullptr ; } } 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 ; } int EveLuaManager::Lua_CallMemberFunction (lua_State* L) { void * Object = lua_touserdata (L, 1 ); if (!Object) { UE_LOG (LogTemp, Error, TEXT ("[Eve-Log-Cpp] Invalid object pointer passed to CallMemberFunction" )); 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" )); 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 ()); 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_pushinteger (L, ReturnValue); } else { UE_LOG (LogTemp, Error, TEXT ("[Eve-Log-Cpp] Unsupported return type: %s" ), *ReturnProp->GetClass ()->GetName ()); lua_pushnil (L); } } else { lua_pushnil (L); } FMemory::Free (Params); return 1 ; } 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 ; } AActor* NewActor = World->SpawnActor <AEveLuaActor>(); if (!NewActor) { UE_LOG (LogTemp, Error, TEXT ("[Eve-Log-Cpp] Failed to spawn AEveLuaActor" )); lua_pushnil (L); return 1 ; } 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 Print("[Eve-Log-Lua] Hello from Lua!" ) local actor = SpawnActor()if actor then local result = CallMemberFunction(actor, "Add" , 2 , 3 ) if result then Print("[Eve-Log-Lua] Add result: " .. result) else Print("[Eve-Log-Lua] CallMemberFunction returned nil." ) end else 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 void * NewUserdataWithPadding (lua_State* L, int32 Size, int32 Alignment) { const int32 PaddingSize = (Alignment - sizeof (FUserdataDesc) % Alignment) % Alignment; const int32 TotalSize = sizeof (FUserdataDesc) + PaddingSize + Size; 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; } void FObjectRegistry::PushObjectCore (lua_State* L, UObject* Object) { void ** Userdata = (void **)NewUserdataWithPadding (L, sizeof (void *), 8 ); *Userdata = Object; luaL_setmetatable (L, "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 只会沿着 UPROPERTY、AddReferencedObjects 等 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 ); 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) { }
这样即使 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 表、UObject、UClass、UFunction,最后通过反射走到 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 大致会按这个顺序找:
Lua 实例表 INSTANCE 上是否有同名字段。
Lua 模块表 REQUIRED_MODULE 上是否有同名函数。
C++ 反射类型里是否有同名 UFunction / FProperty。
父类 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 会遍历 UFunction 的 FProperty:
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,例如 FVector、FRotator
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 覆写 :例如 ReceiveBeginPlay、ReceiveTick、蓝图事件、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); }
SelfRef 和 FunctionRef 一般都是 Lua 注册表引用:
1 2 lua_rawgeti (L, LUA_REGISTRYINDEX, FunctionRef); lua_rawgeti (L, LUA_REGISTRYINDEX, SelfRef);
然后 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,可以抽象成四步:
拿到 lua_State*。
从 Lua 注册表或全局表取 Lua 函数。
压入 self 和参数。
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); lua_rawgeti (L, LUA_REGISTRYINDEX, SelfRef); lua_pushnumber (L, DeltaSeconds); 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); lua_rawgeti (L, LUA_REGISTRYINDEX, SelfRef); 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 / FString、TArray、UStruct、Delegate。
C++ 调 Lua 没进函数 :检查 Lua 模块是否 require 成功、覆写函数名是否和 UE 事件名一致、FunctionRef 是否有效。
self 是 nil 或参数错位 :检查 C++ 调 Lua 时是否压入了 self,Lua 调 C++ 时是否用 : 而不是 .。
调用后随机崩溃 :检查 Lua 栈是否平衡、UObject 是否已经被释放、userdata 是否命中 ReleasedPtr。
返回值不对 :检查返回值数量、Out 参数顺序,以及 lua_pcall 的 NumResults 是否和读取逻辑一致。
原生 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 标准库,例如 print、table、string。
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 ); lua_pushstring (L, "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 ; }
函数签名必须是:
注册到 Lua 全局表:
1 lua_register (L, "Add" , Add);
Lua 侧调用:
1 2 local result = Add(10 , 20 )print (result)
完整调用链:
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" ); lua_pushnumber (L, 6 ); lua_pushnumber (L, 7 ); 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 表示不用自定义错误处理函数。
调用前栈形态:
调用后栈形态:
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" ); lua_getfield (L, -1 , "AddHp" ); lua_pushvalue (L, -2 ); lua_pushinteger (L, 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 ); return true ; }
这里最容易错的是 self。Lua 的冒号调用:
等价于:
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 里的 FunctionRef、SelfRef 很像:都不是保存“栈位置”,而是保存 Lua 注册表中的强引用。
9. 错误处理和栈平衡 原生互调最常见的问题是栈不平衡。建议每个 C++ 调 Lua 的函数都记录进入前的栈顶:
1 2 3 4 5 int Top = lua_gettop (L);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