fwalker 发表于 2021-4-14 14:51

slua unreal分析( 三)slua与GC

相关文章:
本章介绍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结构体。
入口代码如下:
int LuaObject::push(lua_State* L, UObject* obj, bool rawpush, bool ref) {
    if (!obj) return pushNil(L);
    if (!rawpush) {
      if (auto it = Cast<ILuaTableObjectInterface>(obj)) {
            return ILuaTableObjectInterface::push(L, it);
      }
    }
    if (auto e = Cast<UEnum>(obj)
      return pushEnum(L, e);
    else if (auto c = Cast<UClass>(obj))
      return pushClass(L, c);
    else if (auto s = Cast<UScriptStruct>(obj))
      return pushStruct(L, s);
    else
      return pushGCObject<UObject*>(L,obj,"UObject",setupInstanceMT,gcObject,ref);
}我们先看rawpush的情况,rawpush会直接调用pushGCObject函数,函数内容如下:
template<typename T>
static int pushGCObject(lua_State* L,T obj,const char* tn,lua_CFunction setupmt,lua_CFunction gc,bool ref) {
    if(getFromCache(L,obj,tn)) return 1;
    lua_pushcclosure(L,gc,0);
    int f = lua_gettop(L);
    int r = pushType<T>(L,obj,tn,setupmt,f);
    lua_remove(L,f); // remove wraped gc function
    if (r) {
      addRef(L, obj, lua_touserdata(L, -1), ref);
      cacheObj(L, obj);
    }
    return r;
}把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方法,代码如下:
void LuaState::AddReferencedObjects(FReferenceCollector & Collector)
{
    for (UObjectRefMap::TIterator it(objRefs); it; ++it)
    {
      UObject* item = it.Key();
      GenericUserData* userData = it.Value();
      if (userData && !(userData->flag & UD_REFERENCE))
      {
            continue;
      }
      Collector.AddReferencedObject(item);
    }
    // do more gc step in collecting thread
    // lua_gc can be call async in bg thread in some isolate position
    // but this position equivalent to main thread
    // we just try and find some proper async position
    if (enableMultiThreadGC && L) lua_gc(L, LUA_GCCOLLECT, 128);
}方法内部会遍历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中对应的记录。
int LuaObject::gcObject(lua_State* L) {
    CheckUDGC(UObject,L,1);
    removeRef(L,UD);
    return 0;
}

void LuaObject::removeRef(lua_State* L,UObject* obj) {
    auto sl = LuaState::get(L);
    sl->unlinkUObject(obj);
}2. UObject被显式删除
UE中,我们可以对actor调用destroy方法显式删除它,也可以把UObject标记为PendingKill进行显式删除,即使当前还有对它们的引用,因此objRefs中的记录的UObject可能在任何时候被UE删除掉。为了应对这一情况,LuaState专门继承了FUObjectDeleteListener,当有UObject被销毁时,LuaState会通过NotifyUObjectDeleted函数收到通知,然后把objRefs中对应的记录删掉。因为unlinkUObject中会对UserData的flag设置UD_HADFREE属性,所以当lua再访问该对象时就知道它已经被删除了,不会再去访问它的地址。
void LuaState::NotifyUObjectDeleted(const UObjectBase * Object, int32 Index)
{
    PROFILER_WATCHER(w1);
    unlinkUObject((const UObject*)Object);
}

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

UStruct本身不会被GC管理,但UStruct会对内部的ObjectProperty产生引用,因此把UStruct传递到lua时,也需要处理GC。
Push Struct代码如下:
int pushUStructProperty(lua_State* L,UProperty* prop,uint8* parms,bool ref) {
    auto p = Cast<UStructProperty>(prop);
    ensure(p);
    auto uss = p->Struct;

    if (LuaWrapper::pushValue(L, p, uss, parms))
      return 1;

    uint32 size = uss->GetStructureSize() ? uss->GetStructureSize() : 1;
    uint8* buf = (uint8*)FMemory::Malloc(size);
    uss->InitializeStruct(buf);
    uss->CopyScriptStruct(buf, parms);
    return LuaObject::push(L, new LuaStruct(buf,size,uss));
}可以看到,slua用LuaStruct把ustruct给包装起来,然后把LuaStruct传递到了lua,当然,最终传到Lua的还是UserData。
LuaStruct
struct SLUA_UNREAL_API LuaStruct : public FGCObject {
    uint8* buf;
    uint32 size;
    UScriptStruct* uss;

    LuaStruct(uint8* buf,uint32 size,UScriptStruct* uss);
    ~LuaStruct();

    virtual void AddReferencedObjects(FReferenceCollector& Collector) override;

#if (ENGINE_MINOR_VERSION>=20) && (ENGINE_MAJOR_VERSION>=4)
    virtual FString GetReferencerName() const override
    {
      return "LuaStruct";
    }
#endif
};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就知道要与哪个变量关联。
LuaVar::LuaVar(lua_State* l,int p):LuaVar() {
    set(l,p);
}set函数中,会根据lua_type设置LuaVar的type,当type为LUA_TABLE时,会执行如下处理:
case LV_FUNCTION:
case LV_TABLE:
case LV_USERDATA:
    alloc(1);
    lua_pushvalue(l,p);
    vars.ref = new RefRef(l);
    vars.luatype=type;
    break;

struct RefRef: public Ref {
    RefRef(lua_State* l);
    virtual ~RefRef();
    bool isValid() {
      return ref != LUA_NOREF;
    }
    void push(lua_State* l) {
      lua_geti(l,LUA_REGISTRYINDEX,ref);
    }
    int ref;
    int stateIndex;
};首先,alloc(1)会新建一个lua_var类型的变量,关键看RefRef属性,会被设置为一个新建的RefRef对象。RefRef对象创建时,会通过一个全局表给lua对象加引用,这样lua就能知道UE正在使用这个对象。在LuaVar析构时同样会调用RefRef对象的析构函数,然后把引用解除。
LuaVar::RefRef::RefRef(lua_State* l)
    :LuaVar::Ref()
{
    ref=luaL_ref(l,LUA_REGISTRYINDEX);
    stateIndex = LuaState::get(l)->stateIndex();
}

LuaVar::RefRef::~RefRef() {
    if(LuaState::isValid(stateIndex)) {
      auto state = LuaState::get(stateIndex);
      luaL_unref(state->getLuaState(),LUA_REGISTRYINDEX,ref);
    }
}
页: [1]
查看完整版本: slua unreal分析( 三)slua与GC