Lua API深入分析(一)

2021-02-21 黑斑馬團隊

lua是一種嵌入式的語言,意味著lua可以用來嵌入其他應用的程序。lua API是lua與宿主語言(通常為C/C++)通訊交互的橋梁,是一個C/C++代碼與lua進行交互的函數集。lua共約有一百多個API(如lua5.3有129個),主要分為stack操作、函數導入與調用、table操作、協程、調試5類,本文分析stack操作、函數調用、table操作3類的功能與使用要點。

準備知識:1.lua_State

在lua API中,幾乎所有函數的都有一個lua_State類型的參數,lua_State數據包括當前腳本環境的運行狀態信息,還會有gc相關的信息。luaL_newstate用於創建一個新的lua_State,lua API調用時操作會在第一個參數傳入的lua_State上進行,lua_close用於關閉釋放lua_State。

2.lua數據類型

lua語言中變量沒有強制類型,但是在執行過程中如果類型不匹配則會產生運行錯誤,而在C/C++中用lua API對lua變量的操作具有強制類型。lua內置類型有LUA_TNIL、LUA_TNUMBER、LUA_TBOOLEAN、LUA_TSTRING、LUA_TTABLE、LUA_TFUNCTION、LUA_TUSERDATA、LUA_TTHREAD、LUA_TLIGHTUSERDATA 9種。lua_type、lua_typename兩個函數可以用來識別一個變量數據類型,lua_type返回一個指示類型的整數,lua_typename能夠依據該整數返回字符串,用lua_typename(L, lua_type(L, index))可以獲取指定棧元素的類型字符串表示。

3.userdata

lua與C/C++交互時可以用userdata類型變量傳遞數據,userdata允許將C/C++中的數據保存在lua變量中。 userdata類型的值是一個內存塊, 有兩種: 完全用戶數據(full user data),指一塊由lua管理的內存對應的對象;輕量用戶數據(light user data),則指一個簡單的C指針。 用戶數據在lua中除了賦值與相等性判斷之外沒有其他預定義的操作,使用元表可以給完全用戶數據定義一系列的操作。userdata只能通過C API而無法在lua代碼中創建或者修改,數據僅被宿主程序所控制。userdata操作API有lua_newuserdata、lua_pushlightuserdata、lua_touserdata、lua_isuserdata、lua_islightuserdata,在使用lua_newuserdata函數創建userdata空間時,一種常見的寫法是採用replacement new:

new(lua_newuserdata(L,sizeof(int))) int(10);

寫法等效於

int* udptr=(int*)lua_newuserdata(L,sizeof(int);*udptr=10;

創建的userdata可以形成命名變量或者關聯到table欄位或者作為函數upvalue使用。

stack操作類API:

lua API裡一類重要操作是對棧(stack)的操作,lua使用一個虛擬棧來和C互傳數據。棧上的的每個元素都是一個lua數據,對棧的API查詢操作都不嚴格遵循棧的操作規則。statck對lua語言層是透明的。

棧索引

API中的函數若需要傳入棧索引,這個索引必須是有效索引或是可接受索引。有效索引指引用棧內真實位置的索引; 即在1到棧頂之間的位置 (1 ≤ abs(index) ≤ top)。正索引處棧底的1往棧頂方向增長,負索引為從棧頂第一個元素的-1絕對值依次增大,例如棧頂元素的負索引表示為-1,次棧頂元素為-2。棧中元素即可以使用正索引也可以使用負所以進行訪問。

對棧操作的API主要有三類:入棧、出棧、取值。

入棧

入棧類函數用於將指定數據放到lua棧頂,對不同數據類型有不同的API函數, lua_pushboolean、lua_pushcclosure、lua_pushcfunction、lua_pushfstring、lua_pushglobaltable、lua_pushinteger、lua_pushlightuserdata、lua_pushlstring、lua_pushnil、lua_pushnumber 這些函數作用相同但是操作的數據類型不統一,在實際中常常將其採用模板形式的形式將其封裝。 還有兩個棧操作函數lua_pushvalue、lua_insert,將棧頂元素移動到其它位置。

出棧

出棧函數從lua棧頂彈出一定數量元素,函數只有lua_pop。

取值

取值函數可以從lua棧中取處元素值,函數有 lua_toboolean、lua_tocfunction、lua_tointeger、lua_tointegerx、lua_tolstring、lua_tolstring、lua_tonumber、lua_tonumberx、lua_topointer、lua_tostring,這些函數從lua棧指定位置處取得數據,不影響棧中數據。

除了上述API顯性主動地從lua棧中插入或彈出元素,其它API在執行過程中也會附帶從棧中彈出或插入數據。

其它棧函數

另外對棧上元素還有的操作函數有: lua_copy 棧元素複製 lua_concat 棧元素連接 lua_compare 棧元素比較 lua_arith 對棧元素執行運算 lua_replace 元素賦值替換

棧元素命名與命名元素壓棧

對棧中的數據,可以對其命名形成lua符號使該數據可以在lua語言中訪問或通過接口訪問,函數為lua_setglobal。原型為 void lua_setglobal (lua_State *L, const char *name); 函數會將棧頂的元素命名為以第二個參數命名的lua符號,並且將該棧頂元素出棧。

cpp:lua_pushstring(L, "lua");lua_setglobal("var_a");lua_pushnumber(L, 123);lua_setglobal("var_b");luaL_dofile("1.lua")1.lua:print(var_a)print(var_b)

上邊代碼等效於lua代碼:

var_a="lua"var_b=123print(var_a)print(var_b)

由lua_setglobal形成的lua語言符號與在lua語言中定義的符號在後續使用中是一致的。 對於已有的lua符號,可以使用lua_getglobal對該變量訪問,該變量會被放置到棧頂。

2.lua:var_c=123cpp:luaL_dofile("2.lua")lua_getglobal(L, "var_c")int c=lua_tonumber(L,-1)std::out<<c;

上述代碼中c++從lua語言中定義的變量符號取得數據。

函數導入與調用:

C/C++函數導入到lua C/C++中的函數可通過lua_pushcfunction函數導入到lua中。函數原型為

void lua\_pushcfunction (lua\_State *L, lua\_CFunction f);

其中lua_CFunction定義為

typedef int (*lua\_CFunction) (lua\_State *L);

是一個函數指針,函數會將一個C函數壓棧。 這個函數接收一個C函數指針,並將一個類型為LUA_TFUNCTION的lua值壓棧。

3.lua:print(csum(2,3))cpp:int csum_for_lua(lua_State* l){int a = lua_tointeger(l,1);int b = lua_tointeger(l,2);lua_pushinteger(l,a+b);return 1;}lua_pushcfunction(lua_state,csum_for_lua); lua_setglobal(lua_state,"csum");luaL_dofile(lua_state,"3.lua");

當C函數被創建出來,我們有可能會把一些值關聯在一起,也就是創建一個C閉包;這些被關聯起來的值被叫做上值,它們可以在函數被調用的時候訪問的到。無論何時去調用C函數,函數的上值都可以用偽索引定位。 我們可以用lua_upvalueindex這個宏來生成這些偽索引。 第一個關聯到函數的值放在lua_upvalueindex(1) 位置處,依此類推。 lua_pushcclosure創建新的C閉包並且壓到棧頂。函數原型為

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n)

第二個參數為c函數指針,第三個參數指定有多少個值需要關聯到該函數上,lua_pushcclosure 也會把這些值從棧上彈出。 代碼段:

4.luaprint(ccnt())print(ccnt())print(ccnt())cpp:int cnt(lua_State* L) { int val = lua_tointeger(L,lua_upvalueindex(1));//pesudo-index access lua_pushinteger(L,++val); lua_pushvalue(L,-1); lua_replace(L,lua_upvalueindex(1));//assign element value return 1;}int newcnt(lua_State* L){ lua_pushinteger(L,0); lua_pushcclosure(L,cnt,1); lua_setglobal(L,"ccnt"); return 1;} newcnt(lua_state); luaL_dofile(lua_state,"4.lua");

輸出1 2 3。C閉包裡的upvalue作用與C函數裡的靜態變量類似。

void lua_call (lua_State *L, int nargs, int nresults); C/C++中調用lua函數時,要調用的函數應該被壓入棧; 接著,把需要傳遞給這個函數的參數按正序壓棧; 這是指第一個參數首先壓棧。 最後調用一下 lua_call; nargs是你壓入棧的參數個數。 當函數調用完畢後,所有的參數以及函數本身都會出棧。 而函數的返回值這時則被壓棧。 返回值的個數將被調整為nresults個,除非 nresults 被設置成LUA_MULTRET。 在這種情況下,所有的返回值都被壓入堆棧中。 lua會保證返回值都放入棧空間中。 函數返回值將按正序壓棧(第一個返回值首先壓棧),因此在調用結束後,最後一個返回值將被放在棧頂。這裡的lua函數既可以是lua語言中的函數,也可以是通過在C/C++中通過lua_pushcfunction、lua_pushcclosure形成的函數。與lua_call類似的還有xpcall、lua_pcall、lua_callk、lua_pcallk等函數,這些函數還有錯誤處理等功能稱之為lua_call系列函數。通過lua_call類的函數可以在c/c++中執行lua函數。 如代碼:

5.lua:function lua_add (x, y) return x+yendcpp:int c_add (lua_State* L,int x, int y ) { int sum; lua_getglobal(L, "lua_add"); lua_pushnumber(L, x); lua_pushnumber(L, y); lua_call(L, 2, 1); sum = (int)lua_tonumber(L, -1); lua_pop(L, 1); return sum; }

函數c_add裡面調用了lua函數lua_add求和。調用時先取出lua_add函數的符號放到棧中,然後將lua函數的參數依次入棧,然後執行luacall(L, 2, 1),其中2即為lua函數參數個數,1為返回參數個數。調用完lua函數後,結果放在棧頂,取值後使用luapop清除在棧頂的結果。

table操作類:

table的操作有創建、欄位訪問、欄位賦值、遍歷、元表存取幾類。 table創建函數為 void lua_newtable (lua_State *L); 創建一個table並將其壓棧。 table欄位訪問函數有

int lua_rawget (lua_State *L, int index);void lua_rawgeti (lua_State *L, int index, lua_Integer i);int lua_gettable (lua_State *L, int index);int lua_getfield (lua_State *L, int index, const char *k);

lua_rawget調用時第二個參數指定table,並以棧頂元素作為key,獲取table[key]後放到棧頂。lua_rawseti與lua_rawget類似,但是key是從第三個參數使用字符串傳入的。lua_rawget和lua_rawgeti在獲取table的欄位時不會觸發"index"元方法(如果設定)。lua_gettable與lua_rawget類似,但是會觸發"index"元方法(如果設定),lua_getfield與lua_gettable一樣會觸發"index"元方法,不同在於lua_getfield的key是從第三個參數傳入的。

table欄位賦值函數有

void lua_rawset (lua_State *L, int index);void lua_rawseti (lua_State *L, int index, lua_Integer i);void lua_settable (lua_State *L, int index);void lua_setfield (lua_State *L, int index, const char *k);

lua_rawset調用時第二個參數指定table,並以棧頂元素作為value,次棧頂元素為key,執行table[key]=value,設置完成後從棧中移除key與value。lua_rawseti與lua_rawset類似,但是key是從第三個參數傳入的。lua_rawset和lua_rawseti在獲取table的欄位時不會觸發"newindex"元方法(如果已設定)。lua_settable與lua_rawset類似,但是會觸發"newindex"元方法(如果已設定),lua_setfield與lua_settable一樣會觸發"newindex"元方法,不同在於lua_setfield的key是從第三個參數傳入的。

table欄位賦值函數使用示例代碼:

cpp:lua_newtable(L);lua_pushstring(L, "a");lua_pushstring(L, "lua");lua_rawset(L, -3);lua_pushstring(L, "b");lua_pushinteger(L,123);lua_rawset(L, -3);lua_setglobal(L, "c");

等價lua代碼為:

c ={["a"]="lua",["b"]=123}

table遍歷

遍歷table主要函數為lua_next與lua_len。lua_next原型為 int lua_next (lua_State *L, int index); 從棧頂彈出一個鍵, 然後把索引指定的表中的一個鍵值對壓棧 (彈出的鍵之後的 「下一」 對)。 如果表中已無待遍曆元素, 那麼lua_next 將返回 0 (什麼也不壓棧)。 如下代碼可對棧頂table進行遍歷訪問:

6.lua:t={1,true,"1"}cpp:void traveltable(lua_State *L){ int nIndex = lua_gettop(L); lua_pushnil(L); while(0!= lua_next(L,nIndex)) { //here stack element index -2 is key,-1 is value lua_pop( L, 1 ); }}luaL_dofile(lua_state,"6.lua");lua_getglobal(L, "t");traveltable(L);

與table遍歷相關lua_len函數可以獲取原型為 void lua_len (lua_State *L, int index); 返回給定索引的值的長度,函數與lua語言中「#」操作等價。

元表操作

通常lua中的每個值都有一套預定義的操作集合,可以通過元表來修改一個值得行為,使其在面對一個非預定義的操作時執行一個指定的操作。這些操作稱為元方法,定義這些操作的集合叫做元表。lua中的table與userdata類型的數據可以設置元表,元表操作API有lua_getmetatable與lua_setmetatable,分別用戶獲取和設置元表。 int lua_getmetatable (lua_State *L, int index); 如果該索引處的值有元表,則將其元表壓棧,返回1 。 否則不會將任何東西入棧,返回0。 void lua_setmetatable (lua_State *L, int index); 把一張表彈出棧,並將其設為給定索引處的值的元表。

總結

stack操作、函數導入與調用、table操作是lua API中的主要部分,理解了這幾類lua API的函數工作方式後,可以理解常見的lua/C++綁定通信庫工作原理,甚至可以修改或封裝出我們自己的lua/C++綁定通信庫。

長按關注黑斑馬微信公眾號

歡迎訪問「百納遊戲團隊」技術博客:http://blog.dolphin-game.com

相關焦點

  • 我們能用lua做什麼
    你也許會說,我可以寫nginx配置,讓它分析請求參數,再做相應的邏輯處理。但是這樣做的話,想想看你的nginx配置會有多麼的複雜,多麼的難以維護,而且就算你這樣做,也不能解決所有的問題。比如說我有個接口要輸出json字符串,或是其它別的格式,你總不能說我再去寫個nginx擴展讓它支持json吧。
  • Nginx與Lua的執行順序和步驟說明
    一、Nginx執行步驟Nginx處理每一個用戶請求時,都是按照若干個不同階段(phase)依次處理的,而不是根據配置文件上的順序。
  • 深入解析Lua腳本加密技術,給遊戲代碼加上「緊箍咒」
    本文將聚焦Lua腳本加密,深入闡述Lua常見的三種加密方式,並探索如何進一步的保護Lua代碼。一、背景「Lua」在葡萄牙語中是「月亮」的意思,1993年由巴西的Pontifical Catholic University開發。
  • redis源碼學習之lua執行原理
    腳本完事,如下:if redis.call("get",KEYS[1]) == ARGV[1]then return redis.call("del",KEYS[1])else return 0end初識eval apiEVAL script numkeys
  • 【Lua篇】《Lua程序設計》全書知識萬字總結!
    function rename (arg)return os.rename(arg.old,arg.new)endrename{old = "temp.lua",new = "temp1.lua"}當實參只有一個table時,可以省略圓括號。第6章 深入函數在lua中,函數是一種第一類值。
  • Lua_Real_Programming
    當使用loadfile加載上述兩個文件時,加載foo1.lua後foo函數是沒有被定義,需要執行之後才可使用,而foo2.lua被加載後foo可直接使用,如下:模塊在lua中不是first-class,其不能作為參數傳遞給函數。require加載模塊時,會從package.path路徑中去搜索,如果找到了對應文件,就通過loadfile進行加載。
  • 國內大講堂-Lua培訓(上篇)
    上篇課程大綱如下:一、應用場景二、基本語法三、環境部署四、課堂練習應用場景2.1 所有語言都開始於Hello World如果你安裝了lua,那麼可以使用lua命令,進入到lua的交互模式中。之後輸入的內容會被lua執行,比如列印了一個字符串Hello World。lua的注釋語法很簡單:單行注釋 -- 多行注釋 --[[  注釋內容 --]]2.2 Lua的數據類型有8種數據類型,如下表:
  • Lua 5.3.0 (beta) 發布
    Lua 5.3.0 (beta) 發布,此版本現已提供測試:http://www.lua.org/work/lua-5.3.0-
  • 移植Lua到ARM平臺
    交叉編譯器:arm-linux-gcc 4.3.2版本(如果你不知道怎麼搭建交叉編譯環境,可以參考我的另一篇文章:http://hi.baidu.com/hqwfreefly/blog/item/8364fed7b3f58dc950da4b5f.html)3.Lua5.1版本源碼移植步驟:1下載Lua原始碼:http://www.lua.org
  • 基於ngx_lua模塊的WAF開發實踐
    0x00 常見WAF簡單分析WAF主要分為硬體WAF和軟體防火牆,硬體WAF如綠盟的NSFOCUS Web Application Firewall,軟體防火牆比較有名的是ModSecurity,再就是代碼級別的ngx_lua_waf。
  • Socket網絡編程核心API深入分析(一):bind函數
    本文轉載自【微信公眾號:小碼逆襲,ID:gh_7c5a039380a0】經微信公眾號授權轉載,如需轉載與原文作者聯繫本篇文章你能學到:1、實現簡單的c++版本的伺服器和客戶端2、深入理解bind注意:本片文章涉及到的內核源碼來自linux內核版本3.6簡單的伺服器與客戶端實現本篇文章的重點在於從底層深入分析bind()函數,相信已經能夠自己實現一個簡單的伺服器和客戶端並進行交互,下面是一個簡單的demo,幫助大家複習一下socket編程api的調用過程。
  • 在Nginx使用Lua擴展功能
    什麼是LUALua從一開始就是作為一門方便嵌入(其它應用程式)並可擴展的輕量級腳本語言來設計的,因此她一直遵從著簡單、小巧、可移植、快速的原則
  • 觸動精靈,觸摸精靈-lua腳本入門
    觸動精靈,觸摸精靈最近比較火,可是沒有lua腳本基礎的人可能上手有點難,這篇經驗就介紹一下lua的入門。lua的入口函數在哪裡?lua沒有入口函數,按照順序 從上到下 執行暴漏在function以外的語句知道了所謂的入口函數,那這裡介紹經典程序 「hello world!」內容如下:print("hello world!"); --我是執行的第一條語句注釋應該怎麼用1、單行注釋中,連續兩個減號"--"表示注釋的開始,一直延續到行末為止。相當於C++語言中的"//"。
  • dlua 0.1 發布,gdb 風格的 lua 調試器
    dlua 是一個類似 gdb 的 lua 調試器。
  • 【物聯網學習---番外篇】Lua腳本編程掃盲
    filename=datatype1&type=luaLua的函數是按照下面格式定義的:function 函數名(參數1, 參數2, ...)   local是lua特有的關鍵詞,表示聲明一個局部變量。
  • 50 個超實用的機器學習API,拿好不謝!
    之前 KDnugget 發布了年度最佳50+機器學習&預測API,整體可分為4類:面部識別&圖像識別文本分析,自然語言處理,情感分析機器翻譯機器學習和預測每個類別下的API按照字母順序列出
  • 從人臉識別到文本分析,50+超實用的 API 推薦清單
    FaceRecthttp://apicloud.me/apis/facerect/demo/一款功能強大且完全免費的人臉檢測 API 。該 API 可在一張照片上查找單個人臉(正面和側面)或多個人臉,並為找到的每個人臉生成 JSON 輸出。此外,FaceRect 可以為每個檢測到的人臉找到人臉特徵(眼睛、鼻子和嘴巴)。
  • LTUI v1.7 發布,一個基於 Lua 的跨平臺字符終端 UI 界面庫
    此框架源於xmake中圖形化菜單配置的需求,類似linux kernel的menuconf去配置編譯參數,因此基於curses和lua實現了一整套跨平臺的字符終端ui庫。 而樣式風格基本上完全參照的kconfig-frontends,當然用戶也可以自己定製不同的ui風格。
  • 動態語言如何做靜態分析
    不僅如此,typedlua還加了不少類型推導/檢查友好的語言機制,比如可空類型、組合類型等等。但是,這種方案應用起來其實就是用一門像lua的方言,現有的lua工程無法無縫遷移。而且,一門新的語言過於重量級,如果背後沒有大公司支撐,讓人只敢當做學術研究,不敢應用在工程中。