一、技术背景

1.1 程序的动态链接技术

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

1.1.1 动态链接库

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

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

1.1.2 动态共享对象

在Linux平台,此项技术名为动态共享对象(dynamic shared objects),常见后缀名为.so

动态共享对象除了上述“动态链接库”的优势之外,也能解决由于Linux的开放性带来的底层接口兼容问题。即通过动态共享对象封装操作系统底层接口,对外提供统一的调用接口,以供上层应用程序调用。相当于提供了一层兼容层。

1.1.3 非编译语言的动态技术

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

1.2 Golang 的动态技术

Golang作为编译型的开发语言,本身并不支持通过源代码实现动态加载和更新。但Golang官方提供了Plugin技术,实现动态加载。

通过在编译时添加参数,将Go程序编译为 Plugin:

go build -buildmode=plugin

但是此技术在当前版本(1.19)局限性非常大。通过其文档 https://pkg.go.dev/plugin 可知:

  1. 平台限制,目前仅支持:Linux, FreeBSD 和 macOS
  2. 卸载限制,仅支持动态加载,不支持动态卸载。
  3. 不提供统一接口,只能通过反射处理Plugin内部的属性和函数。

并且上述问题,Golang官方并不打算解决……

二、Golang 的第三方解释器(Yaegi)

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

参考项目:https://github.com/traefik/yaegi

2.1 使用场景

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

  1. 内嵌解释器
  2. 动态扩展框架
  3. 命令行解释器

并且官方针对上述三种场景,均给出了相应的示例:

2.1.1 内嵌解释器

package main

import (
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
) func main() {
i := interp.New(interp.Options{}) i.Use(stdlib.Symbols) _, err := i.Eval(`import "fmt"`)
if err != nil {
panic(err)
} _, err = i.Eval(`fmt.Println("Hello Yaegi")`)
if err != nil {
panic(err)
}
}

2.1.2 动态扩展框架

package main

import "github.com/traefik/yaegi/interp"

const src = `package foo
func Bar(s string) string { return s + "-Foo" }` func main() {
i := interp.New(interp.Options{}) _, err := i.Eval(src)
if err != nil {
panic(err)
} v, err := i.Eval("foo.Bar")
if err != nil {
panic(err)
} bar := v.Interface().(func(string) string) r := bar("Kung")
println(r)
}

2.1.3 命令行解释器

Yaegi提供了一个命令行工具,实现了 读取-执行-显示 的循环。

$ yaegi
> 1 + 2
3
> import "fmt"
> fmt.Println("Hello World")
Hello World
>

2.2 数据交互

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

2.2.1 数据输入

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

  1. 通过 os.Args 传入数据
  2. 通过 环境变量 传入数据
  3. 通过 赋值语句 传入数据
  4. 通过 函数调用 传入数据

下面是我自己写的代码示例:

package main

import (
"fmt" "github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
)
func main() {
{ // 通过 os.Args 传入数据
i := interp.New(interp.Options{
Args: []string{"666"},
})
i.Use(stdlib.Symbols)
i.Eval(`import "fmt"`)
i.Eval(`import "os"`)
i.Eval(`fmt.Printf("os.Args[0] --- %s\n", os.Args[0])`)
// os.Args[0] --- 666
}
{ // 通过 环境变量 传入数据
i := interp.New(interp.Options{
Env: []string{"inputEnv=666"},
})
i.Use(stdlib.Symbols)
i.Eval(`import "fmt"`)
i.Eval(`import "os"`)
i.Eval(`fmt.Printf("os.Getenv(\"inputEnv\") --- %s\n", os.Getenv("inputEnv"))`)
// os.Getenv("inputEnv") --- 666
}
{ // 执行赋值语句传入数据
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
i.Eval(`import "fmt"`)
i.Eval(fmt.Sprintf("inputVar:=\"%s\"", "666"))
i.Eval(`fmt.Printf("inputVar --- %s\n", inputVar)`)
// inputVar --- 666
}
{ // 通过函数调用传递
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
i.Eval(`import "fmt"`)
i.Eval(`var data map[string]interface{}`)
i.Eval(`func SetData(d map[string]interface{}){ data = d }`)
f, _ := i.Eval("SetData")
fun := f.Interface().(func(map[string]interface{}))
fun(map[string]interface{}{
"data01": 666,
})
i.Eval(`fmt.Printf("SetData --- %d\n", data["data01"])`)
// SetData --- 666
}
}

2.1.2 数据输出

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

  1. Eval 方法直接获取
  2. 通过函数调用获取
  3. Global 方法获取所有全局变量
package main

import (
"fmt" "github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
) func main() {
{ // 通过 Eval 直接获取
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
i.Eval(`data := 666`)
v, _ := i.Eval("data")
value := v.Interface().(int)
fmt.Printf("data = %d\n", value)
// data = 666
}
{ // 通过函数返回值获取
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
i.Eval(`data := 666`)
i.Eval(`func GetData() int {return data}`)
f, _ := i.Eval("GetData")
fun := f.Interface().(func() int)
fmt.Printf("data = %d\n", fun())
// data = 666
}
{ // 通过 Eval 直接获取
i := interp.New(interp.Options{})
i.Use(stdlib.Symbols)
i.Eval(`dataInt := 666`)
i.Eval(`dataStr := "666"`)
for name, v := range i.Globals() {
value := v.Interface()
switch value.(type) {
case int:
fmt.Printf("%s = %d\n", name, value)
// dataInt = 666
case string:
fmt.Printf("%s = %s\n", name, value)
// dataStr = 666
}
} }
}

三、实现原理

就解释器的实现原理,各个语言都大差不差。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语法的子集进行示例:一个简单的条件表达式

`A!=1 && (B>1 || (C<1 && A>2))`

抽象语法树长这样:

     0  *ast.BinaryExpr {
1 . X: *ast.BinaryExpr {
2 . . X: *ast.Ident {
3 . . . NamePos: -
4 . . . Name: "A"
5 . . }
6 . . OpPos: -
7 . . Op: !=
8 . . Y: *ast.BasicLit {
9 . . . ValuePos: -
10 . . . Kind: INT
11 . . . Value: "1"
12 . . }
13 . }
14 . OpPos: -
15 . Op: &&
16 . Y: *ast.ParenExpr {
17 . . Lparen: -
18 . . X: *ast.BinaryExpr {
19 . . . X: *ast.BinaryExpr {
20 . . . . X: *ast.Ident {
21 . . . . . NamePos: -
22 . . . . . Name: "B"
23 . . . . }
24 . . . . OpPos: -
25 . . . . Op: >
26 . . . . Y: *ast.BasicLit {
27 . . . . . ValuePos: -
28 . . . . . Kind: INT
29 . . . . . Value: "1"
30 . . . . }
31 . . . }
32 . . . OpPos: -
33 . . . Op: ||
34 . . . Y: *ast.ParenExpr {
35 . . . . Lparen: -
36 . . . . X: *ast.BinaryExpr {
37 . . . . . X: *ast.BinaryExpr {
38 . . . . . . X: *ast.Ident {
39 . . . . . . . NamePos: -
40 . . . . . . . Name: "C"
41 . . . . . . }
42 . . . . . . OpPos: -
43 . . . . . . Op: <
44 . . . . . . Y: *ast.BasicLit {
45 . . . . . . . ValuePos: -
46 . . . . . . . Kind: INT
47 . . . . . . . Value: "1"
48 . . . . . . }
49 . . . . . }
50 . . . . . OpPos: -
51 . . . . . Op: &&
52 . . . . . Y: *ast.BinaryExpr {
53 . . . . . . X: *ast.Ident {
54 . . . . . . . NamePos: -
55 . . . . . . . Name: "A"
56 . . . . . . }
57 . . . . . . OpPos: -
58 . . . . . . Op: >
59 . . . . . . Y: *ast.BasicLit {
60 . . . . . . . ValuePos: -
61 . . . . . . . Kind: INT
62 . . . . . . . Value: "2"
63 . . . . . . }
64 . . . . . }
65 . . . . }
66 . . . . Rparen: -
67 . . . }
68 . . }
69 . . Rparen: -
70 . }
71 }

图形表示:

3.1.2 执行抽象语法树

简要说明一下如果要执行抽象语法树,应该怎么做:

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

  1. 初始化堆内存和执行栈。
  2. 遍历声明部分,写入堆,等待调用。
  3. 找到主函数声明,主函数入栈,遍历其函数体语句,逐语句进行深度优先遍历执行。
    1. 遇到变量定义,则写入栈顶缓存。
    2. 遇到函数调用,则函数入栈。从堆中寻找函数定义,遍历其函数体语句,递归执行语句。
    3. 遇到变量使用,依次从下述位置获取值:栈顶缓存 -> 堆内存
    4. 遇到表达式,递归执行表达式。
    5. 函数体执行结束后出栈,出栈后将返回值写入栈顶缓存。
  4. 上述递归过程完成,程序结束。

上述是简单的执行过程,并未处理特殊语法和语法糖,各个语言的语法定义均有不同,需要单独处理。比如,Golang支持的语法可以参考:https://pkg.go.dev/go/ast

若能对其中定义的所有语法进行处理,就可以实现golang的脚本解释器。

对于上面(3.1.1)的那个简单示例,可以通过下述代码直接执行:

(不处理函数,只处理括号和有限的操作符。也未定义执行栈,堆内存使用全局变量Args代替)

package main

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"strconv"
) var Args map[string]int func main() {
{
Args = map[string]int{"A": 1, "B": 2, "C": 3}
code := `A==1 && (B>1 || C<1)`
expr, _ := parser.ParseExpr(code)
result := runExpr(expr)
fmt.Println(result)
}
{
Args["A"] = 3
Args = map[string]int{"A": 1, "B": 2, "C": 3}
code := `A!=1 && (B>1 || (C<1 && A>2))`
expr, _ := parser.ParseExpr(code)
result := runExpr(expr)
fmt.Println(result)
}
} // 执行表达式
// 支持操作:>, <, ==, !=, &&, ||
// 支持括号嵌套
func runExpr(expr ast.Expr) interface{} {
var result interface{}
// 二元表达式
if binaryExpr, ok := expr.(*ast.BinaryExpr); ok {
switch binaryExpr.Op.String() {
case "&&":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(bool) && y.(bool)
case "||":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(bool) || y.(bool)
case ">":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(int) > y.(int)
case "<":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(int) < y.(int)
case "==":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(int) == y.(int)
case "!=":
x := runExpr(binaryExpr.X)
y := runExpr(binaryExpr.Y)
return x.(int) != y.(int)
}
}
// 基本类型值
if basicLit, ok := expr.(*ast.BasicLit); ok {
switch basicLit.Kind {
case token.INT:
v, _ := strconv.Atoi(basicLit.Value)
return v
}
}
// 标识符
if ident, ok := expr.(*ast.Ident); ok {
return Args[ident.Name]
}
// 括号表达式
if parenExpr, ok := expr.(*ast.ParenExpr); ok {
return runExpr(parenExpr.X)
} return result
}

执行结果:

A==1 && (B>1 || C<1) => true
A!=1 && (B>1 || (C<1 && A>2)) => false

Golang 动态脚本调研的更多相关文章

  1. 【开源】.Net 动态脚本引擎NScript

    开源地址: https://git.oschina.net/chejiangyi/NScript 开源QQ群: .net 开源基础服务  238543768 .Net 动态脚本引擎 NScript   ...

  2. JavaScript 动态脚本

    动态脚本,指的是在页面加载时不存在,但将来的某一个时刻通过修改DOM动态添加的脚本. <script type="text/javascript"> function ...

  3. DOM动态脚本和动态样式

    动态脚本 [定义] 在页面加载时不存在,但将来的某一时刻通过修改DOM动态添加的脚本. [方式] [1]插入外部文件方式 var script = document.createElement(&qu ...

  4. js插入动态脚本

    原文章:https://www.w3cmm.com/dom/insert-javascript.html 动态脚本指的是在页面加载时不存在,但将来的某一时刻通过修改该DOM动态添加的脚本.和操作HTM ...

  5. LR动态脚本的处理

    在处理SSO修改密码脚本时遇到一个问题,根据用户名的不同,提交请求中数据会不一样.处理此问题,如果经分析用同类型的账号(每个账号含有的子账号类型和数目一致)测试与实际不同类型账号性能没有大的差别,则用 ...

  6. 【前端基础】动态脚本与JSONP

    博主入职两个月了,越来越感受到打好基础对于前端工程师的重要性,在向着狂拽酷炫的框架&构建工具高速狂奔之前,必须有一个坚实的基础打底,才不至于轻易翻车.所以博主最近一直在恶补<JS高级程序 ...

  7. golang动态加载原生代码思路

    golang动态加载原生代码思路(非plugin,非so文件.使用mmap形式运行机器码,可释放) 1.用go tool objdump,可以看到任意函数的机器码.汇编指令.偏移.(go源码下面有一个 ...

  8. 使用Roslyn脚本化C#代码,C#动态脚本实现方案

    [前言] Roslyn 是微软公司开源的 .NET 编译器. 编译器支持 C# 和 Visual Basic 代码编译,并提供丰富的代码分析 API. Roslyn不仅仅可以直接编译输出,难能可贵的就 ...

  9. doubleclick cookie、动态脚本、用户画像、用户行为分析和海量数据存取 推荐词 京东 电商 信息上传 黑洞 https://blackhole.m.jd.com/getinfo

    doubleclick cookie https://mp.weixin.qq.com/s/vZUj-Z9FGSSWXOodGqbYkA 揭密Google的网络广告技术:基于互联网大数据视角 原创:  ...

随机推荐

  1. 3D还原货拉拉女孩身亡真相,这一环值得反思!

    货拉拉女孩跳车身亡的消息,让人惋惜又震惊.司机多次偏离原始路线,女孩最终选择跳车,结果不幸身亡. 货拉拉女孩跳车真相被3D还原 有人质疑平台监管不力,造成如此惨剧,有人吐槽企业压榨员工,司机绕路是不得 ...

  2. linux系统健康检查脚本

    #!/bin/bash echo "You are logged in as `whoami`"; if [ `whoami` != root ]; then echo " ...

  3. rhel6下eth1恢复eth0

    问题:VMware 虚拟机中,从模板克隆出来的虚拟机的网卡名都会变成为eth1,而程序或者脚本,默认网卡是eth0,这时需要将eth1改为eth0. 原因:/etc/udev/rules.d/70-p ...

  4. Nacos配置失败(java.lang.IllegalStateException: failed to req API:/nacos/v1/ns/instance after all server)

    解决: nacos服务器过载,可以删掉nacos文件夹下的data文件夹,重新启动.

  5. NC207028 第k小数

    NC207028 第k小数 题目 题目描述 给你一个长度为 \(n\) 的序列,求序列中第 \(k\) 小数的多少. 输入描述 多组输入,第一行读入一个整数 \(T\) 表示有 \(T\) 组数据. ...

  6. The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32

    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32

  7. C++几种特殊形式的函数

    本篇文章主要介绍C++语言中5中特殊形式的函数,分别是带默认形参值的函数.重载函数.内联函数.带形参和返回值的主函数以及递归函数. 一.带默认形参值的函数 如果函数的形参在多数情况下都可以取某个固定的 ...

  8. k8s的部署

    一.k8s的二进制部署 1.环境准备: IP                               节点 172.16.10.1         k8s-master01 172.16.10.3 ...

  9. 第十三天python3 生成器yield

    生成器generator 生成器指的是生成器对象,可由生成器表达式得到,也可以使用yield关键字得到一个生成器函数,调用这个函数得到一个生成器对象: 生成器函数 函数体中包含yield语句的函数,返 ...

  10. C++ 内存模型 write_x_read_y 试例构造

    之前一段时间偶然在 B 站上刷到了南京大学蒋炎岩(jyy)老师在直播操作系统网课.点进直播间看了一下发现这个老师实力非凡,上课从不照本宣科,而且旁征博引又不吝于亲自动手演示,于是点了关注.后来开始看其 ...