Lua Generic Call

lua-users home
wiki

引言

使用标准的 Lua API,在传递输入参数并检索输出结果的同时调用一个块函数需要大量的代码,并且需要对 Lua 堆栈有完整的了解。例如,这里是如何要求 Lua 执行 3 乘以 2.5 的乘法,同时检查可能的语法和运行时错误。
const char* errmsg = NULL;
double result;
lua_settop(L, 0);
if(luaL_loadstring(L, "local a,b = ...; return a*b"))
  errmsg = lua_tostring(L, -1);
else
{
  lua_pushinteger(L, 3);
  lua_pushnumber(L, 2.5);
  if(lua_pcall(L, 2, 1, 0))
    errmsg = lua_tostring(L, -1);
  else
    result = lua_tonumber(L, -1);
}
通用调用功能主要旨在简化此类调用任务。它允许用单个函数调用替换之前的所有代码。
double result;
const char* errmsg = lua_genpcall(L, "local a,b = ...; return a*b",
  "%d %f > %lf", 3, 2.5, &result);
第二个目标是将 API 中需要的函数数量最小化,理想情况下只剩一个。这可以简化 Lua 共享库的动态加载,因为对于每个导出的函数,您都需要定义函数原型的类型,实例化该类型的变量,并调用 GetProcAddressdlsym。但是,对于这项工作,最好遵循 EasyManualLibraryLoad 指南。

源代码可以在这里找到 [lgencall.zip]

变体

实际上定义了通用调用函数的四个版本,但通常您只使用其中一个。与 Windows 操作系统下的所有包含文件一样,编译开关会自动将 lua_gencall 重定向到 lua_gencallAlua_gencallW,具体取决于是否定义了 UNICODE。也可以在不进行宽字符支持的情况下编译库文件。

通用形式

这些函数中的每一个都有一个可变数量的参数,以及三个固定参数。
1. lua_State* L:指向 Lua 状态实例的指针。如果指针为 NULL,则函数会自动调用 lua_newstate 来创建一个新实例,并且还会调用 lua_close 来释放其内存,除非该实例的指针是通过 %S 选项检索的(见下文)。如果状态被释放并且在此之前发生了错误,函数会分配一个缓冲区来复制错误消息,该缓冲区必须使用 free 关闭。
2. 脚本字符串:它包含要执行的 Lua 代码片段。它通常以参数检索开始:local var1, var2, var3 = ...;,并以返回结果结束:return res1, res2, res2。如果指针为 NULL,则相当于空脚本 ""。
3. 格式字符串:一个类似于 printfscanf 格式字符串的字符串,使用 % 字符来描述输入和输出值的变量类型。如果指针为 NULL,则相当于空格式 ""。
4. 零个或多个值参数。输入参数按值传递,而输出结果必须通过传递变量的地址来检索。分配选项也可能改变变量的预期类型。

出于性能原因,存在一个已编译代码块的缓存。它实现为 Lua 注册表中 Lua 表,以代码块字符串为索引。因此,如果您多次调用具有相同脚本字符串的 lua_genpcall,它只会在第一次编译。所有后续调用都将重用缓存版本。缓存表不是弱表,以避免由于垃圾回收而多次重新编译相同的脚本。缺点是当脚本代码块在运行时可以任意更改时,缓存可能会无限增长。例如,这可能发生在服务器解释器执行来自客户端程序的 Lua 代码块时。但是,您可以显式清除缓存,方法是在格式字符串中指定它。

printf 甚至更与 scanf 一样,您必须非常小心参数的类型和相应的格式规范。任何不匹配都可能导致意外结果,甚至更糟,导致应用程序崩溃。

格式字符串的通用形式如下:

"[ Directives < ] Inputs [ > Outputs ]"
在此通用形式中,指令输入输出是类似于 printfscanf 格式的字符串,由以百分号开头的格式项组成。指令输出是可选部分。如果它们存在,则必须用 <> 字符与输入分隔。输入也可以是空字符串。

输入和输出格式字符串

与标准 printf 格式规范一样,每个输入或输出格式项最多包含 6 个字段,如下例所示。指令项的选项较少,稍后将进行解释。任何空白字符(空格、制表符、回车符和换行符)都将被忽略,并可用于提高格式字符串的清晰度。其他字符要么按本章所述进行解释,要么在无效时引发 Lua 错误。
%#12.4Ls
1. 一个强制性的百分号('%')
2. 一个可选的标志参数('#')
3. 一个可选的宽度参数('12')
4. 一个可选的精度参数('.4')
5. 一个可选的大小修饰符('L')
6. 一个强制性的转换字符('s')

标志参数可以是以下字符之一:

宽度参数与字符串、字符串列表和数组一起使用。它表示内存缓冲区中的元素或字符数。它可以是以下形式之一:

精度参数用于数值类型,以指示 C 类型的大小(以字节为单位)。这对于数值数组和输出值很重要。因为在这两种情况下,传递的是指向变量的指针而不是值本身。它具有以下形式之一:

大小修饰符参数是数值精度值的替代方法,它改变了值的预期 C 类型。它可以是以下字符之一。请参考下面的大小表进行对应。

最重要的是,转换字符指定了数据类型。它必须是以下字符之一:

对于数值,下表列出了默认和修改后的底层 C 类型:

         (default)               'hh'       'h'       'l'           'L'
                                                                   
'f'      float                   ---        float     double        long double (*)
                                                                   
'd','i'  int                     char       short     long          int64_t (*)
                                                                   
'u'      unsigned                unsigned   unsigned  unsigned      uint64_t (*)
         int                     char       short     long         
                                                                   
'b'      C89: int                ---        char      int           ---  
         C99: _Bool                                                
         C++: bool                                                 
                                                                   
's','z'  gencallA: char*                                           
         gencallW: wchar_t* (*)  ---        char*     wchar_t* (*)  ---
(*) 如果您的编译器支持并且在编译时启用。

其他对象值的关联 C 类型如下:

最后,对于每个参数,其预期类型取决于它是输入还是输出方向,以及它的宽度和标志参数。设 TYPE 为前面两表中所述的基本 C 类型。除了 'n' 和 'k' 转换字符外,复合类型为:
Width argument    (none)            number, '*' or '&'   number, '*' or '&'
                                    
Flag              (don't care)      (none)               '#' or '+'

                                                         
Input             TYPE              const TYPE*          const TYPE*
                                                         
Output            TYPE*             TYPE*                TYPE**

指令格式字符串

在格式字符串的指令部分,支持以下转换字符(全部为大写):

源代码

版权

Lua Generic call 库已根据与 Lua 本身相同的 MIT 许可证进行许可。这意味着,尽管该库受 Olivetti Engineering SA 的版权保护,但它是免费软件,可用于学术和商业目的,完全免费。

源文件

库发行版仅包含一个 C 实现文件 lgencall.c 和一个头文件 lgencall.h。还有一个测试文件 testwin.cpp,它包含了下一章的所有测试示例,包括 Windows 头文件 tchar.h。使用此实用程序头文件,可以编写同时适用于 ANSI 和 Unicode 平台的 C++ 代码。

主 C 文件包含 ANSI 标准文件以及公共 Lua API 头文件。与其他标准 Lua 库一样,不使用私有功能,并且该文件可以在 C 和 C++ 语言中进行编译。但是,它需要新的 C99 头文件 stdint.h 来定义固定大小的整数。如果您的编译器不支持此功能,则可以在 WWW 上找到几个免费版本。 [1] [2]

源文件可以与应用程序一起编译,或者放置在 Lua 共享库中,如果您愿意重新编译它。

编译开关

在头文件 lgencall.h 中定义了 3 个编译宏,用于自定义库以适应您的平台。每个参数都可以通过修改文件本身或在编译器的命令行中指定。头文件中对此进行了简要说明,列出了可能的值。此外,编译还会受到以下标准宏的影响:__cplusplusINT_MAXUINT_MAX__STDC_VERSION__

示例

指令元素

当然,示例将帮助您理解各种功能。第一个示例展示了各种指令格式的用法。让我们从最简单的开始,当然是一个 "Hello World" 程序。
lua_genpcall(NULL, "print 'Hello World!'", "%O<");
这里没有将 Lua 状态传递给函数,因此它会自动分配。指令部分中的 %O 指示打开标准库,包括全局 print 函数。由于未传递 %S 选项,Lua 状态会在调用结束时被释放。我们没有测试返回值,它是一个错误消息。

这是同一个示例,但作为更完整、更逼真的实现。

lua_State* L = lua_open();
luaL_openlibs(L);
const char* errmsg = lua_genpcall(L, "print 'Hello World!'", "");
if(errmsg)
  fprintf(stderr, "Lua error: %s\n", errmsg);
lua_close(L);
在这里,Lua 状态被手动创建,填充了标准库并被释放。如果出现问题,将测试并打印返回的错误消息。

再次是同一个示例,但仅使用 lua_genpcall

lua_State* L;
lua_Alloc falloc;
lua_genpcall(NULL, NULL, "%O %S %&M<", &L, &falloc);
char* errmsg = lua_genpcall(L, "print 'Hello World!'", "%C<");
if(errmsg)
{
  fprintf(stderr, "Lua error: %s\n", errmsg);
  falloc(NULL, errmsg, 10, 0);
}
第一次调用将分配一个新的 Lua 状态,打开标准库(%O),返回 Lua 状态(%S)以及内存分配函数(%&M)。第二次调用将打印消息并销毁 Lua 状态(%C)。因此,错误消息(如果存在)将使用 Lua 分配函数分配,而不是从堆栈中获取。因此,最好使用相同的函数释放它(将 0 作为新大小传递)。

输入元素

将有 6 个关于如何将数据从 C 输入到 Lua 的示例。
1. 数字
2. 布尔值、nil、简单字符串和轻量级用户数据
3. C 函数和回调
4. 数值数组
5. 高级字符串
6. 字符串列表
对于所有这些示例,我们将假定 Lua 状态已打开,并且将在最后关闭。我们不测试返回值以简化编码。

1. 数字

lua_genpcall(L, "for k,v in pairs{...} do print(k, type(v), v) end", 
  "%i %d %u %f %f", -4, 0xFFFFFFFF, 0xFFFFFFFF, 
  3.1415926535f, 3.1415926535);
-->
1       number  -4
2       number  -1
3       number  4294967295
4       number  3.1415927410126
5       number  3.1415926535
脚本代码块遍历参数,并为每个参数打印其类型和值,以及索引。这里传递了五个数值参数:三个整数、一个浮点数和一个双精度浮点数(在 Lua 中都视为 number 类型)。由于 Lua 中的所有数字都存储为 double,因此浮点参数 Pi 的值存在截断误差。请注意 %d%u 对于值 0xFFFFFFFF 的行为差异。对于 double 参数,在这里不必指定 %lf 而不是 %f,因为在 C 中将浮点数传递给可变参数函数时,它们总是会被转换为 double。小于 int 的整数也会自动转换为 int

2. 布尔值、nil、简单字符串和轻量级用户数据

lua_genpcall(L, "for k,v in pairs{...} do print(k, type(v), v) end", 
  "%b %b %n %s %p", 0, 1, "Hello", L);
-->
1       boolean false
2       boolean true
4       string  Hello
5       userdata        userdata: 0096CE70
布尔值可以是 0 或 1,或者如果编译为 C++ 或 C99 语言,则为 truefalsenil 参数 %n 仅存在于格式字符串中,没有关联的参数(因为它没有被打印,因为 pairs 函数会跳过 nil 值)。这里假设字符串是零终止的,而最后一个参数 L(Lua 状态)只是一个通用指针的示例。

3. C 函数和回调

int cFunction(lua_State* L)
{
  printf("%s\n", luaL_checkstring(L, 1));
  return 1;
}
void pushMessage(lua_State* L, const void* ptr)
{
  lua_pushstring(L, *(const char**)ptr);
}
...
lua_genpcall(L, "local fct, msg = ...; fct(msg)", 
  "%c %k", cFunction, pushMessage, "Hello from C!");
第一个 Lua 参数是函数类型,并作为指向 cFunction 的指针传递。第二个参数是回调参数,由用户函数 pushMessage 和一个字符串组成。请注意,回调函数接收参数的指针而不是参数本身!

4. 数值数组

short array[] = { 1,2,3 };
lua_genpcall(L, 
  "for k,v in pairs{...} do print(k, #v, table.concat(v, ', ')) end", 
  "%2hd %5.1u %*.*d", array, "Hello", 
  sizeof(array)/sizeof(array[0]), sizeof(array[0]), array);
-->
1       2       1, 2
2       5       72, 101, 108, 108, 111
3       3       1, 2, 3
代码块打印参数索引、数组的长度以及包含其值的列表。对于数组数据,必须指定精度或大小修饰符(除非它是默认类型)和宽度值。第一个参数声明宽度为 2,因此只接收 short 数组的前两个数字。第二个参数是字符串(char[]),因此其精度为 1,宽度为字符串长度。对于第三个参数,宽度和精度都通过参数列表传递,因为格式指定了 '*'。

5. 高级字符串

unsigned char data[] = { 200, 100, 0, 3, 5, 0 };
lua_genpcall(L, "for k,v in pairs{...} do print(k, v:gsub('.', "
  "function(c) return '\\\\'.. c:byte() end)) end", 
  "%s %6s %*s %ls", "Hello", "P1\0P2", sizeof(data), data, L"�t�"); 
-->
1       \72\101\108\108\111     5
2       \80\49\0\80\50\0        6
3       \200\100\0\3\5\0        6
4       \195\169\116\195\169    5 
字符串不一定是 char 的零终止数组。这里脚本代码块打印参数索引,然后打印字符串,其中每个字节都替换为反斜杠和其十进制值。请注意,反斜杠必须两次转义:首先是 C(传递给 Lua 的代码块是 ... return '\\' ...),然后是 Lua。第一个参数是零终止的字符串;第二个是包含二进制 0 的字符串,由其长度指定。第三个参数是二进制数据数组,其长度通过参数传递。最后一个参数是宽字符字符串,它被转换为 UTF-8 字符串或其他形式的多字节字符串,具体取决于模块的编译方式。

6. 字符串列表

lua_genpcall(L, 
  "for k,v in pairs{...} do print(k, #v, table.concat(v, ',')) end",
  "%z  %7z %hz %*lz", "s1\0s2\0s3\0", "s4\0\0s5\0", 
  "c1\0c2\0c3\0", 7, L"w1\0\0w2\0"	);
-->
1       3       s1,s2,s3
2       3       s4,,s5
3       3       c1,c2,c3
4       3       w1,,w2
C 端的字符串数组应该是一个零终止的零终止字符串列表。换句话说,它是一个包含一个或多个额外零字符的字符串,用于分隔元素。因此,不可能支持包含嵌入零的字符串。在第一个示例中,未指定宽度,因此字符串列表自动以第一个双零字节结束。第二个列表的第二个字符串元素长度为 0。在这种情况下,如果格式字符串中未提供宽度,则数组将错误地为长度 1,因为中间有两个连续的零字节。通过指定宽度为 7(因此不包括最后一个零字节,如同普通字符串一样),正确接收的数组元素数量为 3。第三个示例仅指定字符串列表的类型为 char*。否则,在 Unicode 支持下,lua_genpcallW 期望宽字符字符串。最后一个列表是宽字符版本,它通过额外的 '*' 参数指定其长度。您肯定已经注意到字符串和字符串列表参数之间的强烈相似性。

输出元素

在输出模式下,主要区别在于我们通常必须传递变量的指针而不是值,并且我们应该始终指定精度字段。注意:在 Intel 等小端处理器上,传递错误的精度值可能仍然有效;但肯定会在大端平台上失败!这里有 6 个示例,演示了与输入元素相同的数据类型。

1. 数字

char var1; unsigned short var2; int var3;
float var4; double var5;
lua_genpcall(L, "return 1, 2, 3, 4, 5", ">%hhd %hu %d %f %lf", 
  &var1, &var2, &var3, &var4, &var5);
printf("%d %u %d %f %f\n", var1, var2, var3, var4, var5);
-->
1 2 3 4.000000 5.000000
此示例检索了 5 个不同类型和大小的数值。第二个变量是无符号的,var1var3 是有符号整数,最后两个是浮点数。在这种情况下,%lf 格式是强制性的!

2. 布尔值、nil、简单字符串和轻量级用户数据

bool bool1; int bool2; 
const char* str; void* ptr;
lua_genpcall(L, "return true, false, 'dummy', 'Hello', io.stdin", 
  ">%hb %lb %n %+s %p", &bool1, &bool2, &str, &ptr);
printf("%d %d %s %p\n", bool1, bool2, str, ptr);
-->
1 0 Hello 00975598
在此示例中,检索到的前两个参数是两个布尔值,类型为 C 的不同类型。第三个返回值因 %n 格式而被丢弃。通过 %+s 习语,通过 Lua 堆栈接收到 'Hello' 字符串。最后一个返回值,一个用户数据值,通过地址获取到通用指针中。

3. C 函数和回调

void getMessage(lua_State* L, int idx, void* ptr)
{
  *(const char**)ptr = lua_tostring(L, idx);
}
...
lua_CFunction fct;
const char* msg;
lua_genpcall(L, "return print, 'Hello World!'", 
  ">%c %k", &fct, getMessage, &msg);
lua_pushstring(L, msg);
fct(L);
此示例是实现 Hello World 程序的一种复杂方式。第一个返回值是指向 Lua 注册的 C 函数(全局 print 函数)的指针。第二个值是一个简单字符串,通过回调函数检索,该回调函数在 ptr 参数中接收 msg 变量的地址。然后,我们可以通过将消息推送到 Lua 堆栈并直接通过 C 调用 print 函数来打印消息(这在正常情况下肯定不是一个好习惯)。

4. 数值数组

unsigned int int_a[3];
bool bool_a[4];
char* str; 
short* pshort;
int short_len;
int bool_len = sizeof(bool_a)/sizeof(bool_a[0]);
lua_genpcall(L, "return {1,2,3,4},{72,101,108,108,111,0}, {5,6,7}, {false,true}", 
  ">%3u %+.1d %#&hd %&.*b", &int_a, &str, &short_len, &pshort, 
  &bool_len, sizeof(bool_a[0]), &bool_a);
printf("int_a = {%u,%u,%u}\nstr = %s\npshort[%d]=%d\nbool_a = #%d:{%d,%d,%d,%d}\n", 
  int_a[0], int_a[1], int_a[2], str, short_len-1, pshort[short_len-1],
  bool_len, bool_a[0], bool_a[1], bool_a[2], bool_a[3]);
free(pshort);
-->
int_a = {1,2,3}
str = Hello
pshort[2]=7
bool_a = #2:{0,1,204,204}
第一个数组在 C 堆栈上分配,并由 Lua 填充最多 3 个值(最后一个被丢弃)。第二个数组在 Lua 堆栈上分配,并且由于其类型为 char,其精度设置为 1。请注意,由于存在 '+' 符号,因此无需指定宽度。在第三个数组中,除了由当前 Lua 分配函数(由 '#' 指示)分配的指针外,还会返回实际长度(因为 '&')。使用后我们必须释放此缓冲区。在第四个布尔数组中,精度通过 '*' 功能传递。更有趣的是,宽度参数通过 '&' 符号在输入和输出方向上都进行了传递。bool_len 的值必须在调用之前初始化为数组的大小,因为我们使用的是 C 堆栈分配的缓冲区。由于缓冲区比返回的数组大,其最后两个值将保持未初始化。

5. 高级字符串

const char *str1;
char *str2;
char str3[10];
unsigned char data[6];
int len = sizeof(data);
wchar_t* wstr;
lua_genpcall(L, "return 'Hello', ' Wor', 'ld!', '\\0\\5\\200\\0', 'Unicode'",
  ">%+s %#s %*s %&s %+ls", &str1, &str2, sizeof(str3), str3, &len, data, &wstr);
printf("%s%s%s\ndata (%d bytes): %02X %02X %02X %02X %02X\nwstr = %S\n", 
  str1, str2, str3, len, data[0],data[1],data[2],data[3],data[4], wstr);
free(str2);
-->
Hello World!
data (4 bytes): 00 05 C8 00 00
wstr = Unicode
此示例以不同方式检索五个字符串。第一个字符串从 Lua 堆栈中获取('+' 符号)。第二个由 Lua 当前分配函数分配,因此在使用后必须释放。第三个从 C 堆栈中获取,并且缓冲区大小通过 '*' 宽度规范传递。下一个返回值在 Lua 端被视为字符串,但在 C 中定义为原始字节缓冲区。通过 '&' 机制,我们通过初始化变量 len 来设置缓冲区大小,并在调用后获取实际数据大小。请注意,总是有一个额外的零字节被复制到目标缓冲区(如果空间足够)。最后一个值是一个宽字符字符串,放在 Lua 堆栈上。

6. 字符串列表

void print_string_list(const char* title, const void* data, int fchar){
  printf("%-4s = {", title);
  if(fchar) {
    const char* str = (const char*)data;
    while(*str){
      printf("'%s', ", str);
      str += strlen(str) + 1;
    }
  }
  else {
    const wchar_t* str = (const wchar_t*)data;
    while(*str) {
      printf("'%S', ", str);
      str += wcslen(str) + 1;
    }
  }
  printf("}\n");
}
�
const char *str1;
char *str2;
char str3[10];
int len;
wchar_t* wstr;
lua_genpcall(L, "return {1,2,3},{4,5,6},{10,9,8,7},{11,12}",
  ">%+hz %+&z %*z %#lz", &str1, &len, &str2, 
  sizeof(str3)/sizeof(str3[0]), &str3, &wstr );
print_string_list("str1", str1, 1);
print_string_list("str2", str2, 1);
printf("len = %d\n", len);
print_string_list("str3", str3, 1);
print_string_list("wstr", wstr, 0);
free(wstr);
-->
str1 = {'1', '2', '3', }
str2 = {'4', '5', '6', }
len = 6
str3 = {'10', '9', '8', '7', }
wstr = {'11', '12', }
在这个最后的示例中,辅助函数 print_string_list 只是为了以可读的方式显示检索到的字符串列表。第一个字符串列表类型为 char*,无论是否使用 Unicode 版本,而 str2str3 则不是。第一个列表在 Lua 堆栈上分配;第二个列表也是如此,此外还检索了字符串列表的长度(不包括最后一个零字节)。str3 的缓冲区在 C 堆栈上;因此,其大小作为通过 '*' 标志指示的附加参数传递。最后一个字符串列表是宽字符版本;其缓冲区使用 Lua 分配器分配,需要在使用后调用 free。您一定已经注意到了字符串和字符串列表参数之间的强烈相似性。

-- PatrickRapin


RecentChanges · preferences
编辑 · 历史
最后编辑于 2010 年 1 月 10 日上午 10:04 GMT (差异)