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 作業系統下的所有 include 檔案一樣,編譯開關會根據是否定義 UNICODE,自動將 lua_gencall 重新導向至 lua_gencallAlua_gencallW。也可以在沒有寬字元支援的情況下編譯函式庫檔案。

一般形式

這些函數各有變數數目的參數,以及三個固定參數。
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 型態,如同前 2 個表格中所述。除了 '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
指令片段會重複執行參數,並為每個參數列印其類型和值,只要與索引相關即可。在此,傳遞了 5 個數值參數:三個整數、一個浮點數和一個雙倍浮點數 (所有都視為 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 端零終止字串的零終止清單。換句話說,它是一個字串,內含一個或更多額外的 null 字元,用來作為元素的區隔符號。因此,無法支援含有內嵌零的字串。在第一個範例中,未指定寬度,因此字串清單會自動於第一個雙零位元組結束。第二個清單的第二個字串元素長度為 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 是帶正負號的整數,最後 2 個是浮點數。在此情況下,%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 函式。第二個值(簡單字串)會透過 callback 函式擷取,其會接收變數 msg 的地址作為其 ptr 參數。然後,我们可以通過將訊息推送到 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


最新變更 · 喜好設定
編輯 · 歷史
最後編輯於 2010 年 1 月 10 日下午 4:04 GMT (diff)