前言

Lua5.4源代码剖析 - 知乎

https://blog.csdn.net/initphp/article/details/82703846

https://gitee.com/SimanX/lua-Chinese

数据类型有这些:

https://www.zhihu.com/column/c_1595011738562494465

nil、boolean、number、string、userdata、function、thread 和 table

动态弱类型语言

在开始之前,我们先插入讲解一下什么是动态弱类型语言,先说一下这类语言的优缺点:

优点:对使用者来说方便阅读,不需要写许多类型相关的代码,变量不需要提前声明类型;

缺点:不方便调试,安全性比静态语言差。

TValue

TValue是所有数据结构的基础,这边可以看到Value的结构

img

TValue中引用了TValuefields,它是一个宏定义,我们把宏展开可以把TValue看作如下代码:

1
2
3
4
5
6
7
8
9
10
typedef struct TValue {
union Value {
struct GCObject gc; / collectable objects */
void p; / light userdata /
lua_CFunction f; / light C functions /
lua_Integer i; / integer numbers /
lua_Number n; / float numbers */
} value_;
lu_byte tt_;
} TValue;

其中lu_byte类型的tt_即为我们上述所说的”type“字段,Value类型的**value_**则分别用于表示相应的数据。

lu_byte的定义《limits.h》unsigned char,英文注释中说用char是因为各个平台中存储一个char一般只需要最小的存储单位,而lua中变量的类型不多,不需要太多内存用几个位就能区分完,所以使用char做类型的存储。我们这里不需要把char理解为字符类型,可以简单把他当作1个字节即8位的内存片段就好了,见源码《llimits.h》类型定义:

我们再看声明为UnionValue字段,我们知道Union内部声明的变量是互斥只能使用其中一个的,Union为内存中的一个片段,其真实运行时的类型为Union内定义的其中一个,而Value value_占用的内存则由Union中声明的类型中占用内存最大的那个来决定。

union——联合体

为了让更多同学看明白清楚,这里插入一个简单例子讲解一下Union的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
union intOrBoolean{
bool bValue;
int iValue1;
int iValue2;
}t;

我们可以这样用:
t.bValue = true;
-- do something with bool value t.bValue
或者
t.iValue1 = 100;
-- do something with int value t.iValue1
或者
t.iValue2 = 200;
-- do something with int value t.iValue2
但我们无法同时使用Union中的多个值:
t.iValue1 = 100;
t.iValue2 = 200;

若同时对iValue1和iValue2赋值,

在内存中,后赋值的变量会覆盖掉前赋值的变量所在的内存区块;

在iValue2赋值完成后bValue和iValue1都变为无意义状态。

所以更不可能有 t.iValue1 + t.iValue2 这样的操作出现。

在内存占用方面:

bool bValue 布尔占用1字节;

int iValue1,iValue2 整型占用4字节;

所以取他们中的最大值4字节。

所以这个intOrBoolean在内存中占用4字节,就算只用bValue也是占用4字节。

我们回来再看一下Value这个结构体,其中声明的gc, p, f字段都可以简单理为一个指针,指针在32位平台占用4个字节,在64位平台占用8个字节。然后i字段为整型,n字段为浮点型,具体占用多少空间则根据编译时的自己的使用的宏进行选择决定。也可以先简单理解为4或8个字节。

所以TValue这个结构体占用的内存空间,在不考虑内存对齐的情况下,就是tt字段sizeof(unsigned char) 1个字节 + value字段4或8个字节,即为5或9个字节。

讲完union,我们接着继续学习bool布尔类型。

lu_byte tt_

我们可以看到Value结构体中并没有类似整型浮点那样像lua_Boolean b这样声明方式,那么布尔类型的实际值难道不是存放在value_这个Union字段中吗?事实上,确实也不是,为了减少判断和内存的寻址,在Lua中,布尔的值直接存放在类型字段tt_中了。我们接下来详细讲解一下lu_byte tt_字段, tt在源码中英文为type tag,即类型标记:

**lu_byte tt_**字段占用一个字节(8位),lua中充分利用了每一个位,各个位都有其不同的含义:

bit 0 ~ bit 3: 这里4个位,2的4次方即可以代表16个数值,用于存储变量的基本类型,变量的基本类型即为本文开始时图1中类型枚举中0到8分别代表的nil到thread。

bit 4 ~ bit 5: 这里2个位,2的2次方即可以代表4个数值,用于存放类型变体,类型变体也属于它0到3位所对应的的基本类型。

bit 6: 这里1个位,2的1次方即可以代表2个数值,用于存储该变量是否可以垃圾回收,后面学习垃圾回收算法的时候再详细学习。

8个位的tt_字段中,lua用了其中0到6共7个位,真是利用得够充分的。

Boolean——布尔类型

我们由浅入深,先说最简单的类型:布尔类型。

如果我们有其它的编程语言基础,我们知道布尔类型就是true或者false,分别代表真和假,一般在内存中占用最小单位1个字节进行表示。

tt_

布尔类型,刚刚提到它的值是存在tt_这个类型字段中,实际中它就是通过声明类型变体的方式进行实现的。

源码(lobject.h)中会有下面定义:

1
2
3
#define makevariant(t,v)  ((t) | ((v) << 4))
#define LUA_VFALSE makevariant(LUA_TBOOLEAN, 0)
#define LUA_VTRUE makevariant(LUA_TBOOLEAN, 1)

我们可以看得出来这里为布尔类型声明了两个类型变体,用第5位0代表false,1代表true。布尔的基本类型值为LUA_TBOOLEAN为1,看前4位,与第5位的值,看得出来tt_ 若为(高位在左) 0000 0001,即为false;若为 0001 0001,即为true。

判断布尔变量的宏定义见源码《lobject.h》

img

TValue

因为存储的直接用变体形式 则TValue 不存在

数字类型 Int and Float

历史

数字我们都知道是什么,在编程上面,数字分为整数浮点数两种。

int iValue = 1; //整数

float fValue = 3.14f; //浮点

Lua在5.3版本之前,在底层实现中,所有数字都是浮点数,没有整数的概念,整数在通过浮点数(IEEE表示法)存储,会产生浮点误差

而在Lua5.3版本开始,Lua添加了对整数的支持,让整数从浮点中独立出来,不再使用浮点数进行表示,并支持了位运算这类整数运算的操作符。

上文对布尔类型的讲解中我们提到Lua中每个基础对象会需要存储其类型标识 type tag(简称tt),见源码*《lua.h》*,可以看到数字类型的tt枚举值为LUA_TNUMBER(对应数字3)。

img

我们说数字分为整型和浮点,他们都属于LUA_TNUMBER,像布尔值通过类型变体区分true和false的方式一样,数字类型也是通过类型变体来区分整型和浮点。

见源码《lobject.h》*, 类型变体*LUA_VNUMINT即为整型,而LUA_VNUMFLT即为浮点型**:

img

tt_

我们再次复习一下,对象类型标识type tag(tt)字段共8位,前4位用于代表基本类型,此处即为LUA_TNUMBER(3, 二进制为11),第5位用于表达类型变体,而makevariant 函数即是用于修改前4位之后的数值:

所以tt字段,整型LUA_VNUMINT的2进制8个位的具体值即为 0000 0011

浮点型LUA_VNUMFLT则为:0001 0011

TValue

数字类型与布尔类型不同的是,数字类型除了要定义这个类型标识以外,还需要存储实际的数值。说到这里,我们再重新看一下TValue中Value字段的定义,可以看到里面有 in 两个字段:

img

图4

如英文注释的字面意思,这个Union联合体Value中的 i 字段即是用来存储整型, n 用于存储浮点型。lua_Number这里不能按英文意思,单指浮点类型;而lua_Integer则单指整型。

所以,用于设置整数或者浮点数的方法也呼之欲出了,见***《lobject.h》***:

img

图5

val宏的作用即为取Value的value字段:

img

图6

所以存储一个数字类型需要先设置 value_字段中的n字段(整型)或者 i 字段(浮点型),然后用settt_宏设置type tag(tt)字段为对应上面所说的值LUA_VNUMFLT或者LUA_VNUMINT

除了set函数,上图中的chg(代表change)开头的函数用于修改数值,用于相同类型操作,单纯改变数值,不改变tt。

默认数据类型

接下来我们再看一下代表整数的lua_Integer 和 代表浮点数的lua_Number 在底层到底是什么样的数据类型。

源码***《lua.h》***中声明lua_Number为LUA_NUMBER,而lua_Integer的为LUA_INTERGER。

img

图7

再继续深入学习它们的定义,这里我们先看源码***《luaconf.h》***这个地方:

图8

上图表示整型有3种:int, long, long long;默认long long Windows可以使用__int64

浮点型也有3种: float, double, long double。 默认double

然后箭头所指表示当前使用的默认类型,看注释可见Lua5.4默认为64位,然后整型使用long long,浮点型使用的是double

还是源码***《luaconf.h》***下面部分则有真正的类型声明:

img

图9

可见默认定义LUA_INT_LONGLONG最后就是对应long long类型。另外看另一个箭头可以看得出来在Windows平台上整型会使用__int64类型。

浮点的定义也是类似,默认的LUA_FLOAT_DOUBLE会被定义为类型double,这里不再赘述:

img

图10

字符串 String

tt_

《lua.h》字符串的基础数据类型的枚举为LUA_TSTRING,对应数值4

在LUA中,为了有更好的性能,在底层实现中字符串分为两种:一种为短字符串,一种为长字符串,对应两种类型变体《lobject.h》

img

TValue

用类型TString初始化创建一个TValue令其为字符串的函数如下:

img

图6

上图的TValue* io即为需要返回的TValue,主要做两件事:

1)设置io的value字段里面的gc字段指向这个TString字符串;

2)标记type tag(tt)为IS_COLLECTABLE可回收。

CommonHeader宏定义:

img

图7

所有可以被GC垃圾回收的对象结构都会有这个宏加在结构体头部,我们这里先不深入。也正是因为有这个GC对象头部,TValue中的GC指针才能指向同样有此GC头部的TStringcf对象。

类型字段含义

短字符串会用到的字段,用到了下面括号(1~4)共4个变量

1)shrlen:表示短字符串的长度,lua_byte代表短字符串最长为8位即长度最长可为2的8次方即256

(2)hash:代表字符串的哈希值

(3)stringtable:哈希表,冲突解决方案为链表法。u在短字符串中使用hnext字段,用于指向缓冲短字符串的哈希表中的下一个元素。

(4)contents:指向这个字符串char[]数组的第一个字符,用于访问该字符数组。

长字符串则会用到下面括号(1~4)共4个变量:

(1)extra:表示该字符串是否已经计算过哈希值,0代表没有计算过,此时hash字段无意义,使用前需要先进行一次计算。1代表已经计算过,此时hash字段是contents所指向字符串的哈希值;

(2)hash:代表字符串的哈希值;

(3)u:在长字符串中使用lnglen字段,代表长字符串的长度,类型为size_t,足以表示上亿的长度。

(4)contents:跟短字符串一样,表示指向这个字符串char[]数组的第一个字符,用于访问该字符数组。

为什么要有短字符串

stringtable

img

1)hash:指向一个TString链表的数组,即数组里面每一个元素为一个TString链表;

2)nuse:代表hash中实际存储了多少个TString字符串;

3)size:代表hash的容量。

这个stringtable变量是在global_State中进行定义

lua运行的时候全局会有且仅有一个global_State变量

会调用下面luaS_init函数进行字符串相关数据的初始化,这里会初始化stringtable和strcache缓存

img

tb->hash = luaM_newvector(L, MINSTRTABSIZE, TString*);

初始化这个hash数组第一维的大小为MINSTRTABSIZE 128

字符串TString通过size_t l长度进行创建的接口luaS_newlstr

img

若要创建的字符串长度小于等于LUAI_MAXSHORTLEN(40),即视作创建短字符串

调用的创建短字符的接口internshrstr如下

img

(1)char* str是我们用来创建TString的字符数组,首先计算它的哈希值,并根据hash的size进行模运算使其在合理数组长度范围内

(2)遍历这个TString列表list,根据长度和逐字符判断要创建的值是否已经在list中存在,如果是的话则直接返回进行复用

(3)观察hash中num是否达到size,hash冲突使用链表展开,growstrtab会进行双倍扩容。

(4)调用 createstrobj 放入短字符串 链接hash的key

  • luaS_createlngstrobj创建长字符串

使用createstrobj 并extra赋值为0,extra如果为0代表该字符串哈希值还无意义,在使用时需要重新计算

因为计算哈希值需要遍历字符串中的每一个字符,而长字符串字符较多,所以长字符串为了创建的时候能更快速,并没有直接在创建时计算出来哈希值,而是通过extra作为延迟处理标记,用到的时候再进行计算,在Lua5.4版本的代码实现中,目前只会在长字符串作为Table的Key值的时候,才会去计算其哈希值。

luaS_New

在此之前代码做的事情就是简单地通过地址判断字符串是否已经在缓存中,point2uint(str)

访问r的G(L)->strcache为字符串根据地址的缓存数组,也是全局唯一

大小是一个TString[53][2]的一个二维数组

短字符串的stringtable是个缓存,与此相比,这个strcache也是缓存,而更像是1级缓存,然后stringtable相当于短字符串的2级缓存。

Strcache

即是类似C# string 的不可变性

Table

https://zhuanlan.zhihu.com/p/595807320

《lua.h》表的基础数据类型的枚举为LUA_TTABLE,对应数值5

_tt

tt二进制8位为:0000 0101。

TValue

表是存储在Value中的gc字段指向的内存地址空间中,对象类型名字叫作”Table”

1
2
Table *x_ = (x);
val_(io).gc = obj2gco(x_);

Table结构

CommonHeader,能上相当于Table对象继承自GCObject(前面讲string有讲过)

表能同时充当数组和字典,注意,这里是支持“同时”!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct Table {//GC对象通用头部定义
CommonHeader;// 这几个字段暂时先不管
lu_byte flags; //元表字段查询标记
struct Table *metatable; //元表
GCObject *gclist; //垃圾回收相关

/////////// 数组相关变量声明
unsigned int alimit; // 数组长度
TValue *array; // 指向数组的指针

//////////// 字典相关变量声明
lu_byte lsizenode; /* 存储字典容量log2后的值 */Node *node; // 指向字典首个结点的指针
Node *lastfree; // 上一次空的结点位置
} Table;

Table的创建

《ltable.c》luaH_new和luaH_free是表的创建与删除函数

初始化数组部分 是直接 t->array = NULL; 而没有用一个默认长度初始化。

字典初始化setnodevector(L, t, 0) 传入一个size为0 即为初始化。

所以空表

1
local t = {}

内存占用不会太大

Table中元素的操作

对于Table中每个元素,都称为一个槽位(Slot),槽位指向数组or字典部分,根据类型查询or插入实际的数据。

查询:

  • 《ltable.c》luaH_get 返回参数key的槽位
    • 若 tt==(短字符串,整数,浮点,nil)则调用特殊get函数,其他调用默认get函数getgeneric()
  • getgeneric() 查询key是否在hash中

修改:

luaH_set :先用luaH_get 获取tt 后赋值

哈希表

1
2
3
lu_byte lsizenode;  /* 存储字典容量log2后的值 */
Node *node; // 指向字典首个结点的指针
Node *lastfree; // 上一次空的结点位置
  • 初始化 setnodevector()
    • size向上取整2次幂,然后初始化kv都为nil
    • 每一个节点都是Node
    • Key:NodeKey {key_tt,key_val}
    • Val:i_val
  • 哈希函数,在getgeneric()第一行的mainpositionTV()计算
1
Node *n = mainpositionTV(t, key);
  • 简单的介绍一下hashint()

对lsizenode2次幂减1取模(最后按位或1,是为了保持要用来取模的数字是不为0,是大于等于1的数)

  • 哈希冲突:链地址 开放寻址
  • 开放寻址
    • 冲突在luaH_set()中调用luaH_finishset()
    • luaH_finishset()调用 luaH_newkey()
      • 功能包含了槽位定位,哈希处理,哈希表扩容,槽位设置数据。
      • 也就是开放寻址法的落地
      • 寻找空槽位在getfreepos()中

img

扩容Rehash

数组

数组需要满足:2 的幂次方扩容”和“使用率超过 50%

满足:数组部分的结点数量必须要大于数组长度的一半。

则迭代

迭代中止后,以满足条件可以放入数组部分的数量,修正返回的na值,na值为最大合法数组长度时的整数key数量,若没有合法长度,则修正为0;

那么

1
2
local t = {}
t[5] = "hello"

是存在hash还是array呢?

一开始na(需要放入数组部分的元素个数)= 1

数组扩容为1:na > (1 / 2) => 1 > 0.5 ,key=5不属于[1,1]

数组扩容为2:na > (2 / 2) => 1 > 1,不满足

所以这个元素是在hash中

它不会为了一个稀疏的、孤立的整数键(比如 t[1000000] = v)而分配一个巨大的数组,从而有效地节省了内存

哈希表

1)统计插入当前新元素前所有Key的数量,令为totaluse;

2)totaluse加1 (因为当前准备要插入新的元素,预留多一个位置给当前将要插入的元素);

3)令所有key为整数的数量为na(如果新插入的元素的键也是整数,则也统计上);

4)哈希表长度即为totaluse - na,然后向上取最近的2次幂即为实际哈希表分配长度。

在本例t[5] = “hello” 中算法执行逻辑如下:

1)在插入该新元素前,table为空,所以totaluse为0;

2)准备插入新元素,totaluse + 1,所以totaluse为1;

3)在插入该新元素前,key为整数的数量为0,所以na为0;

4)准备插入的新元素的key为整数,所以na + 1,所以na为1;

5)经过我们上述的computesizes算法计算,没有合法数组长度,na被修正为0;

6)最终哈希表长度即为totaluse - na,即1-0为1;

所以 t[5] = “hello” 这句代码会创建出长度为1的哈希表。

MetaTable

https://zhuanlan.zhihu.com/p/595829550

见源码《lobject.h》Table中的metatable

Struct Table* metatable;

元表相关的方法都叫元方法,源码中大多以tm(tag method)开头

源码《ltm.h》中有注册元方法的枚举值TMS(tag methods),枚举值对应的key(string)见luaT_eventname。

元方法

__index

Table 在查询自身不存在的key的时候会调用该函数

1
2
3
local t = {}
local metatable_t = {}
setmetatable(t, metatable_t)

可以这样重定义元方法

metatable_t.__index = { [1] = “hello 1”,[2] = “hello 2”,}

在调用 print(t[1]) 可以打印 hello 1

  • luaV_finishget
    • 会去访问’val = t[key]’ 而在这边会回调 __index
    • 利用tm 进行去 获得元方法的指针(TValue *)会赋值与以下四点
      • 若为非table类型变量,则调用自身的__index元方法。
      • 若为table类型变量,则去查询metatable的__index,若无metatable or metatable 无 __index 则 返回空,若有返回值则为tm
      • 若tm为函数,则调用该函数
      • 若tm为table,则尝试去key从tm这个table中获取val
    • 上面这个tm判定会在一个for中,这个for相当于递归的上层,这边代表可以支持多层级递归。

__newindex

table在设置一个自身还不存在的键的值的时候会调用__newindex元方法来完成设置

用法1(函数方式)

1
2
3
4
5
6
local t = {}
local metatable_t = {
__newindex = function(tt, key, value)
rawset(tt, key, value)
end,
}

用法2(table方式)

1
2
3
4
5
6
7
8
local t = {}
local newIndexTable = {
[1] = "hello in metatable_t __newindex",
}
local metatable_t = {
newindex = newIndexTable,
}
setmetatable(t, metatable_t)
  • luaV_finishset

同index 在 luaV_finishset 中 也一样 会触发不同的tm情况

__gc

《lgc.c》实现,在清除对象的时候会调用GCTM

  • GCTM
    • 获取__gc元方法
    • 若定义了__gc元方法,则调用此函数。

__mode

用于控制元表的数据引用方式。lua中会满足该对象没有被引用的时候才会被GC

1)”k”: 声明键为弱引用,当键引用的对象被置空时,table会清除这个结点。

2)”v”: 声明值为弱引用,当值引用的对象被置空时,table会清除这个结点。

3)”kv”: 声明键和值都为弱引用,当其中一个引用的对象被置空时,table会清除这个结点。

《lgc.c》traversetable中

会分析__mode 的值,若无值,则会标记所有kv,依次下去

后会进行可达性分析,遍历所有table,标记清除table树。

最后清除在

1
linkgclist(h, g->allweak);  /* nothing to traverse now */

中,进行GC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local clearValue = {"123"}
local t = {
["value_key"] = clearValue --1.这边为深拷贝
}

-- 2.声明值为弱引用 浅拷贝
setmetatable(t, {__mode = "v"})

-- set value to nil
clearValue = nil

-- 手动执行gc回收操作
collectgarbage()

for k, v in pairs(t["value_key"]) do
print(k, v) -- 1.依旧输出123 2.输出nil
end

__len与#

《ltable.c》中luaH_getn

为#t 的底层,针对数组部分,若hash的key连着数组部分则一起计算(ipair)

在Table数据结构中

边界长度有关的2个变量分别为

  • alimit:在大部份情况下为数组的长度(2次幂数),若不等于数组长度的时候,则数组长度为刚好比这个数大的下一个2次幂数,此时其数值为上次计算边界后缓存的返回值
  • flags:第8位0表达当前alimit是真正的数组长度,1的话则不是

__eq 相等方法

自定义相等判断方法

  • 《lvm.c》luaV_equalobj
    • 没有定义__eq的话会直接通过指针地址判断两个table对象t1, t2是否相等。
    • 若t1或t2定义了__eq元方法,这里注意不需要两个table都定义,其中一个定义了即生效,则调用此元方法来做判断。

__add

自定义加法

  • 《lobject.c》luaO_arith
    • 当执行加法的两个类型无法做加法运算的时候,就会尝试调用__add元方法。其它元方法的调用判断流程也是类似。

UserData

https://zhuanlan.zhihu.com/p/595959126

源码《lua.h》,类型枚举为LUA_TUSERDATA,数值为7,二进制8位为0000 0111 没有其他变体

  • UserData也可以看作为GCObject 的子类,
    • 判断变量是否合法的时候都会使用ctb宏
    • 只有满足ctb宏IS_COLLECTABLE(第7位为1)才是合法的对象。
    • 所以一个合法的UserData类型的类型标识(type tag)8位二进制为0100 0111。
  • val_(o).gc表示UserData数据存于gc指针(GCObject*)指向的地址上
  • gco2u(GCObject*)函数则把该指针转换为真正的对象
1
#define gco2u(o)  check_exp((o)->tt == LUA_VUSERDATA, &((cast_u(o))->u))

Udata

Udata就是UserData在源码中的具体数据类型结构

union GCUnion 中有 struct Udata u;

Udata 结构:

luaS_newudata在C语言中创建一个userdata后,bindata这部分数据在栈顶(栈顶指针)

img

img

New:《lstring.c》luaS_newudata

Set: 《lapi.c》lua_setiuservalue

Get:《lapi.c》lua_getiuservalue

del: 通过GC清除,单独管理lua域。若c语言调用不算引用。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.lua
local people = People.New()print(people.name)

.c
// 只需要提前调用一次,把__name为"People"的元表注册到Lua
static int RegisterPeopleMetatable(lua_State *L){
luaL_newmetatable(L, "People");
return 1;
}


static int People(lua_State *L){

struct People *pPeople = (struct People *)lua_newuserdata(L, sizeof(struct People));
// 设置上面这个UserData的metatable
//为已经注册好的__name为"People"的元表
luaL_setmetatable(L, "People");

return 1; // 新的userdata已经在栈上了
}


.c
//若修改一下 替换成luaL_testudata函数
static int InitName(lua_State *L){
// 第 1 步
struct People *pPeople = (struct People *)luaL_testudata(L, 1, “People”);
// 第 2 步
if(pPeople != NULL){
pPeople->name = "MaNong";
}
return 1;
}

把lua_touserdata改成luaL_testudata函数,函数实现如下,区别在于调用lua_touserdata后,还会再获取它的元表,并判断元表中的__name字段,看是否我们参数中传进来的字符串,若相等才是我们真正期待获取的UserData并进行返回,否则返回NULL

LightUserData

LightUserData是指针比起(Full)UserData要简单很多。

这种new 方法会将内存分配在c语言部分的堆中。

C语言结构体对象的内存希望在C语言中进行分配与管理,则可以使用LightUserData,Lua只存储一个指针值;

但若希望对象的内存在Lua中分配与管理,则需要使用UserData。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int People(lua_State *L)
{
// 对象分配在C语言的堆内存中,并不在Lua中
People *pPeople = new People();

lua_pushlightuserdata(L, pPeople);

return 1; // 新的lightuserdata已经在栈上了
}

//返回一个指针,指向C语言堆中的People对象,
//函数内部会根据类型转化为UserData或LightUserData
static int InitName(lua_State *L)
{
// 第 1 步
struct People *pPeople = (struct People *)lua_touserdata(L, 1);
// 第 2 步
if(pPeople != NULL)
{
pPeople->name = "MaNong";
}
return 1;
}

Funtion

https://zhuanlan.zhihu.com/p/595965132

Function即为函数,函数加上它的UpValue我们称之为闭包。UpValue可以理解为在当前函数外声明但函数内也可以访问到的变量,类似于全局变量但有一定作用域的一种数据,其中闭包又分为C类型闭包与Lua类型闭包。本文将对闭包进行详细讲解,并会讲解在Lua中如何实现函数的多返回值与尾调用。

1)第一类值语言:它的函数跟数值类型,布尔类型等的地位是一样的,可被动态创建,存储与销毁。

2)第二类值语言:这类编程语言不能存储函数,不能动态创建函数,不能动态销毁函数,函数在编译过后就存储于程序特定内存位置,其它地方只能存储一个指向函数的指针。

Lua是第一类值语言,所以在Lua中可以动态创建销毁一个函数。接下来,我们正式开始函数的学习。

  • 类型枚举

Lua中函数的基本类型枚举为《lua.h》LUA_TFUNCTION,数值为6,对应的8位二进制为0000 0110。

  • 变体

总共有3种函数类型变体,分别用不同的参数调用了makevariant,

CL是Closure(闭包)的缩写

1)LUA_VLCL:Lua闭包,tt为:0000 0110。

2)LUA_VLCF:C函数指针,LUA源码是用C语言实现的,这个是一个纯粹的指向一个C语言函数的指针,不带UpValue。tt为0001 0110。

3)LUA_VCCL:C语言闭包,tt为0010 0110。

  • 声明《lobject.h》

C语言闭包CClosure

Lua闭包,LClosure

都声明了ClosureHeader 宏 ->CommonHeader宏

Lua闭包

1
2
3
4
5
6
7
8
9
10
function Main()
local v1 = 1
local inner_func = function()
local v2 = 99
print(v1 + v2)
end
inner_func()
end

Main()

inner_func中成功访问了不在自身函数作用域内的变量v1,这个v1即为UpValue

Proto

《lvm.c》pushclosure

Proto即为函数的底层数据结构,函数中也对UpValue数组进行了初始化

1)lu_byte numparams:函数参数的数量。

2)lu_byte maxstacksize:该函数所需要的寄存器数量。

3)int sizeupvalues,Upvaldesc *upvalues:UpValue的数量与描述(描述并不是UpValue的具体数值,而只是描述UpValue的变量名字,作用域是在本函数内部还是在外层,还有一个用于定位UpValue所在位置的指针偏移值)。

4)int sizek,TValue *k:k代表常量,这里分别是常量的数量与常量所存储在的数组。这里可以知道常量在Lua中也是像普通数据类型一样需要占用内存的。

5)Proto **p:函数内部又定义了的其它函数,所以说Lua的函数支持嵌套定义。

6)int sizecode,Instruction *code:该函数所调用的所有指令码数量和指令码数组。函数执行的时候就是按顺序跑这些的指令码。

7)int linedefined,int lastlinedefined:函数的开始与结束的行。

debug库

1)ls_byte *lineinfo,int sizelineinfo,AbsLineInfo *abslineinfo,int sizeabslineinfo:存储函数内各条指令码的地址偏移量。若偏移值过大,则会同时记录该指令的绝对偏移值。

2)LocVar *locvars:局部变量的描述。

3)TString *source:该函数的源代码的字符串表示。

UpValue

Thread

《lua.h》LUA_TTHREAD 数值为8,所以8位二进制类型标识type tag(tt)为: 0000 1000。

Thread也是可以看作GCObject的子类型,所以设置与检测一个变量类型是否为合法的Thread类型的时候也需要加上ctb宏标记当前是合法的GC可收集对象

lua_State

thread是通过基础数据结构TValue的gc字段进行访问,然后它指向的真正类型为lua_State

协程有着独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协程共享大部分全局数据。而这些独立的东西,正是存储于上面这个lua_State

1)lu_byte status:当前状态机的状态,这里的状态与协程的状态不太一样,这里的状态的定义见如下源码《lua.h》

2)

《lua.h》

代表分别函数可以在以下阶段设置勾子:

  1. LUA_MASKCALL: 被调用时;
  2. LUA_MASKRET: 执行结束返回的时候;
  3. LUA_MASKLINE: 每一行执行完后;
  4. LUA_MASKCOUNT: 每次执行统计运行次数;

协程(Coroutine)的使用

1)挂起(suspended):对应枚举为COS_YIELD,当一个协程刚被创建时或遇到函数中的yield关键字时。

2)运行(running):对应枚举为COS_RUN,当协程运行时。

3)正常(normal):对应枚举为COS_NORM,当协程处于活跃状态,但没有被运行(这意味着程序在运行另一个协程,比如从协程A中唤醒协程B,此时A就处于正常状态,因为当前运行的是协程B)。

4)死亡(dead):对应枚举为COS_DEAD,当协程运行完要执行的代码时或者在运行代码时发生了错误(error)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//1)
local co1
local co2
co1 = coroutine.create(function( ... )
print('cor1:', ...)
//3.1)
print("cor1 resume cor2 1:", coroutine.resume(co2, "param from co1 1"))
//4.2) -> 5.1)
print("cor1 resume cor2 2:", coroutine.resume(co2, "param from co1 2"))
end)

co2 = coroutine.create(function( ... )
//3.2)
print('cor2:', ...)
//4.1)
print(coroutine.yield("param from co2"))
//5.2
end)

//2) -> 6)
print("main:", coroutine.resume(co1, "param from main"))

1)首先通过coroutine.create创建了两个协程co1,co2。

2)接着调用coroutine.resume(co1, “param from main”),使co1由创建时候的suspended状态切换到running状态,co1对应函数开始执行,调用resume的后面的参数则直接作为运行co1函数的传入参数,所以第一个打印为”co1: param from main”。

3)co1在running状态下又调用了resume(co2),并输入参数”param from co1 1”,这时候co1由running状态切换到normal状态,co2由resume状态切换到running状态。所以第二个打印为”co2: param from co1 1”。

4)接下来在co2的执行中调用了coroutine.yield,co2由running状态切换到suspended状态,co2把 “param from co2” 返回给上次resume它的那个协程,所以控制权又回到了co1,co1由normal状态再次进入runner状态,此时输出第三个打印”cor1 resume cor2 1:true param from co2”。

5)co1再次resume co2,co2此时再次由suspended状态进入到running状态,而由于co2函数已经执行完毕,所以co2进入dead状态,并再次返回给co1,此时在co1中输出第4个打印:”cor1 resume cor2 2:true”。

6)co1执行完毕,进入dead状态,返回到外部主函数,输出最后一个打印”main:true”。