LuaJIT源码分析(二)数据类型

LuaJIT源码分析(二)数据类型

LuaJIT支持的lua数据类型和官方的lua 5.1版本保持一致,它的源文件中也有一个lua.h:

c 复制代码
// lua.h
/*
** basic types
*/
#define LUA_TNONE		(-1)

#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

不过LuaJIT用来表示这些类型的通用数据结构TValue定义就和官方lua不太一样了,它的定义要复杂一些:

c 复制代码
// lj_obj.h
/* 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;

首先可以看到有struct的定义有若干宏在里面,这就无形中增加了阅读的难度。我们先把这些宏给处理掉。

首先是打头的LJ_ALIGN宏,这个盲猜都能猜到,是用来控制struct内存8字节对齐用的,它在MSVC环境的定义如下:

c 复制代码
// lj_def.h
#define LJ_ALIGN(n)	__declspec(align(n))

LJ_GC64宏会在LJ_TARGET_GC64宏生效时生效,而LJ_TARGET_GC64宏会在LuaJIT判断当前平台为64位平台时,而且没有强行禁用时生效。

c 复制代码
// lj_arch.h
#if LUAJIT_TARGET == LUAJIT_ARCH_X86
...
#elif LUAJIT_TARGET == LUAJIT_ARCH_X64
#ifndef LUAJIT_DISABLE_GC64
#define LJ_TARGET_GC64		1
#endif
#elif LUAJIT_TARGET == LUAJIT_ARCH_ARM
...
#endif

/* 64 bit GC references. */
#if LJ_TARGET_GC64
#define LJ_GC64			1
#else
#define LJ_GC64			0
#endif

默认编译时是不会强行禁用GC64的,那么这里就可以认为LJ_GC64宏定义为1。

LJ_ENDIAN_LOHI是一个跟大小端相关的宏,而x64都是小端序:

c 复制代码
// lj_arch.h
#if LUAJIT_TARGET == LUAJIT_ARCH_X86
...
#elif LUAJIT_TARGET == LUAJIT_ARCH_X64
#define LJ_ARCH_ENDIAN		LUAJIT_LE
#elif LUAJIT_TARGET == LUAJIT_ARCH_ARM
...
#endif

#if LJ_ARCH_ENDIAN == LUAJIT_BE
#define LJ_ENDIAN_LOHI(lo, hi)		hi lo
#else
#define LJ_ENDIAN_LOHI(lo, hi)		lo hi
#endif

LJ_FR2宏会在LJ_GC64宏生效时生效:

c 复制代码
// lj_arch.h
/* 2-slot frame info. */
#if LJ_GC64
#define LJ_FR2			1
#else
#define LJ_FR2			0
#endif

那么根据当前的这些宏定义,我们整理一下TValue的定义:

c 复制代码
// lj_obj.h
/* Tagged value. */
typedef __declspec(align(8)) union TValue {
  uint64_t u64;		/* 64 bit pattern overlaps number. */
  lua_Number n;		/* Number object overlaps split tag/value object. */
  GCRef gcr;		/* GCobj reference with tag. */
  int64_t it64;
  struct {
    int32_t i;	/* Integer value. */
    uint32_t it;	/* Internal object tag. Must overlap MSW of number. */
  };
  int64_t ftsz;		/* Frame type and size of previous frame, or PC. */
  struct {
    uint32_t lo;	/* Lower 32 bits of number. */
    uint32_t hi;	/* Upper 32 bits of number. */
  } u32;
} TValue;

那么,不同类型的lua数据是怎么统一都装进这个数据结构里的呢?首先,我们注意到它是一个union,而且实际大小居然只有仅仅64位。luajit为了节省空间,使用了一种名为NaN Boxing的技术。我们在luajit的源码注释中可以看到解释:

c 复制代码
// lj_obj.h
/*
** Format for 64 bit GC references (LJ_GC64):
**
** The upper 13 bits must be 1 (0xfff8...) for a special NaN. The next
** 4 bits hold the internal tag. The lowest 47 bits either hold a pointer,
** a zero-extended 32 bit integer or all bits set to 1 for primitive types.
**
**                     ------MSW------.------LSW------
** primitive types    |1..1|itype|1..................1|
** GC objects         |1..1|itype|-------GCRef--------|
** lightuserdata      |1..1|itype|seg|------ofs-------|
** int (LJ_DUALNUM)   |1..1|itype|0..0|-----int-------|
** number              ------------double-------------
*/

IEEE754规定,64位的浮点数编码分为3个部分,1个符号位,11个指数位,以及52个尾数位。

不过,浮点数中有些特殊的值,比如NaN。IEEE754规定,如果一个浮点数,指数位全为1,且尾数部分不全为0(与无穷大区分),那么它就是NaN。换句话说,只要满足这个条件,剩下的51位尾数,完全可以用来编码其他的数据。这就是NaN boxing。

有个这个先验知识,我们就能明白luajit的注释了。对于普通的number,就用一个double来表示,此时64位编码就是浮点数的编码;对于其他类型,64位编码中的前13位,设定为1用来表示NaN,接下来的4位叫做itype,用来表示TValue的具体类型,不同itype的定义如下:

c 复制代码
// lj_obj.h
/*
** ORDER LJ_T
** Primitive types nil/false/true must be first, lightuserdata next.
** GC objects are at the end, table/userdata must be lowest.
** Also check lj_ir.h for similar ordering constraints.
*/
#define LJ_TNIL			(~0u)
#define LJ_TFALSE		(~1u)
#define LJ_TTRUE		(~2u)
#define LJ_TLIGHTUD		(~3u)
#define LJ_TSTR			(~4u)
#define LJ_TUPVAL		(~5u)
#define LJ_TTHREAD		(~6u)
#define LJ_TPROTO		(~7u)
#define LJ_TFUNC		(~8u)
#define LJ_TTRACE		(~9u)
#define LJ_TCDATA		(~10u)
#define LJ_TTAB			(~11u)
#define LJ_TUDATA		(~12u)
/* This is just the canonical number type used in some places. */
#define LJ_TNUMX		(~13u)

可以看到luajit内部用到的数据类型还要更多一些。这里巧妙的是,luajit直接取反定义了这些type,这样它们的二进制表示都是1打头的,从而可以非常快速地通过移位,即可得到一个TValue的itype:

c 复制代码
// lj_obj.h
#define itype(o)	((uint32_t)((o)->it64 >> 47))

接下来我们来看下不同的数据类型,luajit是如何在这64位中存储的。首先是number,luajit定义了numV这个宏来取出number的值,以及setnumV这个宏来设置number的值:

c 复制代码
// lj_obj.h
#define tvisnum(o)	(itype(o) < LJ_TISNUM)
#define numV(o)		check_exp(tvisnum(o), (o)->n)
#define setnumV(o, x)		((o)->n = (x))

check_exp是luajit的一种assert的宏,当assert通过时才会执行后续的表达式,numV就是先判断TValue的类型是否为number,如果是则按union的n字段取出值。这个n字段是lua_number类型的,其实就是个double。

c 复制代码
// luaconf.h
#define LUA_NUMBER		double

tvisnum的实现也比较巧妙,它不是直接去判断TValue是否为一个非NaN的double,而是尝试取出它的itype,如果itype比最后一个定义的LJ_TISNUM都还要小(注意itype的定义都是取反过的),那么说明它必定不是一个合法定义的TValue类型,也就只能是个double了。

再看primitive types,也就是lua里的nil,true,false,它们只有itype这4位是有效信息,后面47位的payload均为1。

c 复制代码
// lj_obj.h
#define tvisnil(o)	((o)->it64 == -1)
#define tvisfalse(o)	(itype(o) == LJ_TFALSE)
#define tvistrue(o)	(itype(o) == LJ_TTRUE)
#define tvisbool(o)	(tvisfalse(o) || tvistrue(o))
#define boolV(o)	check_exp(tvisbool(o), (LJ_TFALSE - itype(o)))
#define setnilV(o)		((o)->it64 = -1)
#define setboolV(o, x)		((o)->it64 = (int64_t)~((uint64_t)((x)+1)<<47))

由宏定义可看出,nil的值就是-1,而-1的二进制表示即为64个1,中间4位itype就是1111,也就是~0u。类似也可以根据false和true的值算出它们的itype,分别为~1u~2u

接下来就是GC objects了。所谓GC objects,就是指lua中可被自动gc回收的对象,例如string,table类型。对于luajit,除了nil,bool,以及light userdata类型之外,其他的类型均属于GC objects。nil和bool类型是值类型,无需gc管理,而light userdata的定义就是外部管理的对象,只是将指针传给了lua,所以也不受lua的gc管理。那么这里就能看出luajit定义LJ_T顺序的讲究了,不属于GC objects的类型都定义在前面,luajit也提供了一个宏来判断一个TValue是否为GC objects:

c 复制代码
// lj_obj.h
#define LJ_TISGCV		(LJ_TSTR+1)
#define tvisgcv(o)	((itype(o) - LJ_TISGCV) > (LJ_TNUMX - LJ_TISGCV))

这个宏设计的也很巧妙。如果是不属于GC objects的类型,例如nil和bool类型的itype,对应64位无符号整数的值,都比LJ_TISGCV要大,相减得到的值最大也就是3。而LJ_TNUMX的值要比LJ_TISGCV小,相减得到的值是负数,转换成无符号整数又会变成一个很大的值。而如果itype是属于GC objects的类型,itype对应的64位无符号整数的值,都要大于LJ_TNUMX,且小于LJ_TISGCV。最后,如果TValue是一个普通的double,那么取它的itype得到的值,一定要比LJ_TNUMX要小。luajit通过这样一个简单的宏,就能把这几种数据类型给区分开,实在是令人惊讶。

在开启LJ_GC64的情况下,从TValue中取出GC Object的宏定义如下:

c 复制代码
// lj_obj.h
#define LJ_GCVMASK		(((uint64_t)1 << 47) - 1)
#define gcrefu(r)	((r).gcptr64)
#define gcval(o)	((GCobj *)(gcrefu((o)->gcr) & LJ_GCVMASK))

可以看到,这次用到的是TValue的gcr字段,这个字段用来保存真正GC Object的地址。在LJ_GC64下,它是个64位的地址:

c 复制代码
// lj_obj.h
/* GCobj reference */
typedef struct GCRef {
#if LJ_GC64
  uint64_t gcptr64;	/* True 64 bit pointer. */
#else
  uint32_t gcptr32;	/* Pseudo 32 bit pointer. */
#endif
} GCRef;

不过,由于TValue前13位需要设置为全1,中间4位用来表示数据类型,实际上luajit只使用低47位的地址空间,也就是128TB,这在当今的现实世界中也绰绰有余了。

再来看看GCobj这个数据结构,它也是一个union:

c 复制代码
// lj_obj.h
typedef union GCobj {
  GChead gch;
  GCstr str;
  GCupval uv;
  lua_State th;
  GCproto pt;
  GCfunc fn;
  GCcdata cd;
  GCtab tab;
  GCudata ud;
} GCobj;

GChead是一个通用的数据结构,用来在不知道GCobj具体类型时,获取它的通用信息。

c 复制代码
// lj_obj.h
#define GCHeader	GCRef nextgc; uint8_t marked; uint8_t gct
typedef struct GChead {
  GCHeader;
  uint8_t unused1;
  uint8_t unused2;
  GCRef env;
  GCRef gclist;
  GCRef metatable;
} GChead;

nextgc和marked字段是用于gc管理的,gct则是用来表示GCObj类型的字段。GCHeader宏所定义的三个字段,是所有类型的GCObj所共有的。luajit会根据gct字段的值,将一个GCObj转换为实际的类型对象。

c 复制代码
// lj_obj.h
/* Macros to convert a GCobj pointer into a specific value. */
#define gco2str(o)	check_exp((o)->gch.gct == ~LJ_TSTR, &(o)->str)
#define gco2uv(o)	check_exp((o)->gch.gct == ~LJ_TUPVAL, &(o)->uv)
#define gco2th(o)	check_exp((o)->gch.gct == ~LJ_TTHREAD, &(o)->th)
#define gco2pt(o)	check_exp((o)->gch.gct == ~LJ_TPROTO, &(o)->pt)
#define gco2func(o)	check_exp((o)->gch.gct == ~LJ_TFUNC, &(o)->fn)
#define gco2cd(o)	check_exp((o)->gch.gct == ~LJ_TCDATA, &(o)->cd)
#define gco2tab(o)	check_exp((o)->gch.gct == ~LJ_TTAB, &(o)->tab)
#define gco2ud(o)	check_exp((o)->gch.gct == ~LJ_TUDATA, &(o)->ud)

最后,我们来看下int类型。默认情况下,luajit是不开启int的,所有的数值都以double类型存储。但是在实际使用中,整数是会经常被用到的,为此luajit提供了LJ_DUALNUM的选项,一些数值可以直接通过int类型存储,方便使用。此时TValue的i字段用来保存int的值。先前提到大小端的struct在这里就发挥作用了,它保证写入int的i字段一定是TValue的后32位,同时int类型的itype则需要写入代表TValue前32位的it字段,也就是需要向左移位47 - 32 =15位。

c 复制代码
// lj_obj.h
#define tvisint(o)	(LJ_DUALNUM && itype(o) == LJ_TISNUM)
#define intV(o)		check_exp(tvisint(o), (int32_t)(o)->i)

#define setitype(o, i)		((o)->it = ((i) << 15))

static LJ_AINLINE void setintV(TValue *o, int32_t i)
{
#if LJ_DUALNUM
  o->i = (uint32_t)i; setitype(o, LJ_TISNUM);
#else
  o->n = (lua_Number)i;
#endif
}

Reference

[1] LuaJIT的变量实现-TValue

[2] LuaJIT Internals: Intro

[3] LuaJIT GC64 模式

[4] NaN boxing or how to make the world dynamic

相关推荐
unix2linux17 小时前
YOLO v5 Series - Image & Video Storage ( Openresty + Lua)
yolo·lua·openresty
最后一个bug18 小时前
如何理解Lua 使用虚拟堆栈
linux·c语言·开发语言·嵌入式硬件·lua
半夏知半秋2 天前
lua-lru缓存算法解析
笔记·学习·算法·缓存·lua
最后一个bug3 天前
C语言执行Lua进行错误处理
linux·服务器·c语言·开发语言·嵌入式硬件·lua
袁庭新3 天前
Lua如何连接MySQL数据库?
redis·mysql·lua·lua5.4·袁庭新
红黑色的圣西罗3 天前
Lua 实现继承的一种方式
开发语言·lua
weixin_428498494 天前
Lua资料
开发语言·lua
Clown954 天前
go-zero(四) 错误处理(统一响应信息)
开发语言·golang·lua
半夏知半秋5 天前
lua实现雪花算法
开发语言·笔记·算法·lua
最后一个bug5 天前
lua调用C语言函数,在函数中进行类型检查
linux·c语言·开发语言·lua