Lua中閉包詳解 來自RingOfTheC[ring.of.the.c@gmail.com]

這些東西是平時遇到的, 以爲有必定的價值, 因此記錄下來, 之後遇到相似的問題能夠查閱, 同時分享出來也能方便須要的人, 轉載請註明來自RingOfTheC[ring.of.the.c@gmail.com]javascript

 

這裏, 簡單的記錄一下lua中閉包的知識和C閉包調用java

前提知識: 在lua api小記2中已經分析了lua中值的結構, 是一個 TValue{value, tt}組合, 若是有疑問, 能夠去看一下c++

 

一些重要的數據結構

 

    lua中有兩種閉包, c閉包和lua閉包程序員

    兩種閉包的公共部分:api

       #define ClosureHeader CommonHeader;     lu_byte isC;        lua_byte nupvalues; GCObject* gclist;       struct Table env數組

                                                                  /*是不是C閉包*/     /*upval的個數*/                                   /* 閉包的env, set/getenv就是操縱的它 */數據結構

 

    C閉包的結構閉包

        struct CClosure{函數

             ClosureHeader;post

             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

             lua3

                                1. 最初的堆棧

 

               lua_getglobal(L, 「test」)

               lua_pushnumber(L, 3)

               lua_pushnumber(L, 4)      

             lua2

                              2. 壓入了函數和參數的堆棧

 

               lua_call(L, 2, 1)

             lua5

                               3. 調用lua_test開始時的堆棧

 

             lua4

                               4. 調用結束的堆棧

 

               lua_setglobal(L, 「c」)     

             lua3

                               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

 

短短續續一共寫了6個小時......... 先寫到這裏吧, 對了, 我是開始寫blog不久, 並且表達能力有限, 你們若是要看的話取其精華, 捨棄糟粕吧:)

相關文章
相關標籤/搜索