Lua数据类型

5k 词

前言

接触lua近4年了,一直断断续续读相关源码,这次计划把lua源码整体分析一遍,基于最新版本(Lua 5.3.5), 完成如下文章(顺序可能不一致):

  • 基本数据类型
  • string/table 剖析
  • luavm 分析
  • lexer/ast/gencode 词法分析/语法树/生成字节码
  • lua周边支持:debug/continue等

lua源码简介

  • 可以从lua官方网站下载最新源码:源码下载
  • 推荐直接用 vscode+wsl 阅读代码,方案参考,然后可以配置Task/Debug, 很方便地即可实现断点,code定位等常用功能, 注意修改一下gcc编译优化等级即可

Lua数据类型概括

lua数据类型定义在 lobject.h 中, 暴露出来的类型一共有9中:nil/bool/lightud/number/string/table/func/ud/thread, 定义:

#define LUA_TNIL		0
#define LUA_TBOOLEAN		1
#define LUA_TLIGHTUSERDATA	2
#define LUA_TNUMBER		3
#define LUA_TSTRING		4
#define LUA_TTABLE		5
#define LUA_TFUNCTION		6
#define LUA_TUSERDATA		7
#define LUA_TTHREAD		8

如此看,似乎4个bit就能完全满足类型定义了,但是lua在一些数据类型实现上还有进一步细分,如

  • LUA_TNUMBER 细分:LUA_TNUMFLT/LUA_TNUMINT (float64/int64)
  • LUA_TSTRING 细分:LUA_TSHRSTR/LUA_TLNGSTR
  • LUA_TFUNCTION 细分:LUA_TLCL/LUA_TLCF/LUA_TCCL

lua从设计上只想暴露出9中数据类型,但是实现上必须更加精巧地去考虑,那么如何用一个字节去定义9种基础类型以及部分细分类型:

  • 0 - 3bit:定义9种基础数据类型,一共可以定义15种目前用了9种
  • 4 - 6bit: 定义细分类型
#define LUA_TLCL	(LUA_TFUNCTION | (0 << 4))  /* Lua closure */
#define LUA_TLCF	(LUA_TFUNCTION | (1 << 4))  /* light C function */
#define LUA_TCCL	(LUA_TFUNCTION | (2 << 4))  /* C closure */

/* Variant tags for strings /
#define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /
short strings /
#define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /
long strings */

/* Variant tags for numbers /
#define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /
float numbers /
#define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /
integer numbers */

那么为什么不能这么定义呢

#define LUA_TNUMFLT		9
#define LUA_TNUMINT     10
....

如果真的这样定义也不是一定就不行,但是会有一个小问题,当我们判断某个数据类型是不是 LUA_TNUMBER 时就变得有点麻烦,可能就需要if type == LUA_TNUMFLT or type == LUA_TNUMINT {do something}, 但是用bit去定义就可以 if type & LUA_TNUMBER { do something}

Lua数据类型结构体定义

  • lua 是一种弱类型脚本语言,所以在实现时期望用一个统一的结构对象去实现所有类型,类比到高级语言种如:C# 可以用object对象去实现,golang可以用interface{},java可以用Object等等,不考虑值类型装箱问题的话,的确可以这么做的。 但是放到c语言中,假设我们尝试用void*去处理,一则值类型也必须用指针处理,再者此时就失去原类型的定义,如任意 void* ptr并无法知道ptr具体是什么类型

  • 可以用 struct 去实现吗? 自然也不太好,struct会带来内存浪费问题,比如会如下定义 :
    typedef struct LuaType {
      int64 num;
      char* str;
      Table tbl;
      char type;
      ... 
      }  
    

    那么对于任意 LuaType 对象来说,内存都是 sizeof(LuaType) = sizeof(int64)+sizeof(char*)…., 然后需要的仅仅是某个类型+type的内存

  • Lua用了c中union去实现这种“内存共享”逻辑 union 参考

Lua 数据类型大体上分为:可被GC的对象 / 值类型TValue 是lua中所有类型的表示, 定义:

typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Inteluager i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;

#define TValuefields Value value_; int tt_

typedef struct lua_TValue {
TValuefields;
} TValue;

其中 Value value_ 定义了数据部分,tt_ 定义了数据类型:

  • lua_Number/lua_Inteluager 分别定义为:double/long long,所以lua中number就是8字节,浮点的数的话用double,那么它的精度就是52位 (最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M), 所以如果是一个超出52位整型转float64时可能会丢失精度

  • 所以一个lua 数据对象,在内存中占用最少 12byte = sizeof(long long) + sizeof(int)

  • tt_ 定义了具体数据类型,其中如果是 GCObject* 则tt_第7位会标记出来 #define BIT_ISCOLLECTABLE (1 << 6);#define ctb(t) ((t) | BIT_ISCOLLECTABLE)

GCObject 是所有可GC对象的定义

  • next 字段使得GCObject可以变成一个单项链表,用于GC过程中遍历以及“根对象”管理
  • tt 表示该对象类型类型
  • mark 字段用于GC过程中的 扫描标记中 white/gray/black 标记
/* Common Header for all collectable objects (in macro form, to be included in other objects) */
#define CommonHeader	GCObject *next; lu_byte tt; lu_byte marked

/* Common type has only the common header */
struct GCObject {
CommonHeader;
};

Lua 数据结构字段get/set 封装

为了更加方便地操作TValue字段,Lua定义了各种宏去判断类型/字段访问等,之所以用宏,主要考虑避免method频繁调用带来的上下文切换开销:

#define val_(o)		((o)->value_)

/* raw type tag of a TValue */
#define rttype(o) ((o)->tt_)

/* tag with no variants (bits 0-3) */
#define novariant(x) ((x) & 0x0F)

/* type tag of a TValue (bits 0-3 for tags + variant bits 4-5) */
#define ttype(o) (rttype(o) & 0x3F)

/* type tag of a TValue with no variants (bits 0-3) */
#define ttnov(o) (novariant(rttype(o)))

值得注意的是 如果是9种基础类型,那么用:novariant(x) 去处理即可, 细分类型用 ttype(o), 正如Lua源码更高级地接口封装:

/* Macros to test type */
#define checktag(o,t)		(rttype(o) == (t))
#define checktype(o,t)		(ttnov(o) == (t))
#define ttisnumber(o)		checktype((o), LUA_TNUMBER)
#define ttisfloat(o)		checktag((o), LUA_TNUMFLT)
#define ttisinteger(o)		checktag((o), LUA_TNUMINT)
...

对于非GC类型的对象,get/set 不用转型,直接访问即可,如:

#define settt_(o,t)	((o)->tt_=(t))

#define setfltvalue(obj,x)
{ TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_TNUMFLT); }

#define chgfltvalue(obj,x)
{ TValue *io=(obj); lua_assert(ttisfloat(io)); val_(io).n=(x); }

#define setivalue(obj,x)
{ TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_TNUMINT); }

但是GC类型需要特殊处理一下, 因为他们本身是一种”继承“的复合类型,如string的定义:

  /*
** Header for string value; string bytes follow the end of this structure
** (aligned according to 'UTString'; see next).
*/
typedef struct TString {
  CommonHeader;
  lu_byte extra;  /* reserved words for short strings; "has hash" for longs */
  lu_byte shrlen;  /* length for short strings */
  unsigned int hash;
  union {
    size_t lnglen;  /* length for long strings */
    struct TString *hnext;  /* linked list for hash table */
  } u;
} TString;

/*
** Ensures that address after this type is always fully aligned.
/
typedef union UTString {
L_Umaxalign dummy; /
ensures maximum alignment for strings */
TString tsv;
} UTString;

复杂的GCObject类型数据访问方式需要特使处理,TString 内存结构: TString+rawData, 其中rawData就是真正存数据的内存,所以分配过程:

static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) {
  TString *ts;
  GCObject *o;
  size_t totalsize;  /* total size of TString object */
  totalsize = sizelstring(l);
  o = luaC_newobj(L, tag, totalsize);
  ts = gco2ts(o);
  ts->hash = h;
  ts->extra = 0;
  getstr(ts)[l] = '