最近在進入Lua編程的狀態,一度令我困惑的是,Lua提供的功能少的可憐,跟自備電池的python相比,可說是簡陋了。連table的打印,都需要自己實現,也因此有了一打的第三方方案。后來我想明白了,以Lua和C如此緊密的關系,只需要建立Lua的binding,那么豐富而性能強大的C庫資源完全可以為Lua所用,這樣就不愁功能缺失了。
關於C調用Lua,前段時間已經寫過一篇短文了:《多語言協作與二進制交互》,現在補上從Lua調用C的這一篇。先上一段Lua代碼:
#!/usr/bin/lua require("power") print(square(1.414213598)) print(cube(5))
在這段代碼里,我引用了一個power模塊,並且調用了power模塊里的square函數和cube函數。由此可知,在C語言編寫的power模塊里面,需要有相關的函數可以注冊本模塊提供的函數,並且讓Lua可以“感知”到自身是一個模塊。回憶一下Lua的路徑搜索,可以看到除了后綴名為*.lua的文件之外,還有*.so文件,所以C擴展是編譯為.so的。實際情況是,在執行require語句的時候,系統會調用luaopen_power函數,這個函數名是通過luaopen_與power這個模塊名拼接得到的。
現在看看luaopen_power函數的定義:
int luaopen_power(lua_State *L){ lua_register( L, /* Lua 狀態機 */ "square", /*Lua中的函數名 */ isquare /*當前文件中的函數名 */ ); lua_register(L,"cube",icube); return 0; }
可以看到,通過調用lua_register函數,我們在L這個lua虛擬機里面注冊了兩個函數,一個是square,一個是cube,他們分別對應到isquare和icube這兩個C函數。
回過頭來看一看,lua腳本里,這條簡單的require語句,執行了兩個步驟:一是先把名字為power.so的文件加載起來,二是調用其中的luaopen_power函數。下面來看一下具體的函數如何定義:
static int isquare(lua_State *L){ /* C中的函數名 */ float rtrn = lua_tonumber(L, -1); /* 從Lua虛擬機里取出一個變量,這個變量是number類型的 */ printf("Top of square(), nbr=%f\n",rtrn); lua_pushnumber(L,rtrn*rtrn); /* 將返回值壓回Lua虛擬機的棧中 */ return 1; /* 這個返回值告訴lua虛擬機,我們往棧里放入了多少個返回值 */ }
注釋的講解比較詳細了,可以看到為Lua定義的C函數格式都是比較統一的,首先接受一個Lua虛擬機變量L,然后從L里取出相應的參數(需要指定數據類型),最后將返回值再次壓回虛擬機里面,通過返回int告訴Lua虛擬機,自己的返回值有多少個。
好,到這里就只差最后一步了,現在把這段C代碼編譯成.so文件。使用如下的編譯參數:
ubuntu@ubuntu:~$ gcc -Wall -shared -fPIC -o power.so -I/usr/include/lua5.1 -llua5.1 hellofunc.c
(hellofunc.c記得要include lua.h, lauxlib.h, lualib.h三個頭文件哦)
這里解釋一下兩個參數的意思,-shared是告訴gcc,需要編譯成.so文件,並且這個源文件里面不會有main函數,不要大驚小怪。另一個-fPIC,是Position Independent Code的意思,具體的含義可以參考這篇,主要用來避免同一份代碼因為重定位位置不同而在內存中存在多個實例。
調用的結果:
ubuntu@ubuntu:~$ ./hellofunc.lua Top of square(), nbr=1.414214 2.0000002687177 Top of cube(), number=5.000000 125
當然,如果機器安裝有多個版本的Lua,需要指定執行hellofunc.lua的lua解釋器版本。因為本篇教程是針對Lua5.1的,所以需要指定Lua5.1來執行,例如lua5.1 hellofunc.lua,否則用lua5.2調用會引起core dump