Во время игры с Github, наткнулся наgopher-luaЭто чистая виртуальная машина Lua, реализованная на Golang. Мы знаем, что Golang — статический язык, Lua — динамический язык, производительность Golang и эффективность исполнения языка очень хорошие, но по динамическим возможностям, конечно, нельзя сравнивать с Lua. Так что, если мы сможем объединить их, мы сможем объединить их сильные стороны (руководство смешно.
в проектеWiki, мы можем знать, что эффективность выполнения и производительность gopher-lua только хуже, чем привязки, реализованные в C. Так что с точки зрения производительности это должно быть очень хорошее решение для виртуальной машины.
Hello World
Вот простая программа Hello World. Сначала мы создали новую виртуальную машину, а затемDoString(...)
Объясните операцию выполнения кода lua и, наконец, выключите виртуальную машину. Запустите программу, и мы увидим строку «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(...)
После вызова цепного метода мы обнаружили, что каждое выполнениеDoString(...)
илиDoFile(...)
, и синтаксический анализ, и компиляция будут выполняться по одному разу.
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)
// ...
}
С этой точки зрения одна и та же копия кода Lua будет выполняться несколько раз (например,На http-сервере каждый запрос будет выполнять один и тот же код Lua.), если мы сможем скомпилировать код раньше времени, он сможет уменьшить накладные расходы на синтаксический анализ и компиляцию (если это код горячего пути). Согласно результатам Benchmark, упреждающая компиляция снижает ненужные накладные расходы.
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
пул экземпляров виртуальных машин
В сценарии, где выполняется тот же код LUA, в дополнение к использованию предпортирующей компиляции для оптимизации производительности, мы также можем представить пул экземпляра виртуальной машины.
Поскольку создание новой виртуальной машины Lua включает в себя множество операций по выделению памяти, если ее воссоздавать и уничтожать каждый раз при запуске, она будет потреблять много ресурсов. Внедрение пулов экземпляров виртуальных машин может повторно использовать виртуальные машины и сократить ненужные накладные расходы.
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
Результаты тестов показывают, что пулы экземпляров виртуальных машин действительно могут сократить количество операций по выделению памяти.
Реализация пула экземпляров, представленная в README, приведена ниже, но обратите внимание, что в исходном состоянииСоздано недостаточно экземпляров виртуальной машины(изначально количество экземпляров равно 0), и естьПроблема динамического расширения срезаЭто достойно улучшения.
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]
pl.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),
}
вызов модуля
gopher-lua поддерживает Lua для вызова модулей Go. Лично это очень интересная функция, потому что при разработке программ на Golang мы можем создавать множество часто используемых модулей. Этот механизм межъязыкового вызова позволяет нам повторно использовать код и инструменты.
Конечно, вдобавок к этому есть еще вызовы Go к модулям Lua, но лично я считаю, что последние не нужны, поэтому последние здесь не рассматриваются.
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
Переменное загрязнение
Когда мы используем пулы экземпляров для уменьшения накладных расходов, возникает еще одна сложная проблема: поскольку одна и та же виртуальная машина может выполняться несколько раз с одним и тем же кодом Lua, глобальные переменные в ней меняются. Если логика кода опирается на глобальные переменные, то могут возникнуть непредсказуемые результаты (это что-то вроде запаха "неповторяемого чтения" в изоляции базы данных).
глобальная переменная
Если нам нужно ограничить использование локальных переменных, может только код LUA, то стоюм на этой отправной точке, нам нужно сделать ограничения на глобальные переменные. Этот вопрос в том, как это добиться?
Мы знаем, что Lua компилируется в байт-код, а затем интерпретируется и выполняется. Затем мы можем ограничить использование глобальных переменных на этапе компиляции байт-кода. После проверки инструкций виртуальной машины Lua я обнаружил, что есть две инструкции, включающие глобальные переменные: GETGLOBAL (код операции 5) и SETGLOBAL (код операции 7).
На данный момент у нас есть общая идея: мы можем ограничить использование глобальных переменных кода, определив, содержит ли байт-код GETGLOBAL и SETGLOBAL. Что касается получения байт-кода, то его можно получить, вызвавCompileString(...)
иCompileFile(...)
, получите FunctionProto кода Lua, а свойство Code — это фрагмент байт-кода, а тип —[]uint32
.
в виртуальной машинекод реализацииВ том, что мы можем найти функцию инструмента, соответствующую OPCode на основе Bytecode.
// 获取对应指令的 OpCode
func opGetOpCode(inst uint32) int {
return int(inst >> 26)
}
С помощью этой служебной функции мы можем проверить глобальные переменные.
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")
}
}
// 对嵌套函数进行全局变量的检查
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()
}
модуль
В дополнение к возможности испорченных переменных, импортированные модули Go также могут быть изменены во время выполнения. Поэтому нам нужен механизм, гарантирующий, что модули, импортированные в виртуальную машину, не будут подделаны, т. е. импортированные объектытолько чтениеиз.
Глядя вверхСвязанный блогПосле этого мы можем__newindex
Модификация метода для установки модуля в режим только для чтения.
package main
import (
"fmt"
"github.com/yuin/gopher-lua"
)
// 设置表为只读
func SetReadOnly(l *lua.LState, table *lua.LTable) *lua.LUserData {
ud := l.NewUserData()
mt := l.NewTable()
// 设置表中域的指向为 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"))
// 设置只读
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)
// 尝试修改导入的模块
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
напиши в конце
Интеграция Golang и Lua расширила мой кругозор: оказывается, что статический язык и динамический язык могут быть интегрированы таким образом, работа статического языка эффективна, и разработка динамического языка эффективна, я рад думать об этом. это (побег.
После долгих поисков в Интернете я обнаружил, что нет никакого технического обмена информацией о Go-Lua, и нашел только немного связанную статью (Непрерывная оптимизация архитектуры страницы списка третьего уровня JD.com — лучшая практика Golang + Lua (OpenResty)), А в этой статье Lua или C на ходу. Из-за отсутствия информации и неадекватных причин я (Студенческая партия) опыт разработки, не является хорошей оценкой осуществимости схемы в реальном производстве. Таким образом, эту статью можно рассматривать только как "свободный текст", ха-ха.