前言
在UWA學堂上線那天,我買了招文勇這篇Lua互動的課程,19塊還算值,但是前段時間太忙,一直沒空研究,他的demo是基於xlua的,今天終於花了大半天時間在tolua下跑起來了,記錄一下我的理解
效能,仍然是Lua中與C#混用的大坑
Lua跟C#互動的效能問題是老生常談的了,c#跟lua資料互動是通過lua虛擬棧,進行壓棧、出棧來傳遞的,一次呼叫就需要執行很多指令,效能會隨著呼叫次數的頻繁,函式引數的增多而變差。直接操作記憶體的方式,可以在c#端修改lua記憶體,省去了操作虛擬棧,函式呼叫的大把指令,效能也就很高效了
騰訊的UnLua(給虛幻4用的)中也有類似的直接操作記憶體的互動方式,看來這種方式會漸漸成為主流,畢竟效能擺在這呢
Lua跟C#高效共享大量資料的一種方法
原理其實很簡單,在c#端定義好lua table的結構體,必須在記憶體中對齊lua端的table,然後在c#端拿到lua table的指標,讀寫這塊記憶體,就能讀寫這個lua table了。
是不是覺得非常簡單,哈哈哈哈。感覺自己馬上就能弄出來了
想要實現這套東西,還得搞懂幾個問題,下面開始一一講解
Lua Table結構體是什麼樣的?
想在c#端寫一個lua table結構體,那就先看看lua端這個結構體是怎麼實現的吧。在tolua下,我們使用的是luajit,jit的原始碼跟lua是不一樣的,luajit又分32位跟64位。所以我們這個table結構體也需要做多套才行
luajit中的GCTab就是Table的結構體了
typedef struct GCtab {
GCHeader;
uint8_t nomm; /* Negative cache for fast metamethods. */
int8_t colo; /* Array colocation. */
MRef array; /* Array part. */
GCRef gclist;
GCRef metatable; /* Must be at same offset in GCudata. */
MRef node; /* Hash part. */
uint32_t asize; /* Size of array part (keys [0, asize-1]). */
uint32_t hmask; /* Hash part mask (size of hash part - 1). */
#if LJ_GC64
MRef freetop; /* Top of free elements. */
#endif
} GCtab;
GCHeader是每一個GC物件都要包含的一個巨集,定義了這些屬性
#define GCHeader GCRef nextgc; uint8_t marked; uint8_t gct
lua的table支援陣列、雜湊表兩種用法,甚至可以同時是陣列又是雜湊表。我們主要處理陣列的資料互動,結構體中的MRef array;就是這個table的所有資料儲存的地方了,而asize就等於這個陣列的長度+1。所以我們重點關注這2個欄位的記憶體地址
如何設計c#端的table結構體呢?
我們把GCTab結構體展開成這樣看
GCRef nextgc;
uint8_t marked; uint8_t gct; uint8_t nomm; int8_t colo;
MRef array;
GCRef gclist;
GCRef metatable;
MRef node;
uint32_t asize;
uint32_t hmask;
MRef freetop;//這個是64位的才會有
GCRef 跟 MRef 都是一個jit中封裝的指標型別,會自動根據巨集展開為32位跟64位。
GCRef 表示這是一個GC物件的指標
MRef 表示非GC物件的記憶體指標在c#中都可以用IntPtr型別代替
uint8_t 是8位元組的,我們把4個8位元組的放在一起,可以用一個int32位佔用
那麼轉換到c#中,結構體就變成了這樣
// GC64 version
public struct LuaJitGCtabGC64
{
IntPtr nextgc;
UInt32 masks;
IntPtr array;
IntPtr gclist;
IntPtr metatable;
IntPtr node;
UInt32 asize;
UInt32 hmask;
IntPtr freetop; // only valid for LJ_GC64
}
指標array指向的資料是什麼?
在lj_tab.c中看tab的實現,我們很快就能找到array裡存的是TValue結構,TValue其實是一個聯合體。
聯合體是多個結構體可以共享同一塊記憶體,訪問的時候可以用不同的結構體方式去訪問。具體什麼是聯合體可以自行百度哦
TValue原始碼
/* Tagged value. */
typedef LJ_ALIGN(8) union TValue {
uint64_t u64; /* 64 bit pattern overlaps number. */
lua_Number n; /* Number object overlaps split tag/value object. */
#if LJ_GC64
GCRef gcr; /* GCobj reference with tag. */
int64_t it64;
struct {
LJ_ENDIAN_LOHI(
int32_t i; /* Integer value. */
, uint32_t it; /* Internal object tag. Must overlap MSW of number. */
)
};
#else
struct {
LJ_ENDIAN_LOHI(
union {
GCRef gcr; /* GCobj reference (if any). */
int32_t i; /* Integer value. */
};
, uint32_t it; /* Internal object tag. Must overlap MSW of number. */
)
};
#endif
#if LJ_FR2
int64_t ftsz; /* Frame type and size of previous frame, or PC. */
#else
struct {
LJ_ENDIAN_LOHI(
GCRef func; /* Function for next frame (or dummy L). */
, FrameLink tp; /* Link to previous frame. */
)
} fr;
#endif
struct {
LJ_ENDIAN_LOHI(
uint32_t lo; /* Lower 32 bits of number. */
, uint32_t hi; /* Upper 32 bits of number. */
)
} u32;
} TValue;
這中間有很多巨集,看著很亂,但其實我們只需要用2種模式就行了,因為我們只實現int跟double。作者給出的方式是如下這種
[StructLayout(LayoutKind.Explicit, Size = 8)]
public struct LuaJitTValue
{
// uint64
[FieldOffset(0)]
public UInt64 u64;
// number
[FieldOffset(0)]
public double n;
// integer value
[FieldOffset(0)]
public int i;
// internal object tag for GC64
[FieldOffset(0)]
public Int64 it64;
// internal object tag
[FieldOffset(4)]
public UInt32 it;
}
但這裡我有一些我還沒弄明白,因為我實際執行起來後,不管lua賦值的是整形,還是浮點,int i始終沒有值,值都存在了double n中。那為啥作者要弄一個int i跟UInt32 it; 這個it還偏移了4位元組
在c#端我們可以使用[StructLayout(LayoutKind.Explicit)]和[FieldOffset(0)]來實現c語言中的聯合體,具體方式可以看這篇文章
https://blog.csdn.net/wonengxing/article/details/44302661
如何用unsafe模式讀寫結構體?
結構體都定義好了,接下來我們看看怎麼讀寫一個double
LuaJitGCtab32* TableRawPtr; //需要拿到Lua端Table的指標
//賦值操作
TableRawPtr->array[index].n = val;
//取值操作
TableRawPtr->array[index].n;
沒錯,就是這麼簡單。直接就可以操作lua記憶體了。
如何拿到lua端table的指標?
在lua端傳入一個table引數過來,我們可以在c#端操作虛擬棧轉成指標
System.IntPtr arg0 = LuaDLL.lua_topointer(L, 1);
看到這裡,相信大部分的謎團都已經解開了,真的自己可以實現一套出來了。
總結
作者提供的方案裡,只支援int、double。只支援array型別的table。還有luajit64位貌似沒支援好。所以如果真正要使用的話,還要改很多東西