Unity游戏引擎开发者联盟

 找回密码
 立即注册
查看: 61|回复: 0

slua unreal分析( 三)slua与GC

[复制链接]
发表于 2021-4-14 14:51 | 显示全部楼层 |阅读模式
相关文章:
本章介绍slua与luaGC,UEGC的关系,是比较重要的一点。
UE4使用GC管理UObject,lua也有GC管理l对象,因此UE和lua进行数据传递时,slua需要准确的加引用和减引用操作。这样,UE在GC时知道哪些UObject正在被lua runtime使用,相应的,lua在gc时也能知道哪些对象正在被UE4 runtime使用,从而进行正确的GC行为。但是也要记住,UE可以显式调用Destroy()销毁Actor,也可以使用MarkPendingKill()来销毁UObject,即使还有引用存在。
UObject从UE传递到lua

我们知道,UObject会被UE的GC管理,因此首先会想到把UObject传递到lua该如何处理引用?
严格的说,UObject本身并没有传到lua,前文说过,真正传到lua的是UserData结构体。
入口代码如下:
  1. int LuaObject::push(lua_State* L, UObject* obj, bool rawpush, bool ref) {
  2.     if (!obj) return pushNil(L);
  3.     if (!rawpush) {
  4.         if (auto it = Cast<ILuaTableObjectInterface>(obj)) {
  5.             return ILuaTableObjectInterface::push(L, it);
  6.         }
  7.     }
  8.     if (auto e = Cast<UEnum>(obj)
  9.         return pushEnum(L, e);
  10.     else if (auto c = Cast<UClass>(obj))
  11.         return pushClass(L, c);
  12.     else if (auto s = Cast<UScriptStruct>(obj))
  13.         return pushStruct(L, s);
  14.     else
  15.         return pushGCObject<UObject*>(L,obj,"UObject",setupInstanceMT,gcObject,ref);
  16. }
复制代码
我们先看rawpush的情况,rawpush会直接调用pushGCObject函数,函数内容如下:
  1. template<typename T>
  2. static int pushGCObject(lua_State* L,T obj,const char* tn,lua_CFunction setupmt,lua_CFunction gc,bool ref) {
  3.     if(getFromCache(L,obj,tn)) return 1;
  4.     lua_pushcclosure(L,gc,0);
  5.     int f = lua_gettop(L);
  6.     int r = pushType<T>(L,obj,tn,setupmt,f);
  7.     lua_remove(L,f); // remove wraped gc function
  8.     if (r) {
  9.         addRef(L, obj, lua_touserdata(L, -1), ref);
  10.         cacheObj(L, obj);
  11.     }
  12.     return r;
  13. }
复制代码
把UObject传递到lua,核心步骤为在lua中创建一个UserData,并把ud属性与该UObject进行关联,该操作在pushType函数中完成。 对一个UObject创建UserData后,会使用cacheObj缓存起来,这样当下一次把该UObject传递到lua时,就可以通过getFromCache直接获取对应UserData。缓存结构是lua中的一张表,key为UObject指针,value为UserData指针。
创建UserData只完成了一半工作,剩下工作就是处理UE的GC,由objRefs完成。
objRefs
在LuaState类中有objRefs属性,其类型为TMap<UObject*, GenericUserData*>,记录了UObject和在lua中创建的UserData键值对。pushGCObject中的addRef函数,就是在objRefs中添加一条记录,那objRefs又是如何影响UE的GC呢?这和LuaState有关。
LuaState继承自FGCObject,因此UE在GC时会调用到它的AddReferencedObjects方法,代码如下:
  1. void LuaState::AddReferencedObjects(FReferenceCollector & Collector)
  2. {
  3.     for (UObjectRefMap::TIterator it(objRefs); it; ++it)
  4.     {
  5.         UObject* item = it.Key();
  6.         GenericUserData* userData = it.Value();
  7.         if (userData && !(userData->flag & UD_REFERENCE))
  8.         {
  9.             continue;
  10.         }
  11.         Collector.AddReferencedObject(item);
  12.     }
  13.     // do more gc step in collecting thread
  14.     // lua_gc can be call async in bg thread in some isolate position
  15.     // but this position equivalent to main thread
  16.     // we just try and find some proper async position
  17.     if (enableMultiThreadGC && L) lua_gc(L, LUA_GCCOLLECT, 128);
  18. }
复制代码
方法内部会遍历objRefs,并对符合条件的UserData对应的UObject添加引用,这些UObject正在被lua使用。因此,这些UObject在UE中至少有1个引用,从而不会由于没有引用而被删除。
有了添加引用,自然也有删除引用,删除引用代码在unlinkUObject函数,内部首先会把objRefs中的UObject记录移除,然后把UserData的flag设置上UD_HADFREE属性,表示该UserData已经被释放,之后再把lua缓存表中对应记录也删除。
删除引用操作会在几个时机执行:
    lua中对象被gc删除
这个显而易见,当UObject对应的对象在lua中无法被引用到,下次gc时就会被删除。我们在pushGCObject时在元表上设置了__gc方法,对应的C++方法为gcObject,其主要操作就是调用unlinkUObject,删除objRefs中对应的记录。
  1. int LuaObject::gcObject(lua_State* L) {
  2.     CheckUDGC(UObject,L,1);
  3.     removeRef(L,UD);
  4.     return 0;
  5. }
复制代码
  1. void LuaObject::removeRef(lua_State* L,UObject* obj) {
  2.     auto sl = LuaState::get(L);
  3.     sl->unlinkUObject(obj);
  4. }
复制代码
2. UObject被显式删除
UE中,我们可以对actor调用destroy方法显式删除它,也可以把UObject标记为PendingKill进行显式删除,即使当前还有对它们的引用,因此objRefs中的记录的UObject可能在任何时候被UE删除掉。为了应对这一情况,LuaState专门继承了FUObjectDeleteListener,当有UObject被销毁时,LuaState会通过NotifyUObjectDeleted函数收到通知,然后把objRefs中对应的记录删掉。因为unlinkUObject中会对UserData的flag设置UD_HADFREE属性,所以当lua再访问该对象时就知道它已经被删除了,不会再去访问它的地址。
  1. void LuaState::NotifyUObjectDeleted(const UObjectBase * Object, int32 Index)
  2. {
  3.     PROFILER_WATCHER(w1);
  4.     unlinkUObject((const UObject*)Object);
  5. }
复制代码

综上,GC示意图如下:
UStruct从UE传递到lua

UStruct本身不会被GC管理,但UStruct会对内部的ObjectProperty产生引用,因此把UStruct传递到lua时,也需要处理GC。
Push Struct代码如下:
  1. int pushUStructProperty(lua_State* L,UProperty* prop,uint8* parms,bool ref) {
  2.     auto p = Cast<UStructProperty>(prop);
  3.     ensure(p);
  4.     auto uss = p->Struct;
  5.     if (LuaWrapper::pushValue(L, p, uss, parms))
  6.         return 1;
  7.     uint32 size = uss->GetStructureSize() ? uss->GetStructureSize() : 1;
  8.     uint8* buf = (uint8*)FMemory::Malloc(size);
  9.     uss->InitializeStruct(buf);
  10.     uss->CopyScriptStruct(buf, parms);
  11.     return LuaObject::push(L, new LuaStruct(buf,size,uss));
  12. }  
复制代码
可以看到,slua用LuaStruct把ustruct给包装起来,然后把LuaStruct传递到了lua,当然,最终传到Lua的还是UserData。
LuaStruct
  1. struct SLUA_UNREAL_API LuaStruct : public FGCObject {
  2.     uint8* buf;
  3.     uint32 size;
  4.     UScriptStruct* uss;
  5.     LuaStruct(uint8* buf,uint32 size,UScriptStruct* uss);
  6.     ~LuaStruct();
  7.     virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
  8. #if (ENGINE_MINOR_VERSION>=20) && (ENGINE_MAJOR_VERSION>=4)
  9.     virtual FString GetReferencerName() const override
  10.     {
  11.         return "LuaStruct";
  12.     }
  13. #endif
  14. };
复制代码
LuaStruct最大的特定就是也继承FGCObject,并且在AddReferencedObjects函数中对Struct的各个属性添加引用。
先看一下UE是怎么处理UStruct对UObject引用的,它会先扫描UStruct内部各个成员,把成员信息编码为ReferenceToken,GC时不用在管Struct结构,可以根据Token信息直接对UObject添加引用,效率较高。
关于UE的GC,可见之前分析的文章:
slua没有使用UE生成的ReferenceToken信息,而是根据反射自己实现了UStruct引用收集逻辑,主要代码在LuaReference中。由于基于反射,需要运行时再遍历UStruct成员,包括那些不参与GC的成员,因此效率自然比UE的ReferenceToken机制低。
综合来看,UStruct传递到lua后的GC处理和UObject类似,都通过传递FGCobject实现,只是UStruct引用分析过程由slua自己实现。
变量从lua传到UE

同样的,对象从lua传到UE后,相当于UE也对它有引用,lua在gc时应该要统计到这个引用。
slua实现里,lua中变量传递到UE时,会创建一个LuaVar类型变量,通过LuaVar对lua变量产生引用。就以table传递为例子。
当一个table在stack中时,我们可以把table的index传递给LuaVar的构造函数,LuaVar就知道要与哪个变量关联。
  1. LuaVar::LuaVar(lua_State* l,int p):LuaVar() {
  2.     set(l,p);
  3. }
复制代码
set函数中,会根据lua_type设置LuaVar的type,当type为LUA_TABLE时,会执行如下处理:
  1. case LV_FUNCTION:
  2. case LV_TABLE:
  3. case LV_USERDATA:
  4.     alloc(1);
  5.     lua_pushvalue(l,p);
  6.     vars[0].ref = new RefRef(l);
  7.     vars[0].luatype=type;
  8.     break;
复制代码
  1. struct RefRef: public Ref {
  2.     RefRef(lua_State* l);
  3.     virtual ~RefRef();
  4.     bool isValid() {
  5.         return ref != LUA_NOREF;
  6.     }
  7.     void push(lua_State* l) {
  8.         lua_geti(l,LUA_REGISTRYINDEX,ref);
  9.     }
  10.     int ref;
  11.     int stateIndex;
  12. };
复制代码
首先,alloc(1)会新建一个lua_var类型的变量,关键看RefRef属性,会被设置为一个新建的RefRef对象。RefRef对象创建时,会通过一个全局表给lua对象加引用,这样lua就能知道UE正在使用这个对象。在LuaVar析构时同样会调用RefRef对象的析构函数,然后把引用解除。
  1. LuaVar::RefRef::RefRef(lua_State* l)
  2.     :LuaVar::Ref()
  3. {
  4.     ref=luaL_ref(l,LUA_REGISTRYINDEX);
  5.     stateIndex = LuaState::get(l)->stateIndex();
  6. }
  7. LuaVar::RefRef::~RefRef() {
  8.     if(LuaState::isValid(stateIndex)) {
  9.         auto state = LuaState::get(stateIndex);
  10.         luaL_unref(state->getLuaState(),LUA_REGISTRYINDEX,ref);
  11.     }
  12. }
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity游戏引擎开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2021-5-8 21:19 , Processed in 0.139289 second(s), 22 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表