Lua语言入门基础

编程

Wax是一个不怎么热门的App脚本化的框架,基于Lua引擎与Objc运行时特性来实现。苹果对JSPatch着力打压,而刚好我之前有过Wax相关的实践,正好可以介绍下。Wax相关内容主要包括(一)Lua语言基础(二)Wax的实现原理。

本文主要介绍Lua的一些基础概念与入门知识。

需要说明的是,Wax是基于Lua的v5.1版本的(Wax第一次提交是在2009年6月),v5.2是在2011年末发布,C API有变动,且不兼容。本文中主要基于v5.1的manual对与wax框架相关的内容进行介绍。

写在前面

Lua是一个开源的脚本语言,官网是www.lua.org。但除了语言作者的Lua实现之外,还有LuaJIT这样的非官方实现。我们讨论的是Lua官网的标准实现。

Lua跟JavaScript有着很多相似的地方。比如JavaScript的prototype对应Lua的元表、元方法,还有变量对象与执行环境。当然Lua其实更早出现,v1.0在1993年发布(公众版本是’94年v1.1),一直至今遵循其简洁高效的设计目标。且作为一门TIOBE前30、核心库只有100KB(v5.1),且广泛应用于游戏编程、嵌入脚本的语言,也是非常值得学习的。

Lua语法简单且有强大的数据描述结构。相比而言,JavaScript要复杂很多——至少看上去specification也是多很多。Lua是动态类型的,通过一个基于寄存器的虚拟机来解析字节码,并且是支持递进式垃圾回收的自动内存管理;被实现为一个用纯C编写的代码库(Lua核心,其他都是基于这些库来开发)。

Lua通常是作为一个拓展语言被嵌入到宿主程序中,宿主程序可以调用Lua函数、读写Lua变量以及注册C函数提供给Lua调用。Lua核心库的C-API不多,能方便的被嵌入主程序提供脚本化支持,这也是Lua的设计目标及优点所在。

需要说明的是,Lua编程其实涉及两方面:除了Lua脚本编程,还有与宿主程序的对接(基于Lua的C API编程)。像Wax这样的框架,主要是处理与宿主程序的对接,也就是Objc与Lua的互调与数据通信。

学习线路

对于希望了解Lua的童鞋,建议根据需要选择学习路线:

  1. 入门了解:可以参考Coolshell的“简明Lua教程”
  2. Lua语法与C API:“Programming in Lua”(简称PIL,Lua作者编写),主要介绍语法及Lua标准库、C接口API等细节;
  3. Lua的语言设计:语言手册,manual,是Lua的语言规范,可以理解为原理介绍;
  4. Lua5.0的实现:作者的语言设计论文。

学习Lua脚本编程的话1、2就够了。

Lua基本概念

基本数据类型

Lua中有8中基本类型:nil, boolean, number, string, function, userdata, thread, 以及 table。Lua是动态类型语言,这意味着变量没有类型,值才有类型。所有的值都是一等公民(first-class values),也就是包括functionthread等在内的所有类型值都可作为参数或返回值。

nil是一个值,表示一个有意义的值不存在时的状态。这个得提一下作者关于的数据结构设计:

1
2
3
4
5
6
7
8
9
10
11
typedef struct { 
int t;
Value v;
} TObject;

typedef union {
GCObject *gc;
void *p;
lua_Number n;
int b;
} Value;

称之为tagged-union,打了标签的联合体,(t, v)标签-值对。

在Lua的实现中,用t一个标志位即可表示nil值。具体的值则通过Value这个联合体表征,可以是由GC管理的对象GCObject *gc、C指针p表示的轻量用户数据、lua_Number表示double类型(number)、整型b表示booleanGCObject可表示stringtablefunction、heavy userdatathread

string是有明确大小的字节数组,因此可以包含任意二进制数据,包括”\0”。

userdata (通常译为”用户数据“,若非特别指明,指的是”完全用户数据“)类型允许将 C 中的数据保存在 Lua 变量中。 用户数据类型的值是一个内存块。有两种用户数据:完全用户数据,指一块由 Lua 管理的内存对应的对象;轻量用户数据,则指一个简单的 C 指针,内存由宿主程序负责管理。用户数据在 Lua 中除了赋值与相等性判断之外没有其他预定义的操作。 通过使用 元表 (元表在下文介绍),程序员可以给完全用户数据定义一系列的操作。 你只能通过 C API 而无法在 Lua 代码中创建或者修改用户数据的值,这保证了数据仅被宿主程序所控制。wax有使用userdata将Objc中的值传递给Lua中使用。

table类型是Lua中主要的数据结构(通常译为”表“,下文中如无歧义”表“指代table类型的值),也是Lua脚本中唯一的可自定义的数据结构。table在lua中被实现为关联数组,它的索引可以是任意非nil值,值可以是任意值,table的大小动态增减。在Lua5.0之前table被实现为哈希表,v5.0中则针对数组做了优化:由哈希及数组组成,数组可以减少key的存储,且访问更直接,参考图:

thread则代表协程,协程是一种并发编程方式,高大上的内容Wax框架中没有涉及,不做介绍。

function表示Lua中的方法或者遵循Lua虚拟机交互接口的C函数(因为可以注册C函数到Lua中使用)。

tablefunctionthread以及userdata完全用户数据在 Lua 中被称为对象: 变量并不持有这些对象的值(value),而仅保存了对这些对象的引用(reference)。 赋值、参数传递、函数返回,都是针对引用而不是针对值的操作, 不会做任何形式的拷贝。

使用type库函数可以返回一个string格式的值类型的描述。

关键词

主要是以下这些:

1
2
and, break, do, else, elseif, end, false, for, function, if, in,
local, nil, not, or, repeat, return, then, true, until, while

真的是很少的了!

错误处理

作为嵌入式语言,Lua中所有行为始于宿主程序中C代码对Lua库函数的调用,因此它也将错误处理的主导权交给宿主程序。

在Lua中可以使用error来显式抛出错误,若希望在Lua中捕获错误,则使用pcallxpcall在保护模式下调用函数。出现错误时,会抛出一个错误对象。使用 xpcalllua_pcall 时, 需提供一个 消息处理函数用于错误抛出时调用。该函数需接收原始的错误消息,并返回一个新的错误消息。它在错误发生后栈尚未展开时调用, 因此可以利用栈来收集更多的信息, 比如通过探知栈来创建一组栈回溯信息。

若需在C中捕捉异常,使用lua_atpanic指定一个回调方法。

垃圾回收

Lua自动内存管理,运行GC(Garbage Collector,垃圾收集器)来回收内存,所有由Lua使用的内存都遵循自动内存管理,包含tablefunctionstring等。使用两个参数来控制垃圾收集循环:垃圾收集间歇率及步进率,具体参数意义可参考manual中说明。可以通过在 C 中调用 lua_gc 或在 Lua 中调用 collectgarbage 来改变参数。主要介绍垃圾回收元方法及弱表,这在wax中都有使用。

垃圾回收元方法

对于userdata用户数据类型,可以通过C API来设置垃圾回收器元方法,这些元方法也被称之为“终结器(finalizer)“。”终结器“允许你来协调Lua的GC与外部的资源管理(如文件管理、网络、数据库连接或内存释放等)。

对于元表中包含__gc元方法的userdata用户数据类型,GC不做立即回收,而是将userdata放到一个列表中,等当前回收循环接收后,以LIFO的方式,分别调用其__gc垃圾回收元方法,参数为userdata,此步骤中交由宿主程序处理其内存释放问题,而userdata用户数据类型值本身将在下一轮垃圾回收循环中被释放。

弱表

弱表指的是内部元素是弱引用的表。GC会忽略弱引用的对象,除非该对象只被弱引用。

弱表可以有弱键、弱值或键值都是弱引用。弱键的表允许GC回收它的键而阻止回收它的值;键值都是弱引用的表则允许GC回收其键值。在任何情况下,只要键或值被回收,该键值对也会从表中移除。

表的键值弱属性,由其元表的__mode域进行控制,弱键或弱值分别对应kv。在将一个表指定为元表后,其中的__mode域就不应再修改,否则由这张元表控制的表的弱属性行为是未定义的。

语法规则

主要提供完整的扩展BNF范式描述的语法规则(v5.3)。BNF中,{}表示0或多个,[]表示可选,|表示或者,:=表示定义。详细看PIL介绍,不展开介绍语法规则了。对于一些关键的概念则在下文中介绍。

chunk ::= block
block ::= {stat} [retstat]
stat ::=  ‘;’ | 
     varlist ‘=’ explist | 
     functioncall | 
     label | 
     break | 
     goto Name | 
     do block end | 
     while exp do block end | 
     repeat block until exp | 
     if exp then block {elseif exp then block} [else block] end | 
     for Name ‘=’ exp ‘,’ exp [‘,’ exp] do block end | 
     for namelist in explist do block end | 
     function funcname funcbody | 
     local function Name funcbody | 
     local namelist [‘=’ explist] 

retstat ::= return [explist] [‘;’]

label ::= ‘::’ Name ‘::’

funcname ::= Name {‘.’ Name} [‘:’ Name]

varlist ::= var {‘,’ var}

var ::=  Name | prefixexp ‘[’ exp ‘]’ | prefixexp ‘.’ Name 

namelist ::= Name {‘,’ Name}

explist ::= exp {‘,’ exp}

exp ::=  nil | false | true | Numeral | LiteralString | ‘...’ | functiondef | 
     prefixexp | tableconstructor | exp binop exp | unop exp 

prefixexp ::= var | functioncall | ‘(’ exp ‘)’

functioncall ::=  prefixexp args | prefixexp ‘:’ Name args 

args ::=  ‘(’ [explist] ‘)’ | tableconstructor | LiteralString 

functiondef ::= function funcbody

funcbody ::= ‘(’ [parlist] ‘)’ block end

parlist ::= namelist [‘,’ ‘...’] | ‘...’

tableconstructor ::= ‘{’ [fieldlist] ‘}’

fieldlist ::= field {fieldsep field} [fieldsep]

field ::= ‘[’ exp ‘]’ ‘=’ exp | Name ‘=’ exp | exp

fieldsep ::= ‘,’ | ‘;’

binop ::=  ‘+’ | ‘-’ | ‘*’ | ‘/’ | ‘//’ | ‘^’ | ‘%’ | 
     ‘&’ | ‘~’ | ‘|’ | ‘>>’ | ‘<<’ | ‘..’ | 
     ‘<’ | ‘<=’ | ‘>’ | ‘>=’ | ‘==’ | ‘~=’ | 
     and | or

unop ::= ‘-’ | not | ‘#’ | ‘~’

元表及元方法

Lua中的每一个值都可以有一个元表(metatable),这个元表是一个普通的Lua表,其中定义了原始值在特定操作下的行为。可以通过修改元表的相应的域,来改变这些行为。例如,当一个非数值的值作为加法操作的操作数时,Lua会检查其元表中对应”__add“域的方法,若存在则调用该方法来执行加法操作。

元表中的这些键对应被称之为事件,对应的值则称之为元方法(metamethod)。在上面的例子中,”add“是事件而元方法则是执行加法操作的函数(function)。在Lua中可以通过getmetatable方法来获取任何值的元表;对于table类型的值可以通过setmetatable方法来替换其元表,但对于其他类型除非使用debug库不能修改其元表,如要改则需要调用C类型API。gc也是一个特定的事件,其对应的元方法用于配合GC进行内存处理等。

tableuserdata(完全用户数据)类型的值可以有独立的元表(当然也可以多个table或userdata共享同一张元表),而其他类型的值每种类型共享一个元表。

元表中以”前缀+事件名“作为对应的键,用来存放对应事件操作的元方法。如对应”add“操作(事件)的key为”__add“。元方法的访问不会触发另外的元方法,访问一个没有元表的对象的元方法不会失败(仅返回nil)。在Lua或C中可以通过rawsetrawget的方式,在不触发访问元表的情况下进行访问。

有诸多内置的操作和元方法,如算术操作add等、比较操作eq等、索引index、索引赋值newindex以及函数调用call等。如果需要比较的话,大概相当于操作符重载的概念。具体可查看manual,这里主要介绍比较重要的三个:

index

index操作主要是对值进行索引(或者称为subscript),对table类型相当于调用 table[key],也就是一个查找操作。 当 table 不是表或是表 table 中不存在 key 这个键时,index被触发。 此时,会调用__index相应的元方法,可以是一个函数也可以是一张表。

如果是一个函数,则以 table 和 key 作为参数调用它。 如果它是一张表,最终的结果就是以 key 取索引这张表的结果。 (这个索引过程是走常规的流程,而不是直接索引, 所以这次索引有可能引发另一次元方法。)

需要先指出的是,通过C API可以向Lua注册函数,因此同样可以在C中注册__index这些域对应的函数,在宿主程序中接收相应的事件并将处理结果回传到Lua。这也是Wax中所使用到的,用来索引Objc类及创建对象实例。

newindex

newindex操作对值相应的域进行赋值,相当于 table[key] = value,赋值操作。和索引事件类似,当table 不是表或是表 table 中不存在 key 这个键的时候,会触发newindex事件,调用__newindex元方法(若存在的话)。

call

call操作是函数调用, 当 Lua 尝试调用一个非函数的值的时候会触发这个事件 (即 func 不是一个函数)。 查找 func 的元方法, 如果找得到,就调用这个元方法, func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。

Lua的元表跟JavaScript的prototype有几分相像,在JavaScript中可以通过prototype原型链支持面向对象等特性,其实在Lua中同样可以利用元表及执行环境来实现。

执行环境

除了元表,threadfunctionusertable这些类型的对象还绑定了另外一张表,称之为其执行环境(environment)。与元表类似,执行环境也是一张普通的table类型的表,多个对象也可以共享同一个表。

thread类型,协程创建时,得到的thread对象与创建时的协程共享执行环境。

userdata类型及C函数与其创建函数共享执行环境。非嵌套Lua函数(由loadfileloadstringload创建)于创建时的协程共享执行环境。内嵌的Lua函数与其创建时的Lua函数共享执行环境。userdata绑定的执行环境对Lua是无意义的。

Lua函数绑定的执行环境被用于访问函数中的全局变量,也作为其内部创建的内嵌Lua函数的默认执行环境。

可以通过调用setfenv来改变Lua函数或者当前运行协程的执行环境,同理通过getfenv来获取其执行环境。对于其他类型的对象(userdata、C函数及其他协程),必须通过C的API来操作修改执行环境。

另外,Lua 中含有一个被称为全局环境的表,它被保存在 C 注册表(后面还有介绍)的一个特别索引LUA_RIDX_GLOBALS下。 在 Lua 中,全局变量 _G 被初始化为这个值。

但是,执行环境表有什么用,有什么必要修改?先来看一个实际的例子:

1
2
3
4
5
-- Security.lua
waxClass{"Security",NSObject}
function verify(self, pwd)
return true
end
1
2
3
4
5
6
7
8
9
10
-- iPhone.lua
require("Security")
waxClass{"iPhone",NSObject}
function unlock(self, pwd)
self:checkPwd(pwd)
Security:verify(pwd)
end
function checkPwd(self, pwd)
return true
end

Security:verify(pwd)以及self:checkPwd(pwd) 这些方法调用为什么能正常?明明verify并不是Security这个值的一个域,它仅是模块中定义的一个全局function变量吧?实际上,这个正是通过替换执行环境来实现的。(注:functionself的说明见后文)

因此,执行环境可以简单理解为包含其可访问全局变量的表。下例中会报错“attempt to call global ‘print’ (a nil value)”,因为当前执行环境被替换一张空表,并没有print的域。

1
2
3
4
5
-- 定义全局变量a
a = 1
setfenv(1, {})
print(a)
-- 报错

通过setfenv可以指定函数的执行环境,第一个参数1表示当前执行环境,2表示上层调用方的执行环境,以此类推。Wax实现的正是将waxClass定义类所在的模块中的所有function的执行环境指定为对应class的userdata相关表了。

另外,还需要指出的是,新版本的Lua已经废弃了setfenv的接口,而是采用了直接设置_ENV的方式来指定执行环境。

脚本加载及require

package库提供了Lua中加载与编译模块的基本工具,导出了两个方法requiremodule到全局环境。

require

require (modname)用来加载模块代码,首先会查找缓存表package.loaded,若modname模块被加载过,require 返回 package.loaded[modname] 中保存的值,否则尝试为该模块查找加载器 。

加载器的查找,require是由package.loaders数组来引导的,默认配置的查找如下:

首先 require 查找 package.preload[modname] 。 如果存在一个值,那它(应当是一个函数)就是那个加载器。 否则 require 使用package.path 配置的路径来查找Lua加载器。 如果也查找失败了,则使用package.cpath配置的路径去查找一个C加载器。 如果都失败了,就是用默认的all-in-one加载器 。

一旦找到一个加载器,require将调用这个加载器(参数为modname),若加载器返回任何值(非空),则require将赋值到package.loaded[modname];若没有返回值且package.loaded[modname]为空,则require将其赋值为truerequire返回package.loaded[modname]最终的值。

那么加载器会返回什么值呢?其实一般就取决于当前Lua模块的返回,加载器读入代码进行解析,当前模块可以返回一个functiontable或者不返回等。在使用waxClass时,其实一般也没有返回。

package.loaders

这是一张用来控制模块加载的表。这张表中每一项都是一个搜索函数(接受一个modname的参数),require按升序调用其中的搜索器函数,调用结果返回另一个函数(也就是模块加载器函数)或者一个错误描述字符串。Lua初始配置了4个搜索函数,正如上面require中阐述那样。(注:v5.3修改为package.searchers)

module

module (name [, ···])创建一个模块。按以下顺序创建模块:

package.loaded[name]包含一个表,否则如果有一个同名的全局表,否则创建一个表t并赋值给全局同名变量及package.loaded[name]

module这个方法还初始化了t.Name,t.M以及t._PACKAGE(分别是参数name、模块自身以及包名),最后,模块设置t作为当前函数的新的执行环境及package.loaded[name],使得require返回t

初次之外还提供了dofileloadfile相关的函数。

那可以自定义加载器吗?因为默认的加载器执行的是默认的文件读取并作为一个chuck读入执行,假如我们需要加解密脚本,这是不够的。答案也是明确的,可以自定义,这个将在wax中再介绍。另外,wax中还没有用到module

function与self

闭包

首先,什么是闭包?摘录Wikipedia上的定义如下:

In programming languages, closures (also lexical closures or function closures) are techniques for implementing lexically scoped name binding in languages with first-class functions.

中文解释是,引用了自由变量的函数称之为闭包。闭包最初是函数式编程语言实现的,现在如C这样的命令式编程语言其实也已经支持(Apple以block的形式为C添加了闭包的特性,由LLVM编译器支持)。

当Lua编译一个function时,它生成了一个包含该函数的虚拟机指令、常量值以及一些调试信息的原型。在运行时,当Lua执行一个function...end表达式的时候,它创建一个新的闭包,每个闭包包含对相应函数原型的引用、环境表(查找全局变量)的引用以及一组upvalue(用于访问外部局部变量)的引用。

对于访问外部局部变量,词法作用域与一等公民函数(闭包)的组合制造了众所周知的难题。下面的例子中,当add2函数调用时,其函数体访问了外部局部变量x,然而此时创建add2的函数add已经返回了,如果x是存在栈中的,这个变量所在栈幁理应已被回收导致访问出错。那Lua是怎样实现闭包捕获外部局部变量的?

1
2
3
4
5
6
7
function add (x) 
return function (y)
return x+y
end
end
add2 = add(2)
print(add2(5))

Lua使用了一个称之为upvalue的结构来实现的。任意的外部局部变量都是通过一个upvalue间接访问的,该upvalue最初指向局部变量所在的栈元素,当变量离开作用域后,它会从栈移到upvalue中。因为所有对这个变量的访问是间接通过指向这个upvalue的指针来实现的,因此读写对Lua是透明的。不同于内部的函数,声明该变量的函数是直接访问栈来访问局部变量的。

熟悉Objc中的block的实现的,是不是觉得有些似曾相识(关于block的实现另有说明 )。对于堆中的对象,不做说明,因为它始终都是通过指向堆上的指针来访问的,前后一致。简单提一下,对于需捕获的栈中的局部变量,用__block修饰后,事实上编译器已经将其修改为__Block_byref_{$var_name}_{$index}命名的结构体了,也是通过间接访问来实现,该结构体中保存了原始的变量值。另外该结构体中包含一个指向本身类型的指针__forwarding,用来支持__block变量从栈转移到堆内存中,其访问方式为 __blockVar->__forwording->localVar。所以,原理是相通的,增加间接层。

Lua通过保存一个仍指向栈中变量(称之为pending var)的链表来索引upvalue以供重用保证每个局部变量的upvalue唯一性。当Lua创建一个闭包时,会扫描它所有的外部变量,通过重用索引链表中的upvalue或新建一个来捕获外部局部变量。

self

self是Lua中的一个关键字,用法请参照上面的 iPhone.lua。但上面的self用法其实只是个语法糖,下面的调用是一样的:

1
2
3
4
5
6
7
8
9
Person = {"x"=0,"y"=0}
Person.foo = function(self, x)
self.x = x
print("foo")
end

local x = 100
Person.foo(Person, x) --等同于:
Person:foo(x)

在以冒号:调用的语法中, self是将调用者作为第一个隐含参数传递给函数。

C编程接口

这里主要是描述与宿主程序通信的C编程接口。主要关注几个关键的点:栈、索引、C闭包、注册表以及一些关键的API。

Lua中的虚拟机状态机:

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
struct lua_State {
CommonHeader;
lu_byte status;
StkId top; /* first free slot in the stack */
StkId base; /* base of current function */
global_State *l_G;
CallInfo *ci; /* call info for current function */
const Instruction *savedpc; /* `savedpc' of current function */
StkId stack_last; /* last free slot in the stack */
StkId stack; /* stack base */
CallInfo *end_ci; /* points after end of ci array*/
CallInfo *base_ci; /* array of CallInfo's */
int stacksize;
int size_ci; /* size of array `base_ci' */
unsigned short nCcalls; /* number of nested C calls */
unsigned short baseCcalls; /* nested C calls when resuming coroutine */
lu_byte hookmask;
lu_byte allowhook;
int basehookcount;
int hookcount;
lua_Hook hook;
TValue l_gt; /* table of globals */
TValue env; /* temporary place for environments */
GCObject *openupval; /* list of open upvalues in this stack */
GCObject *gclist;
struct lua_longjmp *errorJmp; /* current error recover point */
ptrdiff_t errfunc; /* current error handling function (stack index) */
};

这是个线程独立的参数,所有的接口调用都基于这个状态变量,这是所有C接口函数都必须接受第一个参数。

栈与索引

Lua脚本与C之间的互调必须能进行参数传递、数据通信,Lua 使用一个虚拟栈来和C数据通信,栈上的的每个元素都是一个 Lua 值(nil,数字,字符串,等等)。

当一个C函数被Lua中调用时,将得到一个新的独立于C函数的调用栈及原先Lua调用C的栈,包含了Lua传递给C函数的所有参数,且C函数可以将返回结果放入栈中返回给调用者。

C接口中对栈的操作,通常可以通过一个索引参数来指定操作栈中特定位置的元素。索引可以是1~n的正索引(n为栈大小),也可以是-n~-1的负索引,负索引表示从栈顶开始的偏移量。举例,lua_isnumber(L, i)是读取栈中第i个元素判断是否数值类型。

另外,一些接口方法隐含了入栈或出栈的操作,如lua_gettable (lua_State *L, int index);会读取index索引位置的表t,并以栈顶的值作为key,读取t[key]并将该值入栈。具体行为需要仔细阅读API说明。

lua_CFunction

typedef int (*lua_CFunction) (lua_State *L);

这是能提供Lua调用的C 函数的类型定义,除此之外还约定了参数及返回值传递方法:C函数通过上述所提的栈来接受参数,参数以正序入栈,在函数开始时可以通过lua_gettop(L)来获取函数接收到的参数个数;返回值同样正序分别压入堆栈,并返回返回值的个数。在返回值之下的栈元素将被Lua丢弃。

下面这个例子中的函数将接收若干数字参数,并返回它们的平均数与和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int foo (lua_State *L) {
int n = lua_gettop(L); /* 参数的个数 */
lua_Number sum = 0.0;
int i;
for (i = 1; i <= n; i++) {
if (!lua_isnumber(L, i)) {
lua_pushliteral(L, "incorrect argument");
lua_error(L);
}
sum += lua_tonumber(L, i);
}
lua_pushnumber(L, sum/n); /* 第一个返回值 */
lua_pushnumber(L, sum); /* 第二个返回值 */
return 2; /* 返回值的个数 */

C闭包

上面提到的,绑定自由变量的函数就是闭包。当一个C函数创建后,可以将其与某些值(称之为upvalue)绑定,生成C闭包。当C函数被调用时,它的那些upvalue位于伪索引处,这些索引由lua_upvalueindex产生,其中第n个upvalue位于lua_upvalueindex(n)的索引位置。该函数参数超过函数本身的总上值个数(但<=256)时,产生一个可接受但无效的索引。

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);这个是将C闭包压栈的接口函数。为了将值绑定到函数,需要先将这些值正序压入堆栈,然后调用上述函数创建出闭包并将这个C函数压到栈中。参数n告知需绑定的上值总数,该方法也会将这些值弹出栈。n最大值为255,当n为0时将创建一个轻量C函数,也就是一个指向C函数的指针。

void lua_pushcfunction (lua_State *L, lua_CFunction f);将一个 C 函数压栈。 这个函数接收一个C函数指针,并将一个类型为 function 的 Lua 值压栈。当这个栈顶的值被调用时,将触发对应的 C 函数。注册到 Lua 中的任何函数都必须遵循正确的协议来接收参数和返回值 (参见 lua_CFunction )。该函数作为一个宏:#define lua_pushcfunction(L,f) lua_pushcclosure(L,f,0)

注册表(registry)

Lua提供了一个预定义的表,称之为注册表(registry),提供给C来存放任意Lua值。该表可以通过伪索引LUA_REGISTRYINDEX来访问。因为是全局的,所以应当注意选择合适的key以防碰撞/覆盖。

注册表中的整数键用于引用机制,因此不应将整数键用作其他用途。

LUA_RIDX_GLOBALS则是全局表在注册表中的伪索引。

相关API

简单列举几个常用的API如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
int luaL_newmetatable (lua_State *L, const char *tname);
/*If the registry already has the key tname, returns 0. Otherwise, creates a new table to be used as a metatable for userdata, adds it to the registry with key tname, and returns 1.
In both cases pushes onto the stack the final value associated with tname in the registry.*/ 创建元表并加入注册表中

void luaL_getmetatable (lua_State *L, const char *tname);
/*
Pushes onto the stack the metatable associated with name tname in the registry (see luaL_newmetatable).*/ 从注册表查询元表

int lua_setmetatable (lua_State *L, int index);
/*
Pops a table from the stack and sets it as the new metatable for the value at the given acceptable index.*/ 设置表的元表

void lua_newtable (lua_State *L);
/*
Creates a new empty table and pushes it onto the stack. It is equivalent to lua_createtable(L, 0, 0).*/ 创建表并压栈

void lua_setfield (lua_State *L, int index, const char *k);
/*
Does the equivalent to t[k] = v, where t is the value at the given valid index and v is the value at the top of the stack.
This function pops the value from the stack. As in Lua, this function may trigger a metamethod for the "newindex" event (see §2.8).*/ 设置特定位置表的kv

void lua_pushvalue (lua_State *L, int index);
//Pushes a copy of the element at the given index onto the stack. 拷贝特定位置的值压栈

void lua_pop (lua_State *L, int n);
//Pops n elements from the stack. 出栈

void *luaL_checkudata (lua_State *L, int narg, const char *tname);
/*
Checks whether the function argument narg is a userdata of the type tname (see luaL_newmetatable)*/ 检查是否用户数据类型

void lua_pushlightuserdata (lua_State *L, void *p);
/*
Pushes a light userdata onto the stack.
Userdata represent C values in Lua. A light userdata represents a pointer. It is a value (like a number): you do not create it, it has no individual metatable, and it is not collected (as it was never created). A light userdata is equal to "any" light userdata with the same C address.*/

void *lua_newuserdata (lua_State *L, size_t size);
/*The lua_newuserdata function allocates a block of memory with the given size, pushes the corresponding userdatum on the stack, and returns the block address.*/创建userdata值

void lua_gettable (lua_State *L, int index);
/*
Pushes onto the stack the value t[k], where t is the value at the given valid index and k is the value at the top of the stack.
This function pops the key from the stack (putting the resulting value in its place). As in Lua, this function may trigger a metamethod for the "index" event */ 从特定索引的table查询值并压栈

void luaL_register (lua_State *L,
const char *libname,
const luaL_Reg *l);
/*
Opens a library.

When called with libname equal to NULL, it simply registers all functions in the list l (see luaL_Reg) into the table on the top of the stack. 若libname为空则将方法列表注册到栈顶的表

When called with a non-null libname, luaL_register creates a new table t, sets it as the value of the global variable libname, sets it as the value of package.loaded[libname], and registers on it all functions in the list l. If there is a table in package.loaded[libname] or in variable libname, reuses this table instead of creating a new one. 否则创建一个新表t将它设置为全局变量$libname及package.loaded[libname],将方法注册到t

In any case the function leaves the table on the top of the stack.*/

其他说明

可以考虑阅读Lua的源码!

Author: Jason

Permalink: http://blog.knpc21.com/ios/lua-basic/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments