快速掌握Lua 5.3 —— 從Lua中調用C函數


Q:Lua調用C函數的兩種方式?

A:
1、程序主體在C中運行,C函數注冊到Lua中。C調用Lua,Lua調用C注冊的函數,C得到函數的執行結果。
2、程序主體在Lua中運行,C函數作為庫函數供Lua使用。
第一種方式看起來很羅嗦,也很奇怪。既然程序主體運行在C中,而且最終使用的也是C中定義的函數,那么為何要將函數注冊給Lua,然后再通過Lua調用函數呢?
相比於第一種方式,第二種方式使用的更加普遍。
一個Lua庫(Lua本身所提供的庫)實際上是一個定義了若干Lua函數的”chunk”,這些函數通常作為”table”的域來保存。一個C庫(C語言編寫,注冊給Lua使用的庫)的實現方式類似於Lua庫的實現方式。首先C庫中定義提供給Lua使用的函數,其次還需要一個“特殊函數”,它的作用是注冊所有C庫中的函數,並將它們存儲在適當的位置(類似於Lua庫中的函數作為”table”的域來保存)。
Lua可以調用C庫中的函數,就是通過這個注冊的過程實現的。一旦C函數注冊到Lua中,Lua就可以直接通過C函數的引用獲取到C函數的地址(這也是我們注冊的意義,將C函數的地址提供給Lua)。換句話說,一旦C函數注冊,Lua調用他們不依賴於函數名,”package”位置,或者是可見規則。
以上兩種方式下面都會列舉對應的例子,理解第一種方式,將有助於你理解第二種方式的實現流程。

Q:從Lua中調用C所遵循的規則?

A:當C調用Lua函數的時候,必須遵循一些簡單的協議來傳遞參數和獲取返回結果。同樣的,從Lua中調用C函數,也必須遵循一些協議來傳遞參數和獲得返回結果。此外,從Lua調用C函數我們必須注冊函數,也就是說,我們必須把C函數的地址以一個適當的方式傳遞給Lua解釋器。
任何在Lua中注冊的C函數必須有同樣的原型,
typedef int (*lua_CFunction) (lua_State *L); // 定義在"lua.h"中。
被注冊的C函數接收一個單一的lua_State類型的參數,同時返回一個表示返回值個數的數字。函數在將返回值入棧之前無需清理棧,在函數返回之后,Lua會自動清除棧中返回結果下面的所有內容。

Q:如何在C中調用注冊給Lua的C函數?

A:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_sin(lua_State *L)
{
// 如果給定虛擬棧中索引處的元素可以轉換為數字,則返回轉換后的數字,否則報錯。
double d = luaL_checknumber(L, 1);
lua_pushnumber(L, sin(d)); /* push result */

/* 這里可以看出,C可以返回給Lua多個結果,
* 通過多次調用lua_push*(),之后return返回結果的數量。
*/

return 1; /* number of results */
}

int main(void)
{
lua_State *L = luaL_newstate(); // 創建Lua狀態機。
luaL_openlibs(L); // 打開Lua狀態機"L"中的所有Lua標准庫。

/* 這兩句話還有更簡單的方法:
* lua_register(L, "mysin", l_sin)
* 將C函數"l_sin"定義為Lua的全局變量"mysin"。
* 其實現是如下宏:
* #define lua_register(L,n,f) \
* (lua_pushcfunction(L, f), lua_setglobal(L, n))
*/

lua_pushcfunction(L, l_sin); // 將C函數轉換為Lua的"function"並壓入虛擬棧。
lua_setglobal(L, "mysin"); // 彈出棧頂元素,並在Lua中用名為"mysin"的全局變量存儲。

const char* testfunc = "print(mysin(3.14 / 2))";

if(luaL_dostring(L, testfunc)) // 執行Lua命令。
printf("Failed to invoke.\n");
lua_close(L); // 關閉Lua狀態機。

return 0;
}
prompt> gcc main.c -llua -ldl -lm -Wall
prompt> ./a.out
0.99999968293183

另一個例子(假定我們的系統符合”POSIX”標准)的功能類似於ls,將指定目錄中的所有文件以數組的形式返回。當有錯誤發生時,返回nil加上一個描述錯誤信息的字符串。

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <dirent.h>
#include <errno.h>

static int l_dir(lua_State *L)
{
DIR *dir;
struct dirent *entry;
int i = 0;
// 如果給定虛擬棧中索引處的元素可以轉換為字符串,則返回轉換后的字符串,否則報錯。
const char *path = luaL_checkstring(L, 1);

/* open directory */
dir = opendir(path);
if(dir == NULL) {
// 出錯返回"nil"加上一個描述錯誤信息的字符串。
lua_pushnil(L);
lua_pushstring(L, strerror(errno));
return 2; // "nil"加上字符串,共兩個返回值。
}

/* create result table */
lua_newtable(L);
i = 1;
while((entry = readdir(dir)) != NULL) // 逐一讀取目錄中的文件。
{
lua_pushnumber(L, i++); /* push key */
lua_pushstring(L, entry->d_name); /* push value */
lua_settable(L, -3); // t[k] = v
}

closedir(dir);

return 1; // 返回值只有一個,"table"。
}

int main(void)
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);

// 將C函數"l_dir"定義為Lua的全局變量"mydir"。
lua_register(L, "mydir", l_dir);

// 打印"/home/"目錄下的所有文件。
const char* testfunc = "for i, v in pairs(mydir('/home')) do print(i, v) end";

if(luaL_dostring(L, testfunc)) // 執行Lua命令。
printf("Failed to invoke.\n");

lua_close(L);

return 0;
}
prompt> gcc main.c -llua -ldl -lm -Wall
prompt> ./a.out
1 vermilliontear
2 git
3 .
4 ..
5 lost+found

Q:如何在Lua中調用作為庫函數提供給Lua的C函數?

A:我們使用上面的第一個例子,將其改造為C庫的方式。
“mylib.c”文件中:

#include <stdio.h>
#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

/* 所有注冊給Lua的C函數具有
* "typedef int (*lua_CFunction) (lua_State *L);"的原型。
*/

static int l_sin(lua_State *L)
{
// 如果給定虛擬棧中索引處的元素可以轉換為數字,則返回轉換后的數字,否則報錯。
double d = luaL_checknumber(L, 1);
lua_pushnumber(L, sin(d)); /* push result */

/* 這里可以看出,C可以返回給Lua多個結果,
* 通過多次調用lua_push*(),之后return返回結果的數量。
*/

return 1; /* number of results */
}

/* 需要一個"luaL_Reg"類型的結構體,其中每一個元素對應一個提供給Lua的函數。
* 每一個元素中包含此函數在Lua中的名字,以及該函數在C庫中的函數指針。
* 最后一個元素為“哨兵元素”(兩個"NULL"),用於告訴Lua沒有其他的函數需要注冊。
*/

static const struct luaL_Reg mylib[] = {
{"mysin", l_sin},
{NULL, NULL}
};

/* 此函數為C庫中的“特殊函數”。
* 通過調用它注冊所有C庫中的函數,並將它們存儲在適當的位置。
* 此函數的命名規則應遵循:
* 1、使用"luaopen_"作為前綴。
* 2、前綴之后的名字將作為"require"的參數。
*/

extern int luaopen_mylib(lua_State* L)
{
/* void luaL_newlib (lua_State *L, const luaL_Reg l[]);
* 創建一個新的"table",並將"l"中所列出的函數注冊為"table"的域。
*/

luaL_newlib(L, mylib);

return 1;
}

將”mylib.c”編譯為動態連接庫,

prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c mylib.so a.lua

“a.lua”文件中:

--[[ 這里"require"的參數對應C庫中"luaopen_mylib()"中的"mylib"。
C庫就放在"a.lua"的同級目錄,"require"可以找到。]]

local mylib = require "mylib"

-- 結果與上面的例子中相同,但是這里是通過調用C庫中的函數實現。
print(mylib.mysin(3.14 / 2)) --> 0.99999968293183

附加:

1、每一個與Lua通信的C函數都有其獨有的虛擬棧。
2、在極端情況下,打印指定目錄中文件的例子可能會造成小小的內存泄漏。在內存空間不足的情況下,l_dir()中的lua_newtable()lua_pushstring()lua_settable()都會立即拋出錯誤並終止程序的運行,這將導致closdir()無法被調用。
3、通常C庫中“特殊的函數”都被定義為公有的(使用extern修飾),而其他函數均被定義為私有的(使用static修飾)。
4、當你想要使用C函數擴展你的Lua程序時,即使只有一個C函數,也最好使用C庫的方式。因為在不久的將來(通常來說會很快),你將需要其他的C函數。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com