前言
在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位貌似沒支持好。所以如果真正要使用的話,還要改很多東西
