溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何進(jìn)行g(shù)opher-lua虛擬機(jī)的原理分析

發(fā)布時間:2022-01-11 14:24:31 來源:億速云 閱讀:321 作者:柒染 欄目:編程語言

這篇文章將為大家詳細(xì)講解有關(guān)如何進(jìn)行g(shù)opher-lua虛擬機(jī)的原理分析,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。

在 GitHub 玩耍時,偶然發(fā)現(xiàn)了gopher-lua ,這是一個純 Golang 實(shí)現(xiàn)的 Lua 虛擬機(jī)。我們知道 Golang 是靜態(tài)語言,而 Lua 是動態(tài)語言,Golang 的性能和效率各語言中表現(xiàn)得非常不錯,但在動態(tài)能力上,肯定是無法與 Lua 相比。那么如果我們能夠?qū)⒍呓Y(jié)合起來,就能綜合二者各自的長處了(手動滑稽。

在項(xiàng)目 Wiki 中,我們可以知道 gopher-lua 的執(zhí)行效率和性能僅比 C 實(shí)現(xiàn)的 bindings 差。因此從性能方面考慮,這應(yīng)該是一款非常不錯的虛擬機(jī)方案。

Hello World

這里給出了一個簡單的 Hello World 程序。我們先是新建了一個虛擬機(jī),隨后對其進(jìn)行了 DoString(...) 解釋執(zhí)行 lua 代碼的操作,最后將虛擬機(jī)關(guān)閉。執(zhí)行程序,我們將在命令行看到 "Hello World" 的字符串。

package main  import (      "github.com/yuin/gopher-lua"  )  func main() {      l := lua.NewState()      defer l.Close()      if err := l.DoString(`print("Hello World")`); err != nil {          panic(err)      }  }  // Hello World

提前編譯

在查看上述 DoString(...) 方法的調(diào)用鏈后,我們發(fā)現(xiàn)每執(zhí)行一次 DoString(...) 或 DoFile(...) ,都會各執(zhí)行一次 parse 和 compile 。

func (ls *LState) DoString(source string) error {      if fn, err := ls.LoadString(source); err != nil {          return err      } else {          ls.Push(fn)          return ls.PCall(0, MultRet, nil)      }  }  func (ls *LState) LoadString(source string) (*LFunction, error) {      return ls.Load(strings.NewReader(source), "<string>")  }  func (ls *LState) Load(reader io.Reader, name string) (*LFunction, error) {      chunk, err := parse.Parse(reader, name)      // ...      proto, err := Compile(chunk, name)      // ...  }

從這一點(diǎn)考慮,在同份 Lua 代碼將被執(zhí)行多次(如在 http server 中,每次請求將執(zhí)行相同 Lua 代碼)的場景下,如果我們能夠?qū)Υa進(jìn)行提前編譯,那么應(yīng)該能夠減少 parse 和 compile 的開銷(如果這屬于 hotpath 代碼)。根據(jù) Benchmark 結(jié)果,提前編譯確實(shí)能夠減少不必要的開銷。

package glua_test  import (      "bufio"      "os"      "strings"      lua "github.com/yuin/gopher-lua"      "github.com/yuin/gopher-lua/parse"  )  // 編譯 lua 代碼字段  func CompileString(source string) (*lua.FunctionProto, error) {      reader := strings.NewReader(source)      chunk, err := parse.Parse(reader, source)      if err != nil {          return nil, err      }      proto, err := lua.Compile(chunk, source)      if err != nil {          return nil, err      }      return proto, nil  }  // 編譯 lua 代碼文件  func CompileFile(filePath string) (*lua.FunctionProto, error) {      file, err := os.Open(filePath)      defer file.Close()      if err != nil {          return nil, err      }      reader := bufio.NewReader(file)      chunk, err := parse.Parse(reader, filePath)      if err != nil {          return nil, err      }      proto, err := lua.Compile(chunk, filePath)      if err != nil {          return nil, err      }      return proto, nil  }  func BenchmarkRunWithoutPreCompiling(b *testing.B) {      l := lua.NewState()      for i := 0; i < b.N; i++ {          _ = l.DoString(`a = 1 + 1`)      }      l.Close()  }  func BenchmarkRunWithPreCompiling(b *testing.B) {      l := lua.NewState()      proto, _ := CompileString(`a = 1 + 1`)      lfunc := l.NewFunctionFromProto(proto)      for i := 0; i < b.N; i++ {          l.Push(lfunc)          _ = l.PCall(0, lua.MultRet, nil)      }      l.Close()  }  // goos: darwin  // goarch: amd64  // pkg: glua  // BenchmarkRunWithoutPreCompiling-8         100000             19392 ns/op           85626 B/op         67 allocs/op  // BenchmarkRunWithPreCompiling-8           1000000              1162 ns/op            2752 B/op          8 allocs/op  // PASS  // ok      glua    3.328s

虛擬機(jī)實(shí)例池

在同份 Lua 代碼被執(zhí)行的場景下,除了可使用提前編譯優(yōu)化性能外,我們還可以引入虛擬機(jī)實(shí)例池。

因?yàn)樾陆ㄒ粋€ Lua 虛擬機(jī)會涉及到大量的內(nèi)存分配操作,如果采用每次運(yùn)行都重新創(chuàng)建和銷毀的方式的話,將消耗大量的資源。引入虛擬機(jī)實(shí)例池,能夠復(fù)用虛擬機(jī),減少不必要的開銷。

func BenchmarkRunWithoutPool(b *testing.B) {      for i := 0; i < b.N; i++ {          l := lua.NewState()          _ = l.DoString(`a = 1 + 1`)          l.Close()      }  }  func BenchmarkRunWithPool(b *testing.B) {      pool := newVMPool(nil, 100)      for i := 0; i < b.N; i++ {          l := pool.get()          _ = l.DoString(`a = 1 + 1`)          pool.put(l)      }  }  // goos: darwin  // goarch: amd64  // pkg: glua  // BenchmarkRunWithoutPool-8          10000            129557 ns/op          262599 B/op        826 allocs/op  // BenchmarkRunWithPool-8            100000             19320 ns/op           85626 B/op         67 allocs/op  // PASS  // ok      glua    3.467s

Benchmark 結(jié)果顯示,虛擬機(jī)實(shí)例池的確能夠減少很多內(nèi)存分配操作。

下面給出了 README 提供的實(shí)例池實(shí)現(xiàn),但注意到該實(shí)現(xiàn)在初始狀態(tài)時,并未創(chuàng)建足夠多的虛擬機(jī)實(shí)例(初始時,實(shí)例數(shù)為0),以及存在 slice 的動態(tài)擴(kuò)容問題,這都是值得改進(jìn)的地方。

type lStatePool struct {      m     sync.Mutex      saved []*lua.LState  }  func (pl *lStatePool) Get() *lua.LState {      pl.m.Lock()      defer pl.m.Unlock()      n := len(pl.saved)      if n == 0 {          return pl.New()      }      x := pl.saved[n-1]      plpl.saved = pl.saved[0 : n-1]      return x  }  func (pl *lStatePool) New() *lua.LState {      L := lua.NewState()      // setting the L up here.      // load scripts, set global variables, share channels, etc...      return L  }  func (pl *lStatePool) Put(L *lua.LState) {      pl.m.Lock()      defer pl.m.Unlock()      pl.saved = append(pl.saved, L)  }  func (pl *lStatePool) Shutdown() {      for _, L := range pl.saved {          L.Close()      }  }  // Global LState pool  var luaPool = &lStatePool{      saved: make([]*lua.LState, 0, 4),  }

模塊調(diào)用

gopher-lua 支持 Lua 調(diào)用 Go 模塊,個人覺得,這是一個非常令人振奮的功能點(diǎn),因?yàn)樵?Golang 程序開發(fā)中,我們可能設(shè)計出許多常用的模塊,這種跨語言調(diào)用的機(jī)制,使得我們能夠?qū)Υa、工具進(jìn)行復(fù)用。

當(dāng)然,除此之外,也存在 Go 調(diào)用 Lua 模塊,但個人感覺后者是沒啥必要的,所以在這里并沒有涉及后者的內(nèi)容。

package main  import (      "fmt"      lua "github.com/yuin/gopher-lua"  )  const source = `  local m = require("gomodule")  m.goFunc()  print(m.name)  ` func main() {      L := lua.NewState()      defer L.Close()      L.PreloadModule("gomodule", load)      if err := L.DoString(source); err != nil {          panic(err)      }  }  func load(L *lua.LState) int {      mod := L.SetFuncs(L.NewTable(), exports)      L.SetField(mod, "name", lua.LString("gomodule"))      L.Push(mod)      return 1  }  var exports = map[string]lua.LGFunction{      "goFunc": goFunc,  }  func goFunc(L *lua.LState) int {      fmt.Println("golang")      return 0  }  // golang  // gomodule

變量污染

當(dāng)我們使用實(shí)例池減少開銷時,會引入另一個棘手的問題:由于同一個虛擬機(jī)可能會被多次執(zhí)行同樣的 Lua 代碼,進(jìn)而變動了其中的全局變量。如果代碼邏輯依賴于全局變量,那么可能會出現(xiàn)難以預(yù)測的運(yùn)行結(jié)果(這有點(diǎn)數(shù)據(jù)庫隔離性中的“不可重復(fù)讀”的味道)。

全局變量

如果我們需要限制 Lua 代碼只能使用局部變量,那么站在這個出發(fā)點(diǎn)上,我們需要對全局變量做出限制。那問題來了,該如何實(shí)現(xiàn)呢?

我們知道,Lua 是編譯成字節(jié)碼,再被解釋執(zhí)行的。那么,我們可以在編譯字節(jié)碼的階段中,對全局變量的使用作出限制。在查閱完 Lua 虛擬機(jī)指令后,發(fā)現(xiàn)涉及到全局變量的指令有兩條:GETGLOBAL(Opcode 5)和 SETGLOBAL(Opcode 7)。

到這里,已經(jīng)有了大致的思路:我們可通過判斷字節(jié)碼是否含有 GETGLOBAL 和 SETGLOBAL 進(jìn)而限制代碼的全局變量的使用。至于字節(jié)碼的獲取,可通過調(diào)用 CompileString(...) 和 CompileFile(...) ,得到 Lua 代碼的 FunctionProto ,而其中的 Code 屬性即為字節(jié)碼 slice,類型為 []uint32 。

在虛擬機(jī)實(shí)現(xiàn)代碼中,我們可以找到一個根據(jù)字節(jié)碼輸出對應(yīng) OpCode 的工具函數(shù)。

// 獲取對應(yīng)指令的 OpCode  func opGetOpCode(inst uint32) int {      return int(inst >> 26)  }

有了這個工具函數(shù),我們即可實(shí)現(xiàn)對全局變量的檢查。

package main  // ...  func CheckGlobal(proto *lua.FunctionProto) error {      for _, code := range proto.Code {          switch opGetOpCode(code) {          case lua.OP_GETGLOBAL:              return errors.New("not allow to access global")          case lua.OP_SETGLOBAL:              return errors.New("not allow to set global")          }      }      // 對嵌套函數(shù)進(jìn)行全局變量的檢查      for _, nestedProto := range proto.FunctionPrototypes {          if err := CheckGlobal(nestedProto); err != nil {              return err          }      }      return nil  }  func TestCheckGetGlobal(t *testing.T) {      l := lua.NewState()      proto, _ := CompileString(`print(_G)`)      if err := CheckGlobal(proto); err == nil {          t.Fail()      }      l.Close()  }  func TestCheckSetGlobal(t *testing.T) {      l := lua.NewState()      proto, _ := CompileString(`_G = {}`)      if err := CheckGlobal(proto); err == nil {          t.Fail()      }      l.Close()  }

模塊

除變量可能被污染外,導(dǎo)入的 Go 模塊也有可能在運(yùn)行期間被篡改。因此,我們需要一種機(jī)制,確保導(dǎo)入到虛擬機(jī)的模塊不被篡改,即導(dǎo)入的對象是只讀的。

在查閱相關(guān)博客后,我們可以對 Table 的 __newindex 方法的修改,將模塊設(shè)置為只讀模式。

package main  import (      "fmt"      "github.com/yuin/gopher-lua"  )  // 設(shè)置表為只讀  func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData {      ud := l.NewUserData()      mt := l.NewTable()      // 設(shè)置表中域的指向?yàn)?nbsp;table      l.SetField(mt, "__index", table)      // 限制對表的更新操作      l.SetField(mt, "__newindex", l.NewFunction(func(state *lua.LState) int {          state.RaiseError("not allow to modify table")          return 0      }))      ud.Metatable = mt      return ud  }  func load(l *lua.LState) int {      mod := l.SetFuncs(l.NewTable(), exports)      l.SetField(mod, "name", lua.LString("gomodule"))      // 設(shè)置只讀      l.Push(SetReadOnly(l, mod))      return 1  }  var exports = map[string]lua.LGFunction{      "goFunc": goFunc,  }  func goFunc(l *lua.LState) int {      fmt.Println("golang")      return 0  }  func main() {      l := lua.NewState()      l.PreloadModule("gomodule", load)      // 嘗試修改導(dǎo)入的模塊      if err := l.DoString(`local m = require("gomodule");m.name = "hello world"`); err != nil {          fmt.Println(err)      }      l.Close()  }  // <string>:1: not allow to modify table

關(guān)于如何進(jìn)行g(shù)opher-lua虛擬機(jī)的原理分析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI