現現在,移動應用程序被應用的愈來愈寬泛,程序愈來愈複雜,跨平臺開發也顯得愈來愈重要。針對各自的平臺作原生開發無疑是重複製造輪子,那麼有沒有什麼可讓已造好的輪子進行復用呢? java
Lua 就此提供了這個可行性,而且,基於寄存器設計的 Lua 執行效率是很是好的,幾乎不須要考慮 Lua 執行的代價。但 Lua 並不是像 Python 那樣成爲能夠自立門戶的腳本語言,它必須藉助於 C 庫才能發揮出它強大的功能。也許您會說,既然要藉助於 C 庫,幹嗎不直接用 C 呢,何須再多借助一層?Lua 的代碼是按照「Clean ANSI C」的標準編寫且是解釋執行的腳本,那麼它就能夠在任何支持 C 的環境中運行,同時意味着,你能夠隨時更新它,徹底繞開先編譯再運行,甚至可能重啓應用或服務的過程。 linux
在本篇文章中,咱們拋開語法等大部頭的東西,直接講解最關鍵部分,Lua 在各類平臺中是如何進行嵌入及配置。只有先解決並弄清楚這些關鍵問題,那麼 Lua 的應用纔不會再讓咱們感到那麼棘手,基於 Lua 開發的其餘應用擴展就會勢如破竹一鼓作氣。最後咱們也就會感慨一句,Lua 原來並不複雜,只須要作這點事而已。 macos
本篇分兩部分,第一部分講嵌入實戰,第二部分講 Lua 的相關配置。編程
嵌入實戰 數組
咱們先從最基礎的 C 嵌入講起,直到嵌入到移動應用,由簡入繁,讓咱們逐步弄清移動應用嵌入 Lua 的原理。其中最重要的一點是:Lua 其實是經過堆棧的共享來與 C 進行數據交互 (除了一般意義的數據類型外,函數、文件等也被看成是一種數據)。 本文示例所使用的 Lua 版本是如今用得比較普遍的 Lua 5.1.1 版本。 app
純 C 環境下嵌入 Lua ide
Lua 是「生存」於 C 環境中的,所以,用 Lua,首先得有 C 環境。這個是 Lua 嵌入的根本,因此會花較多的篇幅詳述,若是弄清楚了這個,就邁出了 Lua 嵌入第一步也是最重要的一步。 函數
C 應用中調用 Lua 腳本 ui
第 1 個例子,咱們使用最簡潔的代碼完成這個最基本的功能。this
建立 helloWorld.lua 文件:
print("Hello World!")
建立 helloWorld.c 文件:
#include <lua.h> #include <lualib.h> #include <lauxlib.h> int main() { lua_State *L = luaL_newstate();//使用 luaL_newstate 函數建立 Lua 與 C 進行數據交互的堆棧並返回指針 L; luaL_openlibs(L);//使用 luaL_openlibs 函數爲堆棧加載全部的標準 Lua 庫,支持腳本在 C 應用中執行; luaL_dofile(L, "helloWorld.lua");//使用 luaL_dofile 函數執行整個 Lua 腳本文件 helloWorld.lua lua_close(L);//使用 lua_close 函數關閉堆棧,釋放資源 return 0; }
Lua 腳本 helloWorld.lua 僅作最基本的字符打印。
筆者是使用 gcc 命令編譯執行,可運行在 Mac、Linux、Unix 環境。
運行命令,顯示 Hello World!:
gcc helloWorld.c -o helloWorld.out -I/usr/local/include -L/usr/local/lib –llua &./helloWorld.out
從這個最基本的函數能夠看出:Lua 與 C 的交互是基於堆棧進行的,建立出堆棧後,C 和 Lua 就共同使用這個堆棧的特性進行數據交互以及各類操做。
下面的例子能夠更清楚的說明:
C 應用進行 C 與 Lua 的交互 (Lua 調用 C 函數)
修改 helloWorld.lua 文件:
print(sayHelloWorld("Hello World!"))
修改 helloWorld.c 文件:
#include <lua.h> #include <lualib.h> #include <lauxlib.h> int sayHelloWorld(lua_State *L) { const char * str = lua_tostring(L, 1); lua_pushstring(L, str); return 1; } int main() { lua_State *L = luaL_newstate(); luaL_openlibs(L); lua_register(L, "sayHelloWorld", sayHelloWorld); luaL_dofile(L, "helloWorld.lua"); lua_close(L); return 0; }
在終端運行後,顯示和上例同樣,但其中過程複雜了些。
Lua 腳本再也不單純地打印字符,而是打印出調用 C 文件中函數 sayHelloWorld 的返回值。
能夠看出 Lua 是直接使用 C 函數的,但這好像又不是那麼簡單的。由於一般的 C 函數,Lua 確定不能直接調用,所以對於 C 來講,如何建立用於 Lua 的函數,或者說 Lua 怎麼知道這個函數能夠調用,這個是須要咱們瞭解清楚的。
這個例子用最核心的精簡代碼實現:
1. 咱們先從 Lua 與 C 的橋接函數 int sayHelloWorld(lua_State *L) 看起:
參數使用的是 lua_State 類型的指針,返回的是整型數字。
這裏讀取參數,返回參數和一般的函數有點不一樣:
參數爲指向數據共享堆棧的指針,而實際傳入的參數都放在這個指向的堆棧中,函數經過這個堆棧得到實際的傳入參數;
整型返回值並不是一般意義的返回值,這個數字是用來通知 Lua 腳本這個函數返回值的個數,真正的返回值是壓在堆棧中, 所以,對於 Lua 及 Lua 橋接函數來講,傳入參數及返回值都是放在指針 L 指向的堆棧中,同時,這樣的處理,就支持了能夠傳入的多個參數也能夠返回多個值。
該函數內代碼,使用針對堆棧對應的得到傳入參數及寫入返回值的函數完成功能。
本例使用 Lua 取數據的方法 —— lua_tostring 從堆棧中取得第一個類型爲字符串的數據;
使用 Lua 壓入數據方法 —— lua_pushstring 將字符串類型的返回值壓入到堆棧中 (Lua 有其對應的類型從堆棧上取數據、數據的方法,具體狀況請參考 Lua 手冊)。
同時將返回值的個數 return 給 Lua 腳本,本例壓入堆棧的返回值只有一個,所以返回 1。
由此看出:與 Lua 打交道的函數是特供的,須要此函數來橋接 C 與 Lua,其基本的規則就如上所述,經過它,將 Lua 與 C 結合在一塊兒。
2. 接着看 main 函數。
相比前一例增長了一行 lua_register 調用函數,經過這個函數用來註冊橋接函數。
由於在調用 luaL_openlibs 函數加載 Lua 函數庫到堆棧中,此時堆棧中是沒有咱們自定義函數 sayHelloWorld 的任何信息的,此時直接執行 Lua 腳本,確定會報找不到函數而錯誤。要讓 Lua 能夠調用此函數,必須得讓 Lua 腳本「知道」有這樣一個函數的存在,因此 main 函數中使用代碼 lua_register(L, "sayHelloWorld", sayHelloWorld) 將橋接函數 sayHelloWorld 註冊進堆棧中的,讓 Lua 能夠經過堆棧調用 C 中的該函數,完成交互。
經過本例咱們能夠了解到:Lua 要使用自定義的 C 函數,須要書寫特定規則的橋接函數,接着將橋接函數註冊進堆棧中,這樣 Lua 腳本就能經過調用這個橋接函數完成 Lua 與 C 的數據交互。
運行命令,顯示 Hello World!:
gcc helloWorld.c -o helloWorld.out -I/usr/local/include -L/usr/local/lib –llua &./helloWorld.out
--------------------------------------------------------------------------------------------------------------------
這個例子說明了 Lua 調用 C 函數的的狀況,那 C 調用 Lua 的數據又是怎樣的呢?
C 應用進行 C 與 Lua 的交互 (C 調用 Lua 數據)
修改 helloWorld.lua 文件:
text = "Hello World!" function say(str) print(str) end
修改 helloWorld.c 文件:
#include <lua.h> #include <lualib.h> #include <lauxlib.h> char* readLuaFile() { // 讀文件代碼略去,直接返回文件內容 return "text=\"Hello World!\"\n function say(str)\n print(str)\n end" } int main() { lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dostring(L, readLuaFile()); lua_getglobal(L, "text"); const char * text = lua_tostring(L, -1); lua_getglobal(L, "say"); lua_pushstring(L, text); lua_pcall(L, 1, 0, 0); lua_close(L); return 0; }
此時 helloWorld.lua 腳本修改成:
定義了一個全局變量 text,和一個將傳入參數打印出來的 say 函數。
這裏爲了方便,C 文件中咱們用 readLuaFile 函數返回模擬讀取的 Lua 腳本 helloWorld.lua 內容,完整的話得有一個通用的文件讀取函數來讀取文件,這裏不贅述。
接下來咱們從 main 函數內調用函數 luaL_openlibs 後的代碼看起:
1. 使用 luaL_dostring 函數加載由函數 readLuaFile 讀取的 Lua 腳本 helloWorld.lua 的內容,以字符串類型壓入到堆棧中;
2. 使用 lua_getglobal 函數從堆棧中得到 Lua 全局變量 text(由前一步壓入到堆棧中的 Lua 腳本數據中獲取),並使用 lua_tostring 函數獲取數據並賦值給 C 變量 text;
3. 再次使用 lua_getglobal 函數從堆棧中得到 Lua 函數 say;
4. 將 C 變量 text 經過 lua_pushstring 壓入到堆棧中,做爲 Lua 函數 say 的參數;
5. 最後使用 lua_pcall 函數調用 Lua 函數 say,其中參數 2 指明參數個數,參數 3 指明返回值個數。
運行命令,打印結果爲 Hello World!:
gcc helloWorld.c -o helloWorld.out -I/usr/local/include -L/usr/local/lib –llua &./helloWorld.out
經過這幾例咱們能夠看出:luaL_dofile 用來執行整個 Lua 腳本文件,而使用 luaL_dostring 則針對加載的 Lua 腳本,能夠得到腳本中的變量、函數,而後能夠有選擇的進行操做。
以上都是以 C 應用爲主導的使用 Lua 的示例,那麼以 Lua 爲主導該怎麼辦?
Lua 腳本與 C 交互 根據上面的例子,咱們大體能夠推斷出,Lua 調用 C,得有橋接函數,Lua 調用它來進行交互,但還須要作其它的一些事情才能達成目的,更多細節以下例所示:
建立 saySomethingTest.lua 文件:
require "saySomething" print(saySomething.say("Hello Lua!」))
建立 saySomething.c 文件:
#include <lua.h> #include <lualib.h> #include <lauxlib.h> int say(lua_State *L) { lua_pushstring(L, lua_tostring(L, 1)); return 1; } static luaL_Reg functions[] = { {"say", say}, {NULL, NULL} }; int luaopen_saySomething(lua_State* L) { luaL_register(L, "saySomething", functions); return 1; }
本例中,咱們首先看 Lua 腳本,使用 require 加載自定義的 saySomething 庫,而後調用該庫中的 say 函數並將結果打印出來。
由此能夠看出:對於 Lua 調用 C,首先須要使用 require 關鍵字引用自定義的模塊,而後調用該庫中的註冊的橋接函數完成交互。
接着看 C 文件,對於代碼中關於橋接函數再也不贅述。
這裏定義了一個類型爲 luaL_Reg 的用於註冊橋接函數靜態數組,將橋接函數 say,以函數別名(別名可自定義),函數名的形式 {「say」, say} 做爲數組第一個元素,以 {NULL, NULL} 做爲結尾元素 (必須)。
緊接着是函數 luaopen_saySomething,這裏使用函數 luaopen_XXX 來進行接口模塊註冊(注意 XXX 爲自定義的模塊名稱),此時 XXX 即爲 Lua 腳本 require 的模塊名稱。
在此註冊模塊函內使用 luaL_register 函數將剛纔定義的橋接函數註冊數組 functions 註冊到別名 saySomething 中並壓入到堆棧中,這樣在 Lua 腳本中引用了該模塊後,就能經過堆棧調用註冊進別名 saySomething 的全部橋接函數,使用點號調用指定函數。最後返回參數個數 1。
運行命令生成動態連接庫 saySomething.so
gcc -c -fPIC saySomething.c -I/usr/local/include gcc -shared -fPIC -I/usr/local/include -L/usr/local/lib -llua -o saySomething.so saySomething.o
最後運行 Lua 腳本,顯示 Hello Lua!
lua saySomethingTest.lua
---------------------------------------------------------------------------------------------------------------------
Lua 腳本與 Java 做爲 Java 語言自己是不支持 Lua 的,但 Java 支持本地化編程,能使用 JNI 調用 C,於是讓 Lua 嵌入到 Java 中成爲可能。話雖如此,要將 Lua 大部分須要的函數經過 JNI 轉換成對應的 Java 方法實際上也是比較浩大的工程。不過,已經有 LuaJava 這個開源軟件幫咱們完成這個工做,將大部分 Lua 函數封裝成堆棧類 LuaState 對應的 Java 方法,咱們就能夠直接拿來用,對應的具體方法能夠參看源碼,配置方法見第二部分說明。
圖 1. 開發具備 Lua 特性的 Java 工程結構圖
新建一個 Java 工程 TestJava,引入 luajava-1.1.jar。
建立 hello.lua 文件:
print("Hello World!")
建立 TestMain.java 文件:
import org.keplerproject.luajava.LuaState; import org.keplerproject.luajava.LuaStateFactory; public class TestMain { public static void main(String[] args) { LuaState L = LuaStateFactory.newLuaState(); L.openLibs(); L.LdoFile("hello.lua"); } }
運行後,顯示 Hello World!
熟悉了前面的示例,應該很容易理解。執行的流程與之前同樣,建立堆棧 L,再裝載 Lua 庫,最後執行 Lua 腳本。
這些 Lua 功能函數都封裝成爲堆棧對象 LuaState 的方法,經過 JNI 調用 Lua 庫對應的 Lua 函數。實際上,Java 使用 Lua,就是在本來 Lua 與 C 的交互中,增長了一層 Java 與 Lua 間的 JNI 層。
這樣,經過封裝後的 luajava.jar 的 jar 包提供的類,咱們直接使用 Java 面向對象的編程便可與 Lua 交互。 若是須要修正或擴展 Java 中對應的 Lua 函數,能夠經過增改 JNI 這一層來實現。
好比:Luajava 如今僅支持到 Lua5.1,若是要支持新版本,則要根據新版本 Lua 與 Lua5.1 的不一樣的地方在 JNI 層進行相應的增改,以支持新的 Lua 庫。
既然 Java 可使用 Lua,那麼 Android 呢?
在安卓中使用 Lua 經過上例咱們知道了 Java 如何調用 Lua,那麼支持 Java 及 JNI 開發的安卓一樣能使用 Lua,只是安卓有其自身獨立的系統,所以須要將 Lua 腳本,Lua 庫,以及 LuaJava 的源碼包放到安卓工程中(本例,將 LuaJava 的 Java 源碼放在工程 src 目錄下;
將 Lua 庫及 LuaJava 的 C 源碼統一打包爲 libluajava.so 庫文件放在工程 libs/armeabi 目錄下做爲 Lua 腳本的運行環境,詳情見本文檔附帶的完整的 Android 代碼包),如圖所示:
圖 2. 開發具備 Lua 特性的 Android 工程結構圖
備註:libluajava.so 見第二部分配置說明。
建立 raw 目錄,並建立 hello.lua 腳本(放在 raw 中,是由於此目錄的文件不會被編譯,以原始文件放置,一樣放在 asserts 目錄以及擴展 SD 卡中也行):
hello.lua
function getData(param) return "Hello " .. param end
修改 MainActivity.java 代碼
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LuaState L = LuaStateFactory.newLuaState(); L.openLibs(); L.LdoString(toStringForStream(getResources() .openRawResource(R.raw.hello))); L.getGlobal("getData"); try { L.pushObjectValue("Lua^^"); } catch (LuaException e) { e.printStackTrace(); } L.pcall(1, 1, 0); final String text = L.toString(-1); TextView tv = new TextView(this); tv.setText(text); setContentView(tv); } private String toStringForStream(InputStream is) { BufferedReader reader = new BufferedReader( new InputStreamReader(is)); StringBuilder sb = new StringBuilder(); String line = null; try { while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } } catch (IOException e) { e.printStackTrace(); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } return sb.toString(); } }
本例中,
使用經過 LuaState 聲明的堆棧對象 L 的相關方法完成功能 ;
使用 LdoString 方法加載從資源文件夾中獲取的 Lua 腳本內容;
使用 getGlobal 方法得到 Lua 腳本的函數 getData;
使用 pushObjectValue 方法將參數傳給 Lua 腳本函數 getData;
使用 pcall 方法調用 Lua 腳本函數 getData,參數爲上一步傳入的 Lua^^;
使用 toString 得到 Lua 腳本函數 getData 的返回值,賦值給變量 text;
最後將 text 的內容經過 Android 控件 TextView 定義的 tv 顯示出來:Hello Lua^^。
因此理解了 Java 與 Lua 的交互,Android 環境下使用 Lua 就很容易理解了,只要經過 JNI 爲使用 Lua 建立出運行環境,那麼在 Android 中就可使用 Lua 腳本了。
-----------------------------------------------------------------------------------------------------------------------
IOS 使用 Lua 由於 IOS 開發使用 Objective-C,自己就支持 C 混編,因此,只須要將 Lua 擴展爲庫文件加載到 IOS 工程中就能夠直接用了(在配置說明中會詳細說明如何將 Lua 庫加載到 IOS 工程)。
這裏給個簡單例子:
修改 ViewController.m 文件:
#import "ViewController.h" #include "lua.h" #include "lualib.h" #include "lauxlib.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *showLabel; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dostring(L, "text='Hello World!'"); lua_getglobal(L, "text"); _showLabel.text = [NSString stringWithUTF8String: lua_tostring(L, -1)]; lua_close(L); } (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } @end
根據代碼可見,IOS 開發中,使用 Lua 就和在 C 中使用同樣,只是,Objective-C 與 C 的數據類型要作相應的轉換,這裏就再也不贅述了。
配置說明 Lua 桌面環境安裝 進入Lua 官網, 下載相應的版本的 Lua,本文章以現今使用最普遍 5.1 爲例來說解,使用更高的版本,可能根據不一樣的地方作相應的代碼修改。
Windows 中: 直接下載二進制的安裝版進行安裝。
Linux 中: 使用終端解壓後進入到 Lua 目錄中執行 make linux test Mac 中: 解壓後進入到 Lua 目錄中執行 sudo make macosx test LuaJava 桌面環境配置 LuaJava 如今僅支持到 Lua 5.1.1,但咱們能夠基於 5.1.1 在 JNI 層將後續版本的新特性加上去,廢棄的函數剔除掉就能夠兼容最新版本的 Lua 了。
Linux、Mac 系統 先修改 config 文件,配置好各個參數的路徑,而後使用終端到 LuaJava 目錄下使用 make 命令編譯,會生成一個 Jar 包,和一個.so(Linux 系統) 或.jnilib(Mac 系統) 庫文件。Jar 包就放到 Java 工程中引用,庫文件放到 Java 的庫文件目錄,可使用以下代碼得到 Java 庫文件的目錄位置: System.out.println(System.getProperty("java.library.path"));
Windows 系統 先修改 config.win 文件,配置好各個參數的路徑,而後使用 VS 等 C 編譯器編譯,會生成一個 Jar 包,和一個.dll 庫文件。位置同上。
安卓使用 Lua 相關配置 安卓使用 Lua 腳本須要 Lua 庫及 Java 與 Lua 的 JNI 層,這時就須要將 Lua、LuaJava 集成到安卓開發環境中: 一種是能夠將安卓工程轉化爲 C/C++工程,將 LuaJava 的 C 源碼、Lua 的 C 源碼放到 JNI 目錄中,配置好 Android.mk 文件,Java 源碼直接放到工程 src 目錄中,每次使用 NDK 編譯運行; 另外一種將 LuaJava 的 C 源碼、Lua 的 C 源碼放到 JNI 目錄中,配置好 Android.mk 文件,在終端中進入工程目錄,使用 ndk-build 命令生.so 庫文件,之後其它工程就使用該庫文件進行開發。 關於 NDK 的編譯本文再也不贅述,能夠參看相關資料。
IOS 使用 Lua 相關配置 建立 IOS 工程後,點擊菜單 Editor 選擇 Add Target…彈出上圖對話框,選擇 Cocoa Touch Static Library 建立靜態庫,添加靜態庫 lua。
圖 3.IOS 工程添加靜態庫 lua
而後將 Lua 的 C 源碼整個 src 文件夾拖到新增長的靜態庫工程文件夾 lua 中。
圖 4. 添加 Lua 源碼到靜態庫文件夾 lua 中
最後點擊工程根目錄,在 Linked Frameworks and Libraries 條目中點擊+,將 Lua 靜態庫添加到工程中。
這樣,就在 IOS 工程中集成了 Lua 腳本運行環境,就能夠自由使用 Lua 腳本了。 圖 5. 將 Lua 靜態庫添加到工程中
結束語 到這裏,關於 Lua 和 C 的交互已經所有說明完畢,最後總結下:
1. 環境支持
要使用 Lua 腳本,必需要有支持 Lua 運行的環境――依賴於 C 的 Lua 庫。在桌面環境及 IOS 中,僅須要 Lua 庫;在 Java 及 Android 中除了集成 Lua 庫外,還要集成 Java 與 Lua 函數的 JNI 層做爲本地化編程的支持。
2. 嵌入原理
a. Lua 和 C 的交互都是經過堆棧進行數據交互,Lua 庫提供了一系列的在堆棧上的數據操做函數;
b. Lua 要使用 C 中的功能必須經過符合規則並註冊到堆棧上的橋接函數;
c. C 使用 Lua 腳本,一是經過直接執行文件方式;二是經過讀入 Lua 文件數據,使用 Lua 中的變量或執行 Lua 中的函數。
從上面全部的例子來看,Lua 歸根結底就是寄生於 C 的腳本應用,只要能用 C 的地方,Lua 也就能用起來。所以,只要能配置好 Lua 的運行環境,那麼 Lua 腳本就能夠運行於不一樣的平臺,使跨平臺成爲可能。 Lua 腳本自己的特色可讓應用具備更多的靈活性,咱們能夠最大限度地使用 Lua 腳原本配置或者擴展需求的功能,這樣不少更新沒必要每次重啓服務或更新應用,只須要更新 Lua 腳本就能完成。 Lua 能用來擴展到什麼程度,就看咱們如何設計應用了。