Lua 通用调用

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 ]"
在一般形式中,DirectivesInputsOutputs 是类似于 printfscanf 格式的字符串,由以百分号开头的格式项组成。DirectivesOutputs 是可选部分。如果存在,则必须用 <> 字符与 Inputs 分隔。Inputs 也可以是空字符串。

输入和输出格式字符串

与标准 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 为前 2 个表中所述的基本 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 通用调用库已与 Lua 本身采用相同的 MIT 许可证。这意味着,尽管该库受 Olivetti Engineering SA 的版权保护,但它是免费软件,可用于学术和商业目的,完全免费。

源文件

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

主 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 语言中编译时为 truefalse。**nil** 参数 **%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 个不同类型和大小的数值。第二个变量是无符号的,`var1` 和 `var3` 是有符号整数,最后两个是浮点数。在这种情况下,**%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** 格式而被丢弃。一个 `'Hello'` 字符串通过 Lua 堆栈使用 **%+s** 语法接收。最后一个结果值,一个用户数据值,通过地址获取到一个通用指针。

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 版本,而 `str2` 和 `str3` 则不是。第一个列表分配在 Lua 堆栈上;第二个列表也是,此外还检索了字符串列表的长度(不包括最后一个零字节)。`str3` 的缓冲区位于 C 堆栈上;因此,它的尺寸作为附加参数传递,由 '**' 标志指示。最后一个字符串列表是宽字符版本;它的缓冲区使用 Lua 分配器分配,需要在使用后调用 `free`。您一定已经注意到字符串和字符串列表参数之间的强烈相似性。

-- PatrickRapin


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2010 年 1 月 10 日下午 4:04 GMT (差异)