[翻译]tolua官方手册

1,277 阅读6分钟

logo.gif

原文 翻译:sumsmile

译者注:为了读起来更符合中文的语法习惯,在尽量保证意思不变的前提下,根据译者的理解有适当的调整,部分专业术语英文读起来更顺畅不做翻译,如struct、get、set等。

译者英语水平有限,仅供参考。 欢迎勘误。


tolua工具大大简化了C/C++与lua代码的集成。基于一个“干净”的头文件,tolua框架能自动生成lua需要获取的C/C++属性。 使用Lua API 和 标记方法,tolua将 C/C++ 常量、external变量、函数、类、方法映射成Lua。

此手册对应tolua version 3.2,基于Lua3.2实现,相对version 3.0做了些优化。(详细改动参考文末:Changes since v3.0)


  • 内容概览:
    • tolua如何工作
    • 如何使用tolua
    • 基础概念
    • 绑定常量
    • 绑定external 变量
    • 绑定函数
    • 绑定struct 字段
    • 绑定类和方法(译者注:注意method 与 function的区别)
    • 模块定义
    • 常量、变量和函数的重命名
    • 存储附加的字段(译者注:additional字面义翻译成额外的比较准确,但是此处更体现“额外增加的变量”的含义)
    • 嵌入lua 代码
    • v2.*之后的改动
    • v1.*之后的改动
    • 致谢
    • 可用的下载

tolua如何工作(How tolua works)

首先,创建一个包文件,即一个干净的C/C++头文件,列举出需要导出到Lua环境的属性,如常量、变量、函数、类和方法等。之后,tolua会解析该文件,并新创建一个C/C++文件以绑定C/C++代码到Lua环境。最后,链接上一步创建的文件到程序中,就可以在Lua中获取已定义好的C/C++代码能力。

我们从几个例子开始。如果我们把下面C风格的头文件作为输入,提供给Lua:

#define FALSE 0
#define TRUE 1

enum { 
 POINT = 100, 
 LINE, 
 POLYGON
}
Object* createObejct (int type);
void drawObject (Object* obj, double red, double green, double blue);
int isSelected (Object* obj);

再看看C++风格的头文件:

#define FALSE 0
#define TRUE 1
class Shape
{
  void draw (void);
  void draw (double red, double green, double blue);
  int isSelected (void);
};
class Line : public Shape
{
 Line (double x1, double y1, double x2, double y2);
 ~Line (void);
};

如果该代码文件输出到tolua,自动生成的C++文件提供对应的接口供lua调用。lua中的代码可以这么写:

...
myLine = Line:new (0,0,1,1)
...
if myLine:isSelected() == TRUE then
 myLine:draw(1.0,0.0,0.0)
else
 myLine:draw()
end
...
myLine:delete()
...

传给tolua的包文件(通常以 .pkg为后缀)不是真正的C/C++头文件,但是更干净简洁。比如,因为要从外部调用C++方法,应该加public修饰,但是这里不需要public声明。tolua未实现对所有的C/C++代码的解析,但是tolua能理解少量输出到lua的属性声明。然而,获取一个精简的头文件,是重排真实头文件声明的问题。

如何使用lua(How to use toLua)

tolua由两部分组成:一个可执行文件和一个库。可执行文件即解析器,读取包文件输出C/C++代码,该代码绑定了lua要调用的C/C++接口。

tolua -n pkgname -o myfile.c myfile.pkg

应该显式的初始化包文件。在C/C++代码中初始化包文件,必须要声明、调用初始化函数。初始化函数定义为:

int tolua_pkgname_open (void);

其中,pkgname 表示被绑定的包名。如果用C++,可以选择自动初始化:

tolua -a -n pkgname -o myfile.c myfile.pkg

这样,初始化函数可以自动被调用。但是,如果你用到了多个Lua states,自动初始化就不好使了,因为C++没有定义静态变量的初始化顺序。

最新版本的tolua也输出了close函数,可以用来解绑package。

void tolua_pkgname_close (void);

或者,可以将open和close函数的原型输出到头文件,头文件的名称由-H选项指定。 tolua生成的绑定代码引用了一些列tolua library里的函数,所以,这些library也必须链接到我们的程序中。编译生成代码时,也要用到tolua.h文件。

应用程序也可以用tolua的面向对象框架(参考下文中 exported utility functions说明),那就不需要绑定任何包了,初始化的接口有区别(所有的package file 初始化函数都会调到这个接口)

int tolua_open (void);

如果用到多个Lua state,在setting 一个Lua state后,需要重置tolua 内部状态:

int tolua_open (void);

基础概念(Basic Concepts)

使用tolua的第一步是创建package文件。从真实的头文件开始,确定lua需要调用的属性,按照tolua能理解的格式清理“冗余”的声明。tolua能理解的格式是简单的C/C++声明,下面会说明。一个package 文件可能会include其他package文件,通常的格式是:$<include_file>.

基础类型(Basic types)

tolua自动映射C/C++基础类型到Lua的基础类型。char、int、float和double会被映射成number,char被映射成string,void被映射成userdata。

C/C++中的类型可能有预处理的修饰符(unsigned、static、short、const等等),注意,tolua会忽略基础类型的const修饰符。所以,如果传一个基本类型的常量到lua再回传到C/C++,会进行一次常量到非常量的转换。

C/C++中的函数也可以显示的操作Lua对象。lua_Object被看成是一个基础类型,且任何Lua值都符合。

自定义类型(User defined types)

package文件里其他的类型都被当做自定义类型,在lua中映射成userdata类型。Lua仅能存储自定义类型的指针;但是,tolua会自动对引用和值进行处理。比如,如果一个函数或方法返回一个自定义类型的值,tolua分配一个clone object并返回给lua,并且在不需要使用该对象时会设置垃圾回收标识方法,以自动释放。

对于用户自定义类型,常量被保留的。所以,传递非常量的自定义类型到接收常量的函数,会产生 mismatching错误。

NULL 和 nil

C/C++中的NULL 或 0被映射成Lua中的nil类型,反过来,nil可以接收为任意C/C++指针类型,比如char*、void或自定义类型类型,反过来,nil可以接收为任意C/C++指针类型,比如char、void或自定义类型类型,反过来,nil可以接收为任意C/C++指针类型,比如char、void*或自定义类型。。。

Typedefs

tolua也能接收简单的typedef's。任何定义的类型被tolua映射成基本类型。这种处理是有用的,因为多个packages会讲基础的C/C++类型定义成自己的类型。比如,你可以定义real类型代表double,real就可以在package中定义变量,并且被tolua解析,但是必须先进行定义。

typedef double real;

否则,real会被解析成自定义类型,而不是Lua的numbers。

导入真实的头文件

在package文件中,你必须定义要导入的真实的header files,以便生成的代码可以获取绑定的常量、变量、函数以及类。在package中,任何以$开始的行(除开$<...>,$ [, and $])在编译后都会原样保留,当然,前面的$会丢弃,我们用此特征来导入真实的头文件。所以,我们的package文件经常以一组$打头的行定义依赖的文件。

/* specify the files to be included */
$#include "header1.h"                 // include first header
$#include "header2.h"                 // include second header

如上面代码,tolua也允许C/C++里常见的评论格式。嵌套的C风格的评论也可以。

接下来的部分,阐述如何定义需要绑定到Lua的C/C++代码。采用简化的C/C++语句。很简单就能将真实的C/C++头文件转换成一个package文件。

绑定常量

tolua接受define和enum形式的常量。define通常形式如下:

#define NAME [ VALUE ]

上面的VALUE是可选的。如果这段代码插入到package文件中,tolua生成的代码中允许使用NAME作为Lua的全局变量,并且对应C/C++中的一个常量值。注意,仅能设置数值常量。

对enum常量,通用形式如下:

enum {
  NAME1 [ = VALUE1 ] ,
  NAME2 [ = VALUE2 ] ,
  ...
  NAMEn [ = VALUEn ]
};

类似,tolua创建一组全局变量,命名为NAMEi,并且有对应的values。

绑定external 变量

可以导出全局extern变量。在精简的头文件中定义如下:

[extern] type var;

tolua绑定上面格式的声明到Lua全局变量。这样在Lua中,就能很自然的获取C/C++变量。如果变量不是常量,我们也可以再Lua中给它赋一个新值。全局数组也可以绑定到Lua,数组可以是任意类型。Lua中与array对应的对象是table,以数值索引[译者注:table是Lua的集合数据类型];注意,Lua中的角标1对应C/C++中的0[译者注:Lua中数组从1开始数]。数组必须预先定义长度,如:

double v[10];

绑定函数

函数按照常规的C/C++声明来定义

type funcname (type1 par1[, type2 par2[,...typeN parN]]);

返回值可以是void,即无返回值。函数也可以没有参数,此时参数以void表示。参数类型必须遵循已发布的规则。tolua创建一个Lua函数,绑定到对应的C/C++函数。当调用Lua中的函数,参数类型必须匹配对应的C/C++类型,否则,tolua生成的错误指出哪个参数定义错误。如果省略了参数名,tolua会自动加上,但是其类型只能是基础类型或前面用到的自定义类型。

Arrays

tolua也能处理有array参数的函数或方法,比较好的是,当C/C++函数改变array的内容,对应的Lua table的值也会更改。 array必须预定义长度,比如:

void func (double a[3]);

这是合法的tolua声明,从lua中调用该函数是OK的,比如:

p = {1.0,1.5,8.6}
func (p)

array的长度不需要以常量来定义,长度也可以是表达式。比如:

void func (int n, int m, double image[n*m]);

这是合理的,因为再绑定函数域中,n * m是合理的。注意,tolua对绑定的函数采用动态内存分配机制,这会降低运行性能。

尽管array长度确定,所有传递到C/C++函数的array存在于绑定函数的局部域,所以,C/C++函数如果持有array指针以便后面使用,绑定的代码可能会异常。

重载函数(Overloaded functions)

支持重载函数。注意,映射到Lua的两个函数,它们之间的区别以参数类型来区分。所以,下面这两个C++函数在tolua中没有区别,因为Lua中 int 和 double都映射成number类型。

void func (int a);
void func (double a);

另一个棘手的情况是指针,看下面几个函数:

void func (char* s);
void func (void* p);
void func (Object1* ptr);
void func (Object2* prt);

虽然在C++中,这是4个不同的函数,但是在Lua中都映射成func(nil)

注意,使用者必须要了解在运行时,tolua具体调用哪一个函数的决策机制,它会尝试匹配每一个函数。tolua会优先调用最后一个定义的函数,如果失败了,继续尝试上一个,找到到合适的重载函数,或抵达第一个重载函数则匹配停止。基于此,如果调用有误,错误信息由第一个函数抛出。另外,如果考虑性能,我们可以把最常调用的重载函数放到最后,可以第一个被匹配。

tolua支持C语言中重载函数的使用,详细参考下文中的Renaming部分。

默认参数值(Default parameter values)

可以有默认值,调用时未传的参数会自动补充默认值。默认值的定义和C++的代码风格相同,如下:

type funcname (..., typeN-1 parN-1 [= valueN-1], typeN parN [= valueN]);

toLua的默认值用法没有使用任何C++的机制,所以也适用于绑定C函数。(译者注:C与C++的函数签名是有区别的)

也可以对array设置默认值(注意,这里指的是函数传参的场景,不能对单独的array设置默认值)。代码如下:

void func (int a[5]=0);

设置array中元素的默认值为0,这样从Lua中调用该函数可以传一个未初始化的table参数。 对Lua的object类型(lua_Object),tolua定义了个常量,以nil表示默认值:

void func (lua_Object lo = TOLUA_NIL);

多个返回值(Multiple returned values)

Lua中一个函数的返回值数量不限。tolua用这个特性来模拟按引用传值。如果参数以指针、或基础类型的引用、或指针的引用定义,tolua接受对应的类型作为输入和返回值,还会附加更新的参数值。

举例说明,下面这个C函数,交换两个值

void swap (double* x, double* y);
//or
void swap (double& x, double& y);

绑定struct字段(Binding struct fields)

自定义类型可以很好的绑定到tolua。非基础类型的变量或函数,tolua自动创建一个标记的userdata以对应C/C++的类型。如果是struct类型,可以直接在lua中获取,并能索引struct中持有object的变量。在C中,通常以typedef来定义:

typedef struct [name] {
   type1 fieldname1;
   type2 fieldname2;
   ...
   typeN fieldnameN;
} typename;

如果这段代码插入到package文件中,经处理后,tolua支持使用字段名称来索引其持有的对象。比如,var 持有上述代码中的对象,var.fieldnamei可以获取fieldnamei字段。

array字段也可以被映射:

typedef struct {
  int x[10];
  int y[10];
} Example;

绑定类和方法(Binding classes and methods)

tolua也支持C++的类定义。实际上,tolua处理单继承和多态非常自然。下面几部分阐述了哪些类型可以以类的定义导出。

定义继承(Specifying inheritance)

如果lua 变量var持有一个子类对象,当要求使用基类时也可以使用var,并且var能获取基类的任意方法(译者注,实际上不是绝对的,父类的方法可以声明不被继承)。实现多态的机制,我们必须指声明子类必须继承基类。常规的实现如下:

class classname : public basename
{
 /* class definition */
};

定义导出成员和方法(Specifying exported members and methods)

对于struct字段、类字段,无论是否是静态的,都可以被导出。类方法和类静态方法也可以被导出。当然,必须在实际的C++代码中声明(虽然public关键字不会在package文件中出现)。

对于每一个绑定的类,tolua创建一个Lua table,并存储为一个全局变量,且以C++类来命名。静态导出的字段可以通过字段名来索引(与struct 字段类似)。非静态导出字段可以通过索引持有对象的变量来获取。类方法遵循函数相同的形式。可以在Lua代码中以常规的方式调用类函数、持有对象的变量、或类表,或静态方法。

有些特殊的方法在tolua中也能很好的支持。构造函数以静态方法调用,使用new关键字。析构函数通常以delete来调用。

注意,tolua支持的重载同样也适用于构造函数。另外需要说明,virtual关键字在package中无效。

下面的代码列举了能被tolua解析的类定义

class Point {
   static int n;    // represents the total number of created Points

   double x;        // represents the x coordinate
   double y;        // represents the y coordinate
   static char* className (void);   // returns the name of the class
   Point (void);                          // constructor 1
   Point (double px, double py);          // constructor 2
   ~Point (void);                         // destructor
   Point add (Point& other);              // add points, returning another one
};
class ColorPoint : public Color {
   int red;      // red color component [0 - 255]
   int green;    // green color component [0 - 255]
   int blue;     // blue color component [0 - 255]
   ColorPoint (double px, double py, int r, int g, int b);
};

对应tolua中的调用:

p1 = Point:new(0.0,1.0)
p2 = ColorPoint:new(1.5,2.2,0,0,255)
print(Point.n)                     -- would print 2
p3 = p1:add(p2)
print(p3.x,p3.y)                   -- would print 1.5 and 3.2
print(p2.red,p2.green,p2.blue)     -- would print 0, 0, and 255
print(
p1:delete()                        -- call destructor
p2:delete()                        -- call destructor

注意:只有显示创建的对象可以显示的删除,如上面的例子中,point p3 可以被tolua自动的回收,但是不能delete。

当然,我们仅需要定义Lua中获取的方法和成员。有时候,为了不破坏继承链,也有必要声明一个无成员、无方法的类。

运算符重载(Overloaded operators)

tolua自动绑定下面这些二元操作符:

operator+   operator-   operator*   operator/ 
operator<   operator>   operator<=  operator>=

对于相关的操作符,toLua会自动的将返回的0值转为nil,所以,C中的false在Lua中也是false。

以上面Point代码为例,替换

Point add (Point& other);              // add points, returning another one

Point operator+ (Point& other);        // add points, returning another one

这样,在Lua中,可以更简单的实现相加:

p3 = p1 + p2

如果以数值为参数,索引操作符(operator[ ])也可以导出到Lua,这样tolua支持返回值为引用,基础类型也同样适用。如果在Lua中返回引用,开发者可以进行get或者set操作。举例说明:假设有个vector类,且绑定了下面的操作符:

double& operator[] (int index);

本例中,对应的Lua代码可以写:value = myVector[i] 也可以写myVector[i] = value,后面的赋值操作会更新C++中的对象值。如果绑定的操作符是:

double operator[] (int index);

只能写:value = myVector[i] 自由函数(即非类成员函数)重载操作符无效。

模块定义(Module definition)

tolua允许在module中组织常量、变量、函数。module本身被映射成Lua中的table,常量、变量和函数映射成table中的字段。通常定义一个module的形式是:

module name
{
      ... // constant, variable, and function declarations
}

如果绑定了下面这个module声明:

module mod
{
 #define N
 extern int var;
 int func (...):
}

那么,在Lua中,可以通过索引获取字段,操作代码如:mod.N,mod.var,mod.func。

常量、变量和函数的重命名(Renaming constants, variables and functions)

对导出的常量、变量和函数(类成员函数或者非类成员函数都可以),可以重命名。重命名后,这些属性在C/C++中有不同的名称映射。以@符号标识新名称,举例:

extern int cvar @ lvar;

#define CNAME @ LNAME

enum {
  CITEM1 @ LITEM1,
  CITEM2 @ LITEM2,
  ...
};

void cfunc @ lfunc (...);

class T
{
   double cfield @ lfield;
   void cmeth @ lmeth (...);
   ...
};

上例中,全局变量cvar在lua中以lvar表示,常量CNAME以LNAME表示,等等。注意,class在C中表示类型,所以不能被重命名。

基于重命名,可以实现C语言函数的重载,因为可以用两个C函数映射同名的Lua函数:

void glVertex3d @ glVertex (double x, double y, double z=0.0);
void glVertexdv @ glVertex (double v[3]=0.0);

存储额外的字段(Storing additional fields)

最后,比较重要的一点是,在Lua中通过标记userdata来持有C/C++对象,tolua支持对object新增额外的字段,换句话说,这些对象可以被看作常规的Lua table数据。

obj = ClassName:new()
obj.myfield = 1  -- even though "myfield" does not represent a field of ClassName

上面的构造方法是合理的,因为tolua会自动创建与对象关联的Lua table。所以,该对象可以存储对应C/C++中没有的字段,存放于复合的table中。Lua开发者可以按常规的方式获取C/C++或新增的字段。注意,如果对象新增的字段和C/C++中已有的字段命名相同,会覆盖掉原字段。

导出工具函数(Exported utility functions)

tolua本身可以导出一些工具函数,包括自带的面向对象框架。tolua使用的package文件如下:

module tolua
{
 void tolua_using @ using (lua_Table module);
 char* tolua_type @ type (lua_Object lo);
 void tolua_foreach @ foreach (lua_Object lo, lua_Function f);
 void tolua_class @ class (lua_Table derived, lua_Table base=TOLUA_NIL);
 void tolua_instance @ instance (lua_Table instance, lua_Table classobj);
 lua_Object tolua_base @ base (lua_Object lo);
}

tolua.using (table)

该函数接收一个table,并映射其所有的属性为到全局环境。这么做可以映射整个module,不用加module前缀就可以获取该module所有的属性。如下面代码:

tolua.using(tolua)

映射所有的tolua工具函数到全局环境中。

tolua.type (var)

返回一个代表对象类型的字符串。如,tolua.type(tolua)返回tolua.type(tolua)返回泛型模块的字符串表示,tolua.type(tolua.type)返回cfunction。类似的,如果var是用户自定义类型T的变量,tolua.type(var)返回const T 或者 T,具体取决于是否是常量引用。

tolua.tag ("type")

返回类型对应的标识值

tolua.foreach (object)

支持遍历用户自定义实例的符合table。如果用于常规的table,与Lua内置的foreach函数功能相似。区别是tolua.foreach会过滤所有以" . "开头的字段。这个过滤是有必要的,因为tolua增加了一些隐藏字段到table,且所有增加的隐藏字段以" . "开头。

tolua.cast (object, "typename")

输入一个object,返回转换后的类型。该object必须是一个自定义类型,否则返回nil。

tolua.takeownership (object)

请求tolua接管给定object的生命周期。即C/C++对象会随着Lua的垃圾回收机制释放或者析构。该object必须是自定义类型,否则会报错。

tolua.class (table, base=nil)

通过table来创建类,table中可以设置合适的标记函数。创建的类可以继承已有的基类。

tolua.instance (table, class)

tolua.instance (table, class)

将table生成class的实例。这种形式很好的支持了面向对象编程。举例如下:

-- define a Point class
classPoint = { x=0, y=0 }
tolua.class(classPoint) -- set as a class

-- define print method
function classPoint:print ()
   print(self.x,self.y)
end

-- define add method
function classPoint:add (p2)
   return Point{x=self.x+p2.x,y=self.y+p2.y}
end

-- define a Point constructor
function Point (p)
   tolua.instance(p,classPoint) -- set as an instance of classPoint
return p end

-- define a Color Point class
classColorPoint = { color = 'black' }
tolua.class(classColorPoint,classPoint) -- set as class inheriting from classPoint

-- define class methods
function classColorPoint:print ()
   print(self.x,self.y,self.color)
end

-- define Color Point constructor
function ColorPoint (p)
   tolua.instance(p,classColorPoint) -- set as an instance of classColorPoint
   return p
end

-- Some valid codes would then be
p = Point{x=1}
q = ColorPoint{x=2,y=3,color=2}
r = p:add(q)
r:print() --> would print "3 3"

嵌入Lua代码(Embedded Lua code)

$[

embedded Lua code
...

$]

以下面节选的.pkg文件内容为例:

/* Bind a Point class */
class Point
{
 Point (int x, int y);
 ~Point ();
 void print ();
 ...
} CPoint;
$[

-- Create a Point constructor
function Point (self)
 local cobj = CPoint:new(self.x or 0, self.y or 0)
 tolua.takeownership(cobj)
 return cobj
end

$]

绑定这段代码,可以实现下面的功能,算是一个小技巧吧~

p = Point{ x=2, y=3 }
p:print()
...

v. 3.0改动

支持绑定array作为变量和struct/class字段; 支持将Lua代码嵌入到生成的绑定代码中; 新增功能函数:cast 和 takeownership; 可选择创建绑定代码的头文件; 新增package函数"close"; 修复clone C++对象的bug; 修复枚举和struct解析的bug;

v. 2.0改动

新增加一个可执行的解析器; 支持多个Lua state; 支持多个module定义; 全局变量直接绑定到Lua的全局变量; Lua中保留自定义类型的常量; 支持C/C++的多个返回值(模拟按引用传参); 绑定到Lua的常量、变量和函数,可以与C/C++取不同的命名; tolua中使用的面向对象框架(和其他的功能函数)导出到Lua,供开发者使用;

不兼容

基于tolua v2.*的代码与tolua v3.0应该是兼容的,无需改动。但是,为保证相同的运行,有必要对.pkg文件做些修改。存在以下不兼容点:

指向基础类型的指针参数不在转换成一维数组;现在以引用的方式传参。 使用新的解析器时,自动初始化C++代码功能必须显示的请求; 全局变量不再映射成table; 包含全局变量的模块定义可用于模拟旧行为 初始化函数由"toLua_package_open"改成"tolua_package_open",没有大写的字母了(sorry!)。

v. 1.*改动

绑定代码的运行速度更快; 清洁的头文件扩展名由" .L "改为 " .pkg "; 支持类型修饰符(当前版本忽略const); 支持返回对象值,内存分配由Lua垃圾回收控制 支持重载函数/方法; 支持默认值参数; 自动绑定部分重载操作符;

致谢

Luiz Henrique de Figueiredo 想到了创建工具来自动绑定C代码到Lua。L.H.F用awk写了最早的实现版本(绑定C函数、变量和常量)。同时,我负责写实现生成绑定逻辑的C代码。

可用的下载

tolua可自由下载,下面提供的软件为原文件,作者无义务去提供维护、支持、更新、增强或修改。


软件和页面由Waldemar Celes创建和维护。 上次更新:1999年7月

参考(译者补充)