Golang 动态脚本调研详解

目录
  • 一、技术背景
    • 1.1 程序的动态链接技术
      • 1.1.1 动态链接库
      • 1.1.2 动态共享对象
      • 1.1.3 非编译语言的动态技术
    • 1.2 Golang 的动态技术
  • 二、Golang 的第三方解释器(Yaegi)
    • 2.1 使用场景
      • 2.1.1 内嵌解释器
      • 2.1.2 动态扩展框架
      • 2.1.3 命令行解释器
    • 2.2 数据交互
      • 2.2.1 数据输入
      • 2.1.2 数据输出
  • 三、实现原理
    • 3.1 AST - 抽象语法树
      • 3.1.1 抽象语法树示例
      • 3.1.2 执行抽象语法树

一、技术背景

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:

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 内嵌解释器

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 数据输入

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

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

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

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 数据输出

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

  • Eval 方法直接获取
  • 通过函数调用获取
  • 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 - 抽象语法树

在计算机科学中,抽象语法树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))`

抽象语法树长这样:

   *ast.BinaryExpr {
   .  X: *ast.BinaryExpr {
   .  .  X: *ast.Ident {
   .  .  .  NamePos: -
   .  .  .  Name: "A"
   .  .  }
   .  .  OpPos: -
   .  .  Op: !=
   .  .  Y: *ast.BasicLit {
   .  .  .  ValuePos: -
   .  .  .  Kind: INT
   .  .  .  Value: "1"
   .  .  }
   .  }
   .  OpPos: -
   .  Op: &&
   .  Y: *ast.ParenExpr {
   .  .  Lparen: -
   .  .  X: *ast.BinaryExpr {
   .  .  .  X: *ast.BinaryExpr {
   .  .  .  .  X: *ast.Ident {
   .  .  .  .  .  NamePos: -
   .  .  .  .  .  Name: "B"
   .  .  .  .  }
   .  .  .  .  OpPos: -
   .  .  .  .  Op: >
   .  .  .  .  Y: *ast.BasicLit {
   .  .  .  .  .  ValuePos: -
   .  .  .  .  .  Kind: INT
   .  .  .  .  .  Value: "1"
   .  .  .  .  }
   .  .  .  }
   .  .  .  OpPos: -
   .  .  .  Op: ||
   .  .  .  Y: *ast.ParenExpr {
   .  .  .  .  Lparen: -
   .  .  .  .  X: *ast.BinaryExpr {
   .  .  .  .  .  X: *ast.BinaryExpr {
   .  .  .  .  .  .  X: *ast.Ident {
   .  .  .  .  .  .  .  NamePos: -
   .  .  .  .  .  .  .  Name: "C"
   .  .  .  .  .  .  }
   .  .  .  .  .  .  OpPos: -
   .  .  .  .  .  .  Op: <
   .  .  .  .  .  .  Y: *ast.BasicLit {
   .  .  .  .  .  .  .  ValuePos: -
   .  .  .  .  .  .  .  Kind: INT
   .  .  .  .  .  .  .  Value: "1"
   .  .  .  .  .  .  }
   .  .  .  .  .  }
   .  .  .  .  .  OpPos: -
   .  .  .  .  .  Op: &&
   .  .  .  .  .  Y: *ast.BinaryExpr {
   .  .  .  .  .  .  X: *ast.Ident {
   .  .  .  .  .  .  .  NamePos: -
   .  .  .  .  .  .  .  Name: "A"
   .  .  .  .  .  .  }
   .  .  .  .  .  .  OpPos: -
   .  .  .  .  .  .  Op: >
   .  .  .  .  .  .  Y: *ast.BasicLit {
   .  .  .  .  .  .  .  ValuePos: -
   .  .  .  .  .  .  .  Kind: INT
   .  .  .  .  .  .  .  Value: "2"
   .  .  .  .  .  .  }
   .  .  .  .  .  }
   .  .  .  .  }
   .  .  .  .  Rparen: -
   .  .  .  }
   .  .  }
   .  .  Rparen: -
   .  }
   }

图形表示:

3.1.2 执行抽象语法树

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

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

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

    遇到变量定义,则写入栈顶缓存。

    遇到函数调用,则函数入栈。从堆中寻找函数定义,遍历其函数体语句,递归执行语句。

    遇到变量使用,依次从下述位置获取值:栈顶缓存 -> 堆内存

    遇到表达式,递归执行表达式。

    函数体执行结束后出栈,出栈后将返回值写入栈顶缓存。

  • 上述递归过程完成,程序结束。

上述是简单的执行过程,并未处理特殊语法和语法糖,各个语言的语法定义均有不同,需要单独处理。比如,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 动态脚本调研详解的详细内容,更多关于Golang 动态脚本的资料请关注我们其它相关文章!

(0)

相关推荐

  • go web 预防跨站脚本的实现方式

    一 点睛 现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多.所谓动态内容,就是根据用户环境和需要,Web 应用程序能够输出相应的内容.动态站点会受到一种名为"跨站脚本攻击"(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响. 攻击者通常会在有漏洞的程序中插入 JavaScript.VBScript. ActiveX 或 Flash 以欺骗用户.一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取或污染 coo

  • Golang使用lua脚本实现redis原子操作

    目录 [redis 调用Lua脚本](#redis 调用Lua脚本) [redis+lua 实现评分排行榜实时更新](#redis+lua 实现评分排行榜实时更新) [lua 脚本](#lua 脚本) Golang调用redis+lua示例 byte切片与string的转换优化 redis 调用Lua脚本 EVAL命令 redis调用Lua脚本需要使用EVAL命令. redis EVAL命令格式: redis 127.0.0.1:6379> EVAL script numkeys key [ke

  • Golang使用CGO与Plugin技术运行加载C动态库

    目录 文章简介 技术背景 解决方案1 解决方案2 文章简介 本文介绍一种 Golang 程序在运行时加载 C 动态库的技术,跳过了 Golang 项目编译阶段需要链接 C 动态库的过程,提高了 Golang 项目开发部署的灵活性. 技术背景 Golang 程序调用 OpenCC 动态库的函数,执行文本繁体转简体操作. 需要在编译时不链接动态库,只在程序运行时加载 OpenCC 动态库. OpenCC 库是使用 C++ 编写的繁简体转换程序,提供 C 语言 API 接口. 开源项目地址 CGO 技

  • Golang动态调用方法小结

    main.go package main import ( "lenu/call" "reflect" ) type FuncCollection map[string]reflect.Value func main() { _, _ = CallFunc("Hello", "执行Hello方法") _, _ = CallFunc("World", "执行World方法") } func

  • go实现脚本解释器gscript

    目录 前言 效果 实现 规划 前言 最近又在重新学习编译原理了,其实两年前也复习过,当初是为了能实现通过 MySQL 的 DDL 生成 Python 中 sqlalchemy 的 model. 相关文章在这里:手写一个词法分析器 虽然完成了相关功能,但现在看来其实实现的比较糙的,而且也只运用到了词法分析:所以这次我的目的是可以通过词法分析->语法分析->语义分析 最终能实现一个功能完善的脚本”语言”. 效果 现在也有了一些阶段性的成果,如下图所示: 目前具备以下基本功能: 变量声明与赋值(只支

  • golang 调用c语言动态库方式实现

    下面我们自己在 Linux 下做一个动态库(.so 文件 - Shared Object),然在用 Go 来使用它.本文所用的操作系统为 Ubuntu18.04, 以 gcc 作为编译器. 1.实现头文件,声明文件中函数.这里创建一个add.h文件. #ifndef __ADD_H__ #define __ADD_H__ char* Add(char* src, int n); #endif 2.实现add主体函数add.c #include <string.h> #include <s

  • Golang 动态脚本调研详解

    目录 一.技术背景 1.1 程序的动态链接技术 1.1.1 动态链接库 1.1.2 动态共享对象 1.1.3 非编译语言的动态技术 1.2 Golang 的动态技术 二.Golang 的第三方解释器(Yaegi) 2.1 使用场景 2.1.1 内嵌解释器 2.1.2 动态扩展框架 2.1.3 命令行解释器 2.2 数据交互 2.2.1 数据输入 2.1.2 数据输出 三.实现原理 3.1 AST - 抽象语法树 3.1.1 抽象语法树示例 3.1.2 执行抽象语法树 一.技术背景 1.1 程序的

  • Golang接口使用教程详解

    目录 前言 一.概述 二.接口类型 2.1 接口的定义 2.2 实现接口的条件 2.3 为什么需要接口 2.4 接口类型变量 三.值接收者和指针接收者 3.1 值接收者实现接口 3.2 指针接收者实现接口 四.类型与接口的关系 4.1 一个类型实现多个接口 4.2 多种类型实现同一接口 五.接口嵌套 六.空接口 七.类型断言 总结 前言 go语言并没有面向对象的相关概念,go语言提到的接口和java.c++等语言提到的接口不同,它不会显示的说明实现了接口,没有继承.子类.implements关键

  • Linux动态库函数的详解

    Linux动态库函数的详解 加载动态库 void *dlopen(const char *filename, int flag); flag的可能值: RTLD_LAZY RTLD_NOW RTLD_GLOBAL RTLD_LOCAL RTLD_NODELETE (since glibc 2.2) RTLD_NOLOAD (since glibc 2.2) RTLD_DEEPBIND 这些flag的具体含义可使用man查看 返回动态库中最近的一次错误 char *dlerror(void); 根

  • Linux静态库与动态库实例详解

    Linux静态库与动态库实例详解 1. Linux 下静态链接库编译与使用 首先编写如下代码: // main.c #include "test.h" int main(){ test(); return 0; } // test.h #include<iostream> using namespace std; void test(); // test.c #include "test.h" void test(){ cout<< &quo

  • golang之log rotate详解

    操作系统: CentOS 6.9_x64 go语言版本: 1.8.3 问题描述 golang的log模块提供的有写日志功能,示例代码如下: /* golang log example */ package main import ( "log" "os" ) func main() { logFile,err := os.Create("test1.log") defer logFile.Close() if err != nil { log.F

  • PHP面向对象之事务脚本模式(详解)

    如下所示: /* 事务脚本模式: 类似于thinkphp中的model层,或者说就是操作数据库的类. 个人觉得实践中使用起来还是挺简单方便的,就是SQL语句写死了的话,灵活性就不够. 示例代码如下: */ namespace woo\process; abstract class Base{ static $DB; //pdo对象 static $stmts = array(); //sql语句句柄 function __construct (){ $dsn = \woo\base\Applic

  • Golang与python线程详解及简单实例

    Golang与python线程详解及简单实例 在GO中,开启15个线程,每个线程把全局变量遍历增加100000次,因此预测结果是 15*100000=1500000. var sum int var cccc int var m *sync.Mutex func Count1(i int, ch chan int) { for j := 0; j < 100000; j++ { cccc = cccc + 1 } ch <- cccc } func main() { m = new(sync.

  • MyBatis 执行动态 SQL语句详解

    大家基本上都知道如何使用 MyBatis 执行任意 SQL,使用方法很简单,例如在一个 XXMapper.xml 中: <select id="executeSql" resultType="map"> ${_parameter} </select> 你可以如下调用: sqlSession.selectList("executeSql", "select * from sysuser where enabled

  • C#动态对象(dynamic)详解(实现方法和属性的动态)

    C#的动态对象的属性实现比较简单,如果要实现动态语言那种动态方法就比较困难,因为对于dynamic对象,扩展方法,匿名方法都是不能用直接的,这里还是利用对象和委托来模拟这种动态方法的实现,看起来有点javascript的对象味道: 1) 定义一个委托,参数个数可变,参数都是object类型:这里的委托多有个dynamic参数,代表调用这个委托的动态对象本身. public delegate object MyDelegate(dynamic Sender, params object[] PMs

  • C语言 动态内存分配详解

    C语言 动态内存分配详解 动态内存分配涉及到堆栈的概念:堆栈是两种数据结构.堆栈都是数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除. 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表. \在C语言中,全局变量分配在内存中的静态存储区,非静态的局部变量(包括形参)是分配在内存的动态存储区,该存储区被

随机推荐