最近對Lua很感興趣,以下是本階段學習的總結,包含三部分,一部分是基礎語法,一部分是擴展和解釋器嵌入,最后一部分是Lua小練習。
知識涉及:Lua語言編程基礎;Lua&C++:Lua擴展、嵌入Lua解釋器、裸寫C++擴展、借助swig寫C++擴展。平台:Linux、gcc4.8.2
一、基礎語法
1、print("Hello World")
Lua中不需要分號,也不需要python那樣的嚴格格式;
單引號跟雙引號等價,多行文本使用兩個中括號擴起來:[[multi line]];
代碼中的注釋:--開頭的一行,或者是--[[ 多行注釋 --]];
Lua關鍵字:and break do else elseif end false for function if in local nil not or repeat return then true until while;
所有變量默認是全局變量,除非加上local 關鍵字;
支持多參數賦值:a, b = 1, 2;
Lua中的變量是對值的引用;
and和or都是short-cut-evaluation,a = 4 and 5, a是5; a = 4 or 5,a是4;
Lua不支持:switch-case、continue、i++、++i;
Lua將false、nil視為假,將其它視為真;
在Lua5.2之后,全局表為_ENV是一個table,這里包含了所有能使用的函數、類型、全局變量;
package.loaded 這也是個table,保存了所有加載的模塊,如果寫擴展(Lua腳本或者C模塊),可以通過修改_ENV表或者package.loaded來手動添加擴展模塊;
2、數據類型
(1)常用數據類型:number、string、table、function、bool、nil;
(2)不常用數據類型:userdata、thread;
注意:table也就是key--value鍵值對;function也是一種類型;thread也是一種類型,與協程相關;userdata用於表示自定義類型。
3、連接字符串
a..b,變量a跟變量b的值連在一起,a和b是number或者string;
eg:
a = 123 b = 'world' print(a..b) --輸出:123world
4、table變量的定義
(1)a = {} 變量a指向空的table
(2)a = {1, "2"} 變量a指向 {1, "2"} table
(3)a = {} a["a"] = 1 a["x"] = 2 變量a指向空的table,接着通過a對這個table賦值
(4)a = {["x"] = 1, y = 2, [1] = "a"} table構造的方式1
(5) a = {[{}] = "x", b = {}} table構造的方式2
table可以作為數組,如上面的(2),也可以是map,如上面的(3)和(4)。
key可以是任何數據類型:number、string、table、function,value也可以是任何數據類型。
這也許就是動態類型帶來的極大隨意性吧。
5、table變量的使用
a[1] 或者是 a["b"],總之就是用map的方式去索引,table也用來表示數組(數組也只能用table表示),數組table的key為數字。
6、獲取數組的長度
function tablelength(T)
local count =0
for _ in pairs(T) do count = count +1 end return count end
7、if--else語句
a = 1 if a == 1 then print("a is 1") elseif a == 2 then print('a is 2') end -- 不等於號:~=
8、while語句
a = 1
while a ~= 5 do a = a + 1
print(a) end
注意:沒有continue、i++
9、for語句
for語句有兩種使用方式:
(1) for 開始數字、終止數字、步長 do ...
for k = 1, 10, 2 do print(k) end -- 輸出:1 3 5 7 9
(2) pair 和 ipair
a = {'a', 'b', 'c'}
for k, v in ipairs(a) do
print(k, v)
end
--[[
輸出:
1 a
2 b
3 c
--]]
a = {b='b', a='a'}
for k, v in pairs(a) do
print(k, v)
end
--[[
輸出:
a a
b b
--]]
ipair只能用在數字為key的table上,且遍歷從1開始,直到對應key的value為nil,pair則沒有這個限制。
10、repeat語句
a = 1
repeat a = a + 1
print(a) until a == 5
11、函數
function add(a, b) return a + b end
function nothing(a, b) return a, b end
括號不能隨便加,多個返回值再加上括號,就只返回一個:
print(nothing(1, 2)): --輸出1 2 print((nothing(1, 2))): -- 輸出1
如果參數是一個table,則可以省略括號:
function add(a) return a.a + a.b end print(add({a = 1, b = 2})) -- 3 print(add{a = 1, b = 2}) --3
支持可變參數:
function test1( ... ) for i = 1, select('#', ...) do local arg = select(i, ...) print(arg) end end function test2( ... )-- 這種方式不推薦,會導致nil參數把參數列表截斷 for i, v in ipairs{...} do print(v) end end test1('a', nil, 'c') --[[ 輸出: a nil c --]] test2('a', nil, 'c') --[[ 輸出: a --]]
12、閉包(closure)
閉包對於C++程序員來說是個新奇的概念(三年前我就覺得它很新奇),舉個栗子:
test = function(a) first = 2; --這個變量可以被嵌套在test函數中的函數訪問,這個在Lua中也稱為upvalue return function(b) tmp, first = first, b return tmp + b end end add = test(); print(add(2)); -- 4 print(add(3)); -- 5
閉包也是實現泛型for的原理(就是有個地方保存着變量),閉包還可以實現私有變量。
15、require引入擴展庫
require('math')
print(math.pi)
16、IO操作
--讀文件:
local file = assert(io.open('file_path', 'r')) for line in file:lines() do print(line)
end
file:close() --寫文件:
f_w = assert(io.open('file_path', 'w')) f_w:write('xxx') f_w:close()
17、table庫,可以對數組做各種操作
注意,是數組。
(1)數組元素連接:table.concat, : a = {"a", "b"} print(table.concat(a)) 輸出:ab
(2)數組元素插入: table.insert, : a = {"a", "b"} table.insert("c")
(3)數組元素最大序號:table.maxn, : a = {"a", "b", [100] = "z"} print(table.maxn(a))
(4)數組元素刪除:table.remove, : a = {"a", "b"} table.remove(a, 1): 刪除到序號為1的元素“a”
(5)數組元素排序:table.sort, :默認會根據元素的值從小到大排序,也可以自定義排序lambda函數:
a = {"b", "1", "z", "a"}
table.sort(a, function(a, b) return a > b end)
print(table.concat(a))
18、string庫可以對數組做各種操作,有類正則表達式的功能
string.gfind string.gmatch string.gsub string.len
eg:
a = 'abc123' b = string.match(a, '[a-z]([a-z])') print(b) --輸出:b b = string.gmatch(a, '[a-z]') for item in b do print(item) end --輸出:a b c local _, count = string.gsub(a, "[a-z]", "X") print(_) -- 輸出:XXX123 print(count) --輸出:3 print(string.len(a)) --輸出:7
19、協同程序
這是個有趣的功能,簡單說就是讓一個函數執行到一半就停下來去干別的,接着由於某些事件觸發它繼續往下走。控制者和被控制者在同一線程中執行,所以協程是單個線程上的設計,設計的初衷可能是為了在執行IO操作時交出控制權,之后再通過狀態切換/事件通知的方式讓程序繼續往下執行,減少CPU的空閑等待浪費。
lua上的demo:
co = coroutine.create(function() print("hi") end) coroutine.resume(co)
協程這個知識很多語言都有,甚至在Linux C++中,都有人為網絡服務實現了協程,做法是對阻塞的socket API做hook,調用這個函數時,應用程序切換去做其它事情,直到有通知觸發,才繼續從阻塞處往下執行。
擴展閱讀:http://www.udpwork.com/item/12045.html;騰訊開源的libco庫:http://code.tencent.com/libco.html
協程的關鍵是,在執行耗時IO操作時,系統可以讓出控制權,讓用戶去干別的事情。這跟異步API很像,不同的是異步API的使用會要求有回調函數,所以我們實現一個功能的時候,會有異步API調用+回調函數,這樣就有兩部分代碼(當然,有些語言可以有lambda函數,這樣代碼就放在一塊了),而如果使用協程,代碼是可以放在一起的,好像在使用同步API寫代碼一樣(調用到關鍵API時會主動停下來,交出控制權,后面能拿到控制權繼續往下執行)。
擴展閱讀:http://blog.csdn.net/kobejayandy/article/details/11856735
在Lua中,通過協程可以實現主程跟協程的數據交換:
function foo (a) for i=1, 10 do print('I get from outsite:'..coroutine.yield('I sent to outsite:'..(2 * a))) end end co = coroutine.create(foo) print(coroutine.resume(co, 1)) --[[ 輸出: I get from outsite:1 true I sent to outsite:2 --]]
20、Lua的擴展模塊加載路徑
require('xxx'),可以加載xxx擴展模塊,如果require找不到Lua文件就會去找C程序庫,Lua文件跟C程序庫是兩種可能的Lua擴展方式。
在Lua解釋器中執行下面兩個語句:
print(package.path) --這是Lua腳本模塊可加載地址 print(package.cpath) --這是C程序庫可加載地址
Lua腳本模塊加載路徑格式舉例:.\?.lua;C:\Program Files (x86)\Lua\5.1\lua\?.lua; 其中?是通配符,表示任意名字。
Lua擴展C程序庫加載路徑格式舉例:.\?.dll;.\?51.dll; 如果在Linux下這是?.so.
在Linux下可以通過修改設置LUA_PATH、CLUA_PATH兩環境變量來設置對應的加載路徑,eg:
export LUA_PATH='/home/work/cswuyg/install/lua-lua/lib/md5/?.lua;./?.lua'
export LUA_CPATH='/home/work/cswuyg/install/lua-so/?.so;./?.so'
21、元表和元方法
對象可以指定元表,元表里包含有元方法,元方法可以做很多高級的事情。
print(xx.yy),如果xx變量沒有yy字段,那么就會去查找xx是否有元表,如果xx有元表,就去查xx的元表中是否有__index元方法,如果有元方法,就從元方法中獲取yy字段的值。
mt = {} -- 元表
a = {}
setmetatable(a, mt)
mt.__index = {['x']=1}
print(a.x)
-- 輸出:1
mt = {} mt.__newindex = {} a = {} setmetatable(a, mt) a.x = 10 for k, v in pairs(mt.__newindex) do print(k, v) end --[[ 輸出: x 10 注意到,x並沒有保存在a中,而是保存在a的元表的__newindex字段所指向的table里。 --]]
a = {['a'] = 'xx'}
print(rawget(a, 'a')) --輸出xx
使用元表.__index元方法設置元素的默認值
--設置table的默認值: function set_defalut(table, value) local key = {} table[key] = value local mt = {__index = function(table) return table[key] end} setmetatable(table, mt) end table = {} set_defalut(table, 100) print(table.x) --輸出100 --當table中不存在x時,會去找__index字段保存的函數。
function to_string(a) local ret = '' for k, v in pairs(a) do ret = ret..k..':'..v..'\n' end return ret end function add_to_string(a) mt = {__tostring=to_string} setmetatable(a, mt) end table = {['a'] = 'a', ['b'] = 'b'} add_to_string(table) print(table) --[[輸出: a:a b:b --]]
22、點號跟冒號的區別
A = {["b"]="b"}
function A:next(info)
print(info, self.b)
end
function A:first(x)
A:next("call from A:first")
end
A:first('x')
--[[
輸出:call from A:first b
--]]
23、Lua匹配UTF8漢字
demo: head = string.match(line, '([%z\1-\127\194-\244][\128-\191]*).*')
%z表示0,
%z\1-\127\194-\244][\128-\191]* 這是一個漢字的UTF8正則表示,
整個句子是用於獲取line的第一個漢字。
二、擴展&C++
1、使用Lua語言寫Lua擴展模塊,實現兩個數組的並集,並能序列化輸出
模塊的實現:mod.lua
module(..., package.seeall) function union(t1, t2) local ret = {} for k, v in pairs(t1) do ret[v] = true end for k, v in pairs(t2) do ret[v] = true end mt = {__tostring=__tostring} setmetatable(ret, mt) return ret end function __tostring(t) ret = '' for k, v in pairs(t) do ret = ret..k..'\n' end return ret end
模塊的使用 user.lua:
require('mod') t1 = {'a', 'b'} t2 = {'b', 'c'} print(mod.union(t1, t2)) --[[ 輸出: a b c ----]]
上面模塊的代碼,涉及到的知識:擴展模塊編寫、元表\元方法、table遍歷、字符串連接
2、嵌入Lua解釋器
Lua語言天生是為嵌入存在,嵌入也是很簡單的事情。
讓你的可執行程序能夠執行Lua腳本代碼,舉個栗子:
下載:首先要到官網下載Lua源碼:http://www.lua.org/download.html,我下載了最新的Lua-5.3.2版本。
編譯:如果Linux環境下出現錯誤:libreadline.so: undefined reference to `PC,需要修改、src/Makefile,增加-lncurses;可能還需要修改Makefile里的安裝目錄,裝到自己目錄下。
然后就可以開始寫測試代碼了:
makefile:
OBJ=test.o CC=g++ CFLAGS= ROOTDIR=./../ LUADIR=/home/users/cswuyg/install/lua/ LIBS=-llua -ldl test: $(OBJ) $(CC) $(CFLAGS) -o test $(OBJ) -L$(LUADIR)lib $(LIBS) test.o: test.cpp $(CC) $(CFLAGS) -c test.cpp -I$(LUADIR)include
test.cpp
/** * @file test.cpp * @author cswuyg * @date 2015/12/20 12:44:38 * @brief 嵌入Lua解釋器 * **/ extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" } #include <iostream> #include <string> #include <iostream> #include <string> #include <fstream> #include <sstream> //測試 luaL_dofile函數 void test_dofile(lua_State* L, const std::string& file_path) { luaL_dofile(L, "test.lua"); std::string lua_ret = lua_tostring(L, 1); std::cout << "lua ret:" << lua_ret << std::endl; } //測試 luaL_dostring函數 void test_dostring(lua_State* L, const std::string& file_path) { std::ifstream ifs; ifs.open(file_path.c_str()); if (!ifs.is_open()) { return ; } std::stringstream buffer; buffer << ifs.rdbuf(); std::string file_info(buffer.str()); luaL_dostring(L, file_info.c_str()); std::string lua_ret = lua_tostring(L, 1); std::cout << "lua ret:" << lua_ret << std::endl; } int main(int argc, char* argv[]) { lua_State* L = luaL_newstate(); luaL_openlibs(L); test_dostring(L, "test.lua"); test_dofile(L, "test.lua"); lua_close(L); return 0; }
test.lua
function add(a, b) return a + b end c = add(10, 20) return "10 + 20 is: "..c
從上面的test.cpp demo中可以看到關鍵函數:luaL_dostring 或者 luaL_dofile。
嵌入Lua解釋器之后產生的ELF文件,strip(Linux strip命令)之后只有204KB。
Lua解釋器的效率非常高,對於hello world類的demo,只需要200微秒(在我的機器上),而對應的采用V8解釋器執行js hello world需要2毫秒。
對於存在大量重復調用的demo代碼,上了LuaJIT性能可以提高幾倍(如果代碼不存在多次調用,JIT很難提高性能,除非是運行時的其它處理能達到代碼優化效果。測試了一次lua add函數調用,LuaJIT性能沒有提高)。以下是多次重復Lua函數調用測試代碼:
function add(a, b) return a + b end for k = 1, 100000 do c = add(10, k) end return "10 + 20 is: "..c
Lua耗時:10700微秒;
LuaJIT耗時:617微秒;
LuaJIT對於熱點代碼能極大提高運行效率,因為這些熱點代碼被執行后,會被以機器碼的形式保存,后續的多次調用不需要再走轉換流程,直接執行機器碼,性能提高許多。
JIT結合了靜態編譯和動態解析的好處,在執行動態語言時,會將翻譯過的代碼以機器碼緩存起來,后續的使用就不需要再次翻譯,更多參考JIT知識:https://en.wikipedia.org/wiki/Just-in-time_compilation
如果要使用LuaJIT,源碼下載地址:http://luajit.org/download.html,我上面測試使用的是LuaJIT-2.0.4;使用方式跟非JIT Lua方式一致,使用JIT的ELF文件比非JIT的大200KB。
3、使用C++語言寫Lua擴展模塊
以下內容可能會比較碎,解決問題的方法不是不唯一的。
Lua調用C++函數,或者C++函數調用Lua,最重要的技術點就是兩種語言的‘溝通’,Lua的C++語言跟Lua語言數據交互是通過一個叫做Lua 狀態機(lua_State)的東西來做的,可以把它看作一個‘棧’。
我在練習寫擴展模塊的時候,基本按照這樣的層次來寫:實現層、包裝層、注冊層。這對業務功能做了划分,也對腳本語言層面的擴展做了划分,后續有新業務功能開發,或者新腳本語言擴展,已經有的業務功能實現不需要改動。
在用C++為Lua寫擴展時,有兩種使用方式,一種是提供一個so,Lua程序去require后使用,另一種是我寫的擴展類型可以跟Lua的內置類型一樣使用。我做了第二種實現的demo,如果要修改為使用so導出,只要增加一個生成so的makefile就可以。
C++寫擴展的demo代碼見github:自定義C++類加入Lua,注:這個例子是將Lua解釋器嵌入到應用程序中,由我的應用程序去執行Lua腳本代碼。
至少有兩種方式可以將自定義函數/類型加入到全局表中,
方式1:
//把lib->func執行的結果作為lib->name的value保存到package.loaded 中 luaL_requiref(L, lib->name, lib->func, 1); //清空狀態機L lua_settop(L, 0);
方式2:
lib->func(L);//執行注冊,L棧頂是注冊了函數的table lua_setglobal(L, lib->name); //把棧頂的table作為value,以lib->name 作為key保存到全局表中。
詳細代碼見這里。
在編寫Lua擴展時,如果有需要在擴展模塊中保存數據狀態,可以使用1、全局數據狀態,關鍵字:LUA_REGISTRYINDEX;2、模塊環境,關鍵字:LUA_ENVIRONINDEX;3、閉包函數,關鍵字:lua_pushcclosure;學習資料:http://www.cnblogs.com/stephen-liu74/archive/2012/07/25/2470025.html
4、借助swig框架實現Lua擴展模塊
swig:http://nchc.dl.sourceforge.net/project/swig/swig/swig-3.0.7/swig-3.0.7.tar.gz,下載之后配置--prefix,做編譯安裝即可。
文檔:http://www.swig.org/Doc2.0/Lua.html
以下為demo,由兩部分組成:我們的實現代碼;xxx.i文件,用於給swig生成框架。
my_module.i (生成wrapper文檔命令行:swig -c++ -lua my_module.i)
%module my_module %include "std_string.i" %{ #include "my_module.h" %} %include "my_module.h"
注意到上面的std_string.i,必須有它才能導出std::string類型,更多的其它類型在這里swig的github。
my_module.h
/** * @file my_module.h * @author cswuyg * @date 2015/12/20 18:05:05 * @brief 我的測試模塊 * **/ #ifndef __MY_MODULE_H_ #define __MY_MODULE_H_ #include <string> int add(int a, int b); class Hello { public: std::string get_info(); bool set_test(Hello* p); Hello* get_test(); private: Hello* _test_p; }; #endif //__MY_MODULE_H_ /* vim: set expandtab ts=4 sw=4 sts=4 tw=100: */
test.lua
a = my_module.Hello() b = my_module.Hello() a:set_test(b); c = a:get_test(); print(c:get_info()); print(my_module.add(1, 2))
完整代碼見github:利用swig做封裝。注:這個例子是將Lua解釋器嵌入到應用程序中,由我的應用程序去執行Lua腳本代碼。
使用swig框架可以讓開發者把精力放在業務層面,省力不少,除了swig,還有Luabind、LuaPlus...等一堆框架/工具。我選擇swig做,是因為swig也支持Python、JavaScript...
三、Lua練手
1、遍歷成語詞典,獲取某個字開頭的成語,用到了MongoDB第三方lua驅動,它的代碼很簡短,文章最后有介紹。
--[[ 生成某個字開頭的所有成語 cswuyg 2015.12.1 思路: 1、先在外面把成語詞典轉換為UTF8格式; 2、使用正則表達式剔除非四字成語; 3、使用正則找出四字成語的首字 ----]] require('math') mongo = require "mongo" client = mongo.client{ host = 'xxxx host', port='27017' } insert_to_mongo = function(t) --這里是單條寫入,批量寫入會快點,但mongo驅動在批量寫入時有core,暫不使用批量 client.idioms_solitaire_dataxx.head:insert(t) local r = client:runCommand('getLastError', 1, 'w', 1) end -- 操作控制 idioms_ctrl = { idioms_path = nil , -- 成語詞典路徑 idioms = {}, -- 內存成語詞典 idioms_len = 0, -- 成語詞典長度 head_table = {} } --建立head 詞典 function idioms_ctrl:build_head_dict() self.head_table = {} for k, v in ipairs(self.idioms) do head = string.match(v, '([%z\1-\127\194-\244][\128-\191]*).*') if self.head_table[head] then table.insert(self.head_table[head], v) else self.head_table[head] = {v} end end end -- 加載字典到idioms中,並創建head_dict function idioms_ctrl:load_dict() local file = assert(io.open(self.idioms_path, 'r')) for line in file:lines() do local idiom = {} local_, count = string.gsub(line, "[^\128-\193]", "") --統計有多少漢字 if count == 4 then --只保留4字成語 for uchar in string.gmatch(line, "[%z\1-\127\194-\244][\128-\191]*") do table.insert(idiom, uchar) end item = table.concat(idiom) if string.len(item) > 0 then table.insert(self.idioms, table.concat(idiom)) self.idioms_len = self.idioms_len + 1 end end end idioms_ctrl:build_head_dict() end -- 寫到mongodb和文件 function idioms_ctrl:write_all_head() self.f_w = assert(io.open(self.result_path, 'w')) for head, idioms in pairs(self.head_table) do idioms_ctrl:write_result(head, idioms) end end -- 寫入結果到結果文件 function idioms_ctrl:write_result(head, list) self.f_w:write(head..':'..table.concat(list, ',')..' len:'..#list..'\n') info = table.concat(list, ',') xx = {['_id']=head, ['data']=info, ['len']=#list} insert_to_mongo(xx) return false end -- 全部跑 function run(idioms_dict_path, result_path) print('dict :'..idioms_dict_path) idioms_ctrl.result_path = result_path idioms_ctrl.idioms_path = idioms_dict_path idioms_ctrl:load_dict() idioms_ctrl:write_all_head() end -- 主函數 function main() math.randomseed( tonumber(tostring(os.time()):reverse():sub(1,6)) ) local s = os.clock() if not arg[1] or not arg[2] then print('input format: lua xx.lua [idioms_dict_path] [result_path]') return false end print('arg[1]:'..arg[1]) print('arg[2]:'..arg[2]) run(arg[1], arg[2]) local e = os.clock() print('cost second:'..e-s) end main()
補充:
1、Lua5.1版本的string.gfind,在5.2版本中更換為string.gmatch.
2、luaL_register函數可以將C函數注冊到Lua語言環境的package.loaded中,這個函數在Lua5.2中被廢棄,不再支持直接注冊到全局表中,一般更換為使用:luaL_newlib,將函數注冊到堆棧頂的table,然后再另行處理(如使用lua_setglobal)。
3、Lua的MD5庫,到github上找一個,然后設置到LUA_PATH上即可,用過的MD5庫:https://github.com/kikito/md5.lua。
后續工作重點已經不在Lua上,但會不定期更新。
本文所在:http://www.cnblogs.com/cswuyg/p/5049935.html
學習資料推薦:
1、《Lua程序設計》
2、http://www.jellythink.com/archives/category/language/lua
3、http://www.lua.org/manual/5.2/manual.html
4、雲風的MongoDB Lua驅動,代碼精致可讀:https://github.com/cloudwu/lua-mongo/,編譯時需要Lua5.2版本,可能需要修改makefile文件
