標籤:blog http io os 使用 java strong for 資料
這裡, 簡單的記錄一下lua中閉包的知識和C閉包調用
前提知識: 在lua api小記2中已經分析了lua中值的結構, 是一個 TValue{value, tt}組合, 如果有疑問, 可以去看一下
一些重要的資料結構
lua中有兩種閉包, c閉包和lua閉包
兩種閉包的公用部分:
#define ClosureHeader CommonHeader; lu_byte isC; lua_byte nupvalues; GCObject* gclist; struct Table env
/*是否是C閉包*/ /*upval的個數*/ /* 閉包的env, set/getenv就是操縱的它 */
C閉包的結構
struct CClosure{
ClosureHeader;
lua_CFunction f;
TValue upvalue[1];
}
結構比較簡單, f是一個滿足 int lua_func(lua_State*) 類型的c函數
upvalue是建立C閉包時壓入的upvalue, 類型是TValue, 可以得知, upvalue可以是任意的lua類型
Lua閉包結構
struct LClosure{
ClosureHeader;
strcut Proto* p;
UpVal* upvals[1];
}
Proto的結構比較複雜, 這裡先不做分析
統一的閉包結構, 一個聯合體, 說明一個閉包要麼是C閉包, 要麼是lua閉包, 這個是用isC表識出來的.
union Closure{
CClosure c;
LClosure l;
}
糾結的閉包
為什麼大家叫閉包, 不叫它函數, 它看起來就是函數啊? 為什麼要發明一個"閉包"這麼一個聽起來蛋疼的詞呢? 我也糾結在這裡好久了, 大概快一年半了吧~~~=.=我比較笨~~~隨著看源碼, 現在想通了, 拿出一些的自己在研究過程中的心得[盡量的通俗易懂]:
1. c 語言中的函數的定義: 對功能的抽象塊, 這個大家沒什麼異議吧.
2. lua對函數做了擴充:
a. 可以把幾個值和函數綁定在一起, 這些值被稱為upvalue.
ps: 可能有人覺得c++的函數對象也可以把幾個值和函數綁定起來啊, 是這樣的, 但是這個問題就像是"在彙編中也可以實現物件導向呀"一樣, lua從語言層面對upvalue提供了支援, 就像c++/java從語言層面提供了對類, 對象的支援一樣, 當然大大的解放了我們程式員的工作量, 而且配上lua動態類型, 更是讓人輕鬆了不少.
b. 每個函數可以和一個env(環境)綁定.
ps: 如果說上面的upvalue還能在c++中coding出來, 那麼env 上下文環境這種動態語言中特有的東西c++就沒有明顯的對應結構了吧? 可能有人覺得lua是c寫的, 通過coding也可以實現, 好吧=.= , "能做和做"是兩碼事, 就想你能步行從北京到上海, 不表明你就必須要這麼做. env是非常重要和有用的東西, 它可以輕鬆創造出一個受限的環境, 就是傳說中的"沙箱", 我說的更通俗一點就是"一個動態名字空間機制". 這個先暫時不分析.
好了, 現在我們看到
c 函數 { 功能抽象 }
lua 閉包 {功能抽象, upvalue, env}
重點: 閉包 == {功能抽象, upvalue, env}
看到這裡, 大家都明白了, 如果把lua中的{功能抽象, upvalue, env}也稱為函數, 不但容易引起大家的誤解以為它就是和c函數一樣, 而且它確實不能很好的表達出lua函數的豐富內涵, 閉包, "閉" 是指的它是一個object, 一個看得見摸得著的東西, 不可分割的整體(first class); "包" 指的是它包含了功能抽象, upvalue, env. 這裡一個很有趣的事實就是, {功能抽象, upvalue, env}是很多動態語言的一個實現特徵, 比如lua, javascript都有實現這樣的結構, 它是先被實現出來, 然后冠以"閉包"這樣一個名稱. 所以, 你單單想去理解閉包這個詞的話, 基本是沒有辦法理解的, 去網上查閉包, 沒用, 你能查到的就是幾個用閉包舉出的例子, 看完以後保證你的感覺是"這玩意挺神秘的, 但是還是不懂什麼是閉包", 為什麼不懂? 因為它指的是一種實現結構特徵, 是為了實現動態語言中的函數first class和上下文概念而創造出來的.
寧可多說幾句, 只要對加深理解有好處就行, 有這樣兩個個句子"我騎車去買點水果" "我用來閉包{功能抽象, upvalue, env}實現動態語言中的函數first class和上下文概念" , 閉包和"騎車"都是你達到目地的一種手段, 為了買水果你才想了"騎車"這樣一個主意, 並不是為了騎車而去買水果. 只把把眼睛盯在騎車上是不對的, 它只是手段.
向lua中註冊c函數的過程是通過lua_pushcclosure(L, f, n)函數實現的
流程: 1. 建立一個 sizeof(CClosure) + (n - 1) * sizeof(TValue)大小的記憶體, 這段記憶體是 CClosure + TValue[n], 並做gc簿記[這點太重要了, 為什麼lua要控制自己世界中的所有變數, 就是因為它要做gc簿記來管理記憶體], isC= 1 標示其是一個C閉包.
2. c->f = f綁定c函數. --------- 閉包.功能抽象 = f
3. env = 當前閉包的env[這說明了被建立的閉包繼承了建立它的閉包的環境]. ----------- 閉包.env = env
4. 把棧上的n個元素賦值到c->upvalue[]數組中, 順序是越先入棧的值放在upvalue數組的越開始位置, c->nupvalues指定改閉包upvalue的個數. ---------- 閉包.upvalue = upvalue
5. 彈出棧上n個元素, 並壓入建立的Closure到棧頂.
整個流程是比較簡單的, 分配記憶體, 填寫屬性, 鏈入gc監控, 綁定c函數, 綁定upvalue, 綁定env一個C閉包就ok了, 請結合上面給的閉包的解釋, 很清楚了.
現在來解析這個C閉包被調用的過程[注意, 這裡只涉及C閉包的調用]
lua 閉包調用資訊結構:
struct CallInfo {
StkId base; /* base for this function */ ---- 閉包調用的棧基
StkId func; /* function index in the stack */ ---- 要調用的閉包在棧上的位置
StkId top; /* top for this function */ ---- 閉包的棧使用限制, 就是lua_push*的時候得看著點, push太多就超了, 可以lua_checkstack來擴
const Instruction *savedpc; ---- 如果在本閉包中再次調用別的閉包, 那麼該值就儲存下一條指令以便在返回時繼續執行
int nresults; /* expected number of results from this function */ ---- 閉包要返回的值個數
int tailcalls; /* number of tail calls lost under this entry */ ---- 尾遞迴用, 暫時不管
}
從注釋就可以看出來, 這個結構是比較簡單的, 它的作用就是維護一個函數調用的有關資訊, 其實和c函數調用的棧幀是一樣的, 重要的資訊base –> ebp, func –> 要調用的函數的棧index, savedpc –> eip, top, nresults和tailcalls沒有明顯的對應.
在lua初始化的時候, 分配了一個CallInfo數組, 並用L->base_ci指向該數組第一個元素, 用L->end_ci指向該數組最後一個指標, 用L->size_ci記錄數組當前的大小, L->ci記錄的是當前被調用的閉包的調用資訊.
下面講解一個c閉包的調用的過程:
情景: c 函數 int lua_test(lua_State* L){
int a = lua_tonumber(L, 1);
int b = lua_tonumber(L, 2);
a = a + b;
lua_pushnumber(L, a);
}
已經註冊到了lua 中, 形成了一個C閉包, 起名為"test", 下面去調用它
luaL_dostring(L, "c = test(3, 4)")
1. 首先, 我們把它翻譯成對應的c api
1. 最初的堆棧
lua_getglobal(L, “test”)
lua_pushnumber(L, 3)
lua_pushnumber(L, 4)
2. 壓入了函數和參數的堆棧
lua_call(L, 2, 1)
3. 調用lua_test開始時的堆棧
4. 調用結束的堆棧
lua_setglobal(L, “c”)
5. 取出調用結果的堆棧
我們重點想要知道的是lua_call函數的過程
1. lua的一致性在這裡再一次的讓人震撼, 不管是dostring, 還是dofile, 都會形成一個閉包, 也就是說, 閉包是lua中用來組織圖的基本構件, 這個特點使得lua中的結構具有一致性, 是一種簡明而強大的概念.
2. 根據1, a = test(3, 4)其實是被組織成為一個閉包放在lua棧頂[方便期間, 給這個lua閉包起名為bb], 也就說dostring真正調用的是bb閉包, 然後bb閉包執行時才調用的是test
[儲存當前資訊到當前函數的CallInfo中]
3. 在調用test的時刻, L->ci記載著bb閉包的調用資訊, 所以, 先把下一個要執行的指令放在L->ci->savedpc中, 以供從test返回後繼續執行.
4. 取棧上的test C閉包 cl, 用 cl->isC == 1斷定它的確是一個C閉包
[進入一個新的CallInfo, 布置堆棧]
5. 從L中新分配一個CallInfo ci來記錄test的調用資訊, 並把它的值設定到L->ci, 這表明一個新的函數調用開始了, 這裡還要指定test在棧中的位置, L->base = ci->base = ci->func+1, 注意, 這幾個賦值很重要, 導致的堆棧狀態由圖2轉化到圖3, 可以看出, L->base指向了第一個參數, ci->base也指向了第一個參數, 所以在test中, 我們調用lua_gettop函數返回的值就是2, 因為在調用它的時候, 它的棧幀上只有2個元素, 實現了lua向c語言中傳參數.
[調用實際的函數]
6. 安排好堆棧, 下面就是根據L->ci->func指向的棧上的閉包(及test的C閉包), 找到對應的cl->c->f, 並調用, 就進入了c函數lua_test
[擷取傳回值調整堆棧, 返回原來的CallInfo]
7. 根據lua_test的傳回值, 把test閉包和參數彈出棧, 並把傳回值壓入並調整L->top
8. 恢複 L->base, L->ci 和 L->savedpc, 繼續執行.
總結: 調用一個新的閉包時 1. 儲存當前資訊到當前函數的CallInfo中 2. 進入一個新的CallInfo, 布置堆棧 3. 調用實際的函數 4. 擷取傳回值調整堆棧, 返回原來的CallInfo
lua 函數調用1 -- 閉包詳解和C調用