Golang 动态脚本调研

打印 上一主题 下一主题

主题 946|帖子 946|积分 2838

一、技术背景

1.1 程序的动态链接技术

在实际开发过程中,我们经常需要动态地更新程序的功能,或者在不变更程序主体文件的情况下添加或者更新程序模块。
1.1.1 动态链接库

首先最常见的是windows平台所支持的动态链接库(Dynamic Link Library),一般后缀名为.dll 。其优势非常明显:

  • 多个程序可以共享代码和数据。即多个程序加载同一个DLL文件。
  • 可以自然地将程序划分为若干个模块。每个模块输出为单独的DLL文件,由主程序加载执行。
  • 跨语言调用。由于DLL文件是语言无关的,一个DLL文件可以被多种编程语言加载执行。
  • 便于更新。在程序更新过程中,仅更新对应模块的DLL文件即可,无需重新部署整个程序。
  • 为热更新提供技术可能性。动态链接库可以通过编程手段实现加载和卸载,以此可以支持不重启程序的情况下更新模块。
  • 为程序提供编程接口。可以将自己程序的调用接口封装为DLL文件,供其他程序调用。
1.1.2 动态共享对象

在Linux平台,此项技术名为动态共享对象(dynamic shared objects),常见后缀名为.so 。
动态共享对象除了上述“动态链接库”的优势之外,也能解决由于Linux的开放性带来的底层接口兼容问题。即通过动态共享对象封装操作系统底层接口,对外提供统一的调用接口,以供上层应用程序调用。相当于提供了一层兼容层。
1.1.3 非编译语言的动态技术

非编译语言,由于本身是通过源代码发布,所以实现动态加载程序模块或者更新模块,直接修改源代码即可。思路简单且容易实现。
1.2 Golang 的动态技术

Golang作为编译型的开发语言,本身并不支持通过源代码实现动态加载和更新。但Golang官方提供了Plugin技术,实现动态加载。
通过在编译时添加参数,将Go程序编译为 Plugin:
  1. go build -buildmode=plugin
复制代码
但是此技术在当前版本(1.19)局限性非常大。通过其文档 https://pkg.go.dev/plugin 可知:

  • 平台限制,目前仅支持:Linux, FreeBSD 和 macOS
  • 卸载限制,仅支持动态加载,不支持动态卸载。
  • 不提供统一接口,只能通过反射处理Plugin内部的属性和函数。
并且上述问题,Golang官方并不打算解决……
二、Golang 的第三方解释器(Yaegi)

解释器一般只存在于脚本语言中,但是Traefik为了实现动态加载的插件功能,开发了一个Golang的解释器。提供了在运行时直接执行Golang源代码的能力。
参考项目:https://github.com/traefik/yaegi

2.1 使用场景

yaegi 项目官方推荐三种场景:

  • 内嵌解释器
  • 动态扩展框架
  • 命令行解释器
并且官方针对上述三种场景,均给出了相应的示例:
2.1.1 内嵌解释器
  1. package main
  2. import (
  3.     "github.com/traefik/yaegi/interp"
  4.     "github.com/traefik/yaegi/stdlib"
  5. )
  6. func main() {
  7.     i := interp.New(interp.Options{})
  8.     i.Use(stdlib.Symbols)
  9.     _, err := i.Eval(`import "fmt"`)
  10.     if err != nil {
  11.         panic(err)
  12.     }
  13.     _, err = i.Eval(`fmt.Println("Hello Yaegi")`)
  14.     if err != nil {
  15.         panic(err)
  16.     }
  17. }
复制代码
2.1.2 动态扩展框架
  1. package main
  2. import "github.com/traefik/yaegi/interp"
  3. const src = `package foo
  4. func Bar(s string) string { return s + "-Foo" }`
  5. func main() {
  6.     i := interp.New(interp.Options{})
  7.     _, err := i.Eval(src)
  8.     if err != nil {
  9.         panic(err)
  10.     }
  11.     v, err := i.Eval("foo.Bar")
  12.     if err != nil {
  13.         panic(err)
  14.     }
  15.     bar := v.Interface().(func(string) string)
  16.     r := bar("Kung")
  17.     println(r)
  18. }
复制代码
2.1.3 命令行解释器

Yaegi提供了一个命令行工具,实现了 读取-执行-显示 的循环。
  1. $ yaegi
  2. > 1 + 2
  3. 3
  4. > import "fmt"
  5. > fmt.Println("Hello World")
  6. Hello World
  7. >
复制代码
2.2 数据交互

数据交互方式比较多,需要注意的是从解释器内部返回的数据都是 reflect.Value 类型,获取其实际的值需要类型转换。
2.2.1 数据输入

可以有(但不限于)下述四种方法:

  • 通过 os.Args 传入数据
  • 通过 环境变量 传入数据
  • 通过 赋值语句 传入数据
  • 通过 函数调用 传入数据
下面是我自己写的代码示例:
  1. package main
  2. import (
  3.         "fmt"
  4.         "github.com/traefik/yaegi/interp"
  5.         "github.com/traefik/yaegi/stdlib"
  6. )
  7. func main() {
  8.         { // 通过 os.Args 传入数据
  9.                 i := interp.New(interp.Options{
  10.                         Args: []string{"666"},
  11.                 })
  12.                 i.Use(stdlib.Symbols)
  13.                 i.Eval(`import "fmt"`)
  14.                 i.Eval(`import "os"`)
  15.                 i.Eval(`fmt.Printf("os.Args[0] --- %s\n", os.Args[0])`)
  16.                // os.Args[0] --- 666
  17.         }
  18.         { // 通过 环境变量 传入数据
  19.                 i := interp.New(interp.Options{
  20.                         Env: []string{"inputEnv=666"},
  21.                 })
  22.                 i.Use(stdlib.Symbols)
  23.                 i.Eval(`import "fmt"`)
  24.                 i.Eval(`import "os"`)
  25.                 i.Eval(`fmt.Printf("os.Getenv("inputEnv") --- %s\n", os.Getenv("inputEnv"))`)
  26.                // os.Getenv("inputEnv") --- 666
  27.         }
  28.         { // 执行赋值语句传入数据
  29.                 i := interp.New(interp.Options{})
  30.                 i.Use(stdlib.Symbols)
  31.                 i.Eval(`import "fmt"`)
  32.                 i.Eval(fmt.Sprintf("inputVar:="%s"", "666"))
  33.                 i.Eval(`fmt.Printf("inputVar --- %s\n", inputVar)`)
  34.                // inputVar --- 666
  35.         }
  36.         { // 通过函数调用传递
  37.                 i := interp.New(interp.Options{})
  38.                 i.Use(stdlib.Symbols)
  39.                 i.Eval(`import "fmt"`)
  40.                 i.Eval(`var data map[string]interface{}`)
  41.                 i.Eval(`func SetData(d map[string]interface{}){ data = d }`)
  42.                 f, _ := i.Eval("SetData")
  43.                 fun := f.Interface().(func(map[string]interface{}))
  44.                 fun(map[string]interface{}{
  45.                         "data01": 666,
  46.                 })
  47.                 i.Eval(`fmt.Printf("SetData --- %d\n", data["data01"])`)
  48.                // SetData --- 666
  49.         }
  50. }
复制代码
2.1.2 数据输出

从解释器获取数据,实际上是获取全局变量的值,可以通过下述方法:

  • Eval 方法直接获取
  • 通过函数调用获取
  • Global 方法获取所有全局变量
  1. package main
  2. import (
  3.         "fmt"
  4.         "github.com/traefik/yaegi/interp"
  5.         "github.com/traefik/yaegi/stdlib"
  6. )
  7. func main() {
  8.         { // 通过 Eval 直接获取
  9.                 i := interp.New(interp.Options{})
  10.                 i.Use(stdlib.Symbols)
  11.                 i.Eval(`data := 666`)
  12.                 v, _ := i.Eval("data")
  13.                 value := v.Interface().(int)
  14.                 fmt.Printf("data = %d\n", value)
  15.                // data = 666
  16.         }
  17.         { // 通过函数返回值获取
  18.                 i := interp.New(interp.Options{})
  19.                 i.Use(stdlib.Symbols)
  20.                 i.Eval(`data := 666`)
  21.                 i.Eval(`func GetData() int {return data}`)
  22.                 f, _ := i.Eval("GetData")
  23.                 fun := f.Interface().(func() int)
  24.                 fmt.Printf("data = %d\n", fun())
  25.                // data = 666
  26.         }
  27.         { // 通过 Eval 直接获取
  28.                 i := interp.New(interp.Options{})
  29.                 i.Use(stdlib.Symbols)
  30.                 i.Eval(`dataInt := 666`)
  31.                 i.Eval(`dataStr := "666"`)
  32.                 for name, v := range i.Globals() {
  33.                         value := v.Interface()
  34.                         switch value.(type) {
  35.                         case int:
  36.                                 fmt.Printf("%s = %d\n", name, value)
  37.                                // dataInt = 666
  38.                         case string:
  39.                                 fmt.Printf("%s = %s\n", name, value)
  40.                                // dataStr = 666
  41.                         }
  42.                 }
  43.         }
  44. }
复制代码
三、实现原理

就解释器的实现原理,各个语言都大差不差。Golang由于其强大的基础库,直接提供了构建抽象语法树(Abstract Syntax Tree)的能力。基于抽象语法树实现脚本解释器,就容易很多。
3.1 AST - 抽象语法树

https://zh.m.wikipedia.org/zh-hans/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9
在计算机科学中,抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
Golang 通过 go/ast 包(https://pkg.go.dev/go/ast),提供抽象语法树相关能力。
3.1.1 抽象语法树示例

我们取Golang语法的子集进行示例:一个简单的条件表达式
  1. `A!=1 && (B>1 || (C<1 && A>2))`
复制代码
抽象语法树长这样:
  1.      0  *ast.BinaryExpr {
  2.      1  .  X: *ast.BinaryExpr {
  3.      2  .  .  X: *ast.Ident {
  4.      3  .  .  .  NamePos: -
  5.      4  .  .  .  Name: "A"
  6.      5  .  .  }
  7.      6  .  .  OpPos: -
  8.      7  .  .  Op: !=
  9.      8  .  .  Y: *ast.BasicLit {
  10.      9  .  .  .  ValuePos: -
  11.     10  .  .  .  Kind: INT
  12.     11  .  .  .  Value: "1"
  13.     12  .  .  }
  14.     13  .  }
  15.     14  .  OpPos: -
  16.     15  .  Op: &&
  17.     16  .  Y: *ast.ParenExpr {
  18.     17  .  .  Lparen: -
  19.     18  .  .  X: *ast.BinaryExpr {
  20.     19  .  .  .  X: *ast.BinaryExpr {
  21.     20  .  .  .  .  X: *ast.Ident {
  22.     21  .  .  .  .  .  NamePos: -
  23.     22  .  .  .  .  .  Name: "B"
  24.     23  .  .  .  .  }
  25.     24  .  .  .  .  OpPos: -
  26.     25  .  .  .  .  Op: >
  27.     26  .  .  .  .  Y: *ast.BasicLit {
  28.     27  .  .  .  .  .  ValuePos: -
  29.     28  .  .  .  .  .  Kind: INT
  30.     29  .  .  .  .  .  Value: "1"
  31.     30  .  .  .  .  }
  32.     31  .  .  .  }
  33.     32  .  .  .  OpPos: -
  34.     33  .  .  .  Op: ||
  35.     34  .  .  .  Y: *ast.ParenExpr {
  36.     35  .  .  .  .  Lparen: -
  37.     36  .  .  .  .  X: *ast.BinaryExpr {
  38.     37  .  .  .  .  .  X: *ast.BinaryExpr {
  39.     38  .  .  .  .  .  .  X: *ast.Ident {
  40.     39  .  .  .  .  .  .  .  NamePos: -
  41.     40  .  .  .  .  .  .  .  Name: "C"
  42.     41  .  .  .  .  .  .  }
  43.     42  .  .  .  .  .  .  OpPos: -
  44.     43  .  .  .  .  .  .  Op: <
  45.     44  .  .  .  .  .  .  Y: *ast.BasicLit {
  46.     45  .  .  .  .  .  .  .  ValuePos: -
  47.     46  .  .  .  .  .  .  .  Kind: INT
  48.     47  .  .  .  .  .  .  .  Value: "1"
  49.     48  .  .  .  .  .  .  }
  50.     49  .  .  .  .  .  }
  51.     50  .  .  .  .  .  OpPos: -
  52.     51  .  .  .  .  .  Op: &&
  53.     52  .  .  .  .  .  Y: *ast.BinaryExpr {
  54.     53  .  .  .  .  .  .  X: *ast.Ident {
  55.     54  .  .  .  .  .  .  .  NamePos: -
  56.     55  .  .  .  .  .  .  .  Name: "A"
  57.     56  .  .  .  .  .  .  }
  58.     57  .  .  .  .  .  .  OpPos: -
  59.     58  .  .  .  .  .  .  Op: >
  60.     59  .  .  .  .  .  .  Y: *ast.BasicLit {
  61.     60  .  .  .  .  .  .  .  ValuePos: -
  62.     61  .  .  .  .  .  .  .  Kind: INT
  63.     62  .  .  .  .  .  .  .  Value: "2"
  64.     63  .  .  .  .  .  .  }
  65.     64  .  .  .  .  .  }
  66.     65  .  .  .  .  }
  67.     66  .  .  .  .  Rparen: -
  68.     67  .  .  .  }
  69.     68  .  .  }
  70.     69  .  .  Rparen: -
  71.     70  .  }
  72.     71  }
复制代码
图形表示:

3.1.2 执行抽象语法树

简要说明一下如果要执行抽象语法树,应该怎么做:
执行过程与程序执行过程相似。先遍历声明列表,将已声明的内容初始化到堆内存(可以使用字典代替)。深度优先遍历抽象语法树,处理遍历过程中遇到的抽象对象,比如(举例而已,实际可能有出入):

  • 初始化堆内存和执行栈。
  • 遍历声明部分,写入堆,等待调用。
  • 找到主函数声明,主函数入栈,遍历其函数体语句,逐语句进行深度优先遍历执行。

    • 遇到变量定义,则写入栈顶缓存。
    • 遇到函数调用,则函数入栈。从堆中寻找函数定义,遍历其函数体语句,递归执行语句。
    • 遇到变量使用,依次从下述位置获取值:栈顶缓存 -> 堆内存
    • 遇到表达式,递归执行表达式。
    • 函数体执行结束后出栈,出栈后将返回值写入栈顶缓存。

  • 上述递归过程完成,程序结束。
上述是简单的执行过程,并未处理特殊语法和语法糖,各个语言的语法定义均有不同,需要单独处理。比如,Golang支持的语法可以参考:https://pkg.go.dev/go/ast
若能对其中定义的所有语法进行处理,就可以实现golang的脚本解释器。
对于上面(3.1.1)的那个简单示例,可以通过下述代码直接执行:
(不处理函数,只处理括号和有限的操作符。也未定义执行栈,堆内存使用全局变量Args代替)
[code]package mainimport (        "fmt"        "go/ast"        "go/parser"        "go/token"        "strconv")var Args map[string]intfunc main() {        {                Args = map[string]int{"A": 1, "B": 2, "C": 3}                code := `A==1 && (B>1 || C1 || (C2))`                expr, _ := parser.ParseExpr(code)                result := runExpr(expr)                fmt.Println(result)        }}// 执行表达式// 支持操作:>,  y.(int)                case "

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

汕尾海湾

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表