扩展阅读:starlark让你的程序更强大
starlark是一门配置语言,它是从Python语言中衍生出来的,但是比Python更简单、更安全。它最初是由Google开发的,用于Bazel构建系统。starlark保留了Python的基本语法和数据类型,但移除了一些危险的特性,比如循环引用、无限递归等。这使得starlark非常适合作为配置语言或者脚本语言嵌入到其他程序中。
starlark的主要特点包括:
- 简单易学 - 采用Python风格的语法,对于熟悉Python的开发者来说几乎没有学习成本
- 确定性 - 相同的输入总是产生相同的输出,没有随机性和副作用
- 沙箱隔离 - 不能访问文件系统、网络等外部资源,保证安全性
- 可扩展 - 可以方便地将宿主语言(如Go)的函数暴露给starlark使用
- 快速执行 - 解释器性能优秀,适合嵌入式使用
这些特性使得starlark成为一个理想的嵌入式配置/脚本语言。通过将starlark集成到我们的Go程序中,我们可以让用户使用starlark脚本来扩展和自定义程序的功能,同时又能保证安全性和可控性。
比如在go-delve/delve调试器中,starlark被用来编写自动化调试脚本。用户可以使用starlark脚本来自动执行一系列调试命令,或者根据特定条件触发某些调试操作。这大大增强了调试器的灵活性和可编程性。
下面我们将通过一个简单的例子来演示如何在Go程序中集成starlark引擎,并实现Go函数与starlark函数的相互调用。
集成starlark引擎到Go程序
首先我们来看一个简单的例子,演示如何将starlark引擎集成到Go程序中。这个例子实现了一个简单的REPL(Read-Eval-Print Loop)环境,允许用户输入starlark代码并立即执行:
package main
import (
...
"go.starlark.net/starlark"
"go.starlark.net/syntax"
)
func main() {
// Create a new Starlark thread
thread := &starlark.Thread{
Name: "repl",
Print: func(thread *starlark.Thread, msg string) {
fmt.Println(msg)
},
}
// Create a new global environment
globals := starlark.StringDict{}
// Create a scanner for reading input
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Starlark REPL (type 'exit' to quit)")
errExit := errors.New("exit")
for {
// Print prompt
fmt.Print(">>> ")
// Read input
readline := func() ([]byte, error) {
if !scanner.Scan() {
return nil, io.EOF
}
line := strings.TrimSpace(scanner.Text())
if line == "exit" {
return nil, errExit
}
if line == "" {
return nil, nil
}
return []byte(line + "\n"), nil
}
// Execute the input
if err := rep(readline, thread, globals); err != nil {
if err == io.EOF {
break
}
if err == errExit {
os.Exit(0)
}
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
}
}
// rep reads, evaluates, and prints one item.
//
// It returns an error (possibly readline.ErrInterrupt)
// only if readline failed. Starlark errors are printed.
func rep(readline func() ([]byte, error), thread *starlark.Thread, globals starlark.StringDict) error {
eof := false
f, err := syntax.ParseCompoundStmt("<stdin>", readline)
if err != nil {
if eof {
return io.EOF
}
printError(err)
return nil
}
if expr := soleExpr(f); expr != nil {
//TODO: check for 'exit'
// eval
v, err := evalExprOptions(nil, thread, expr, globals)
if err != nil {
printError(err)
return nil
}
// print
if v != starlark.None {
fmt.Println(v)
}
} else {
// compile
prog, err := starlark.FileProgram(f, globals.Has)
if err != nil {
printError(err)
return nil
}
// execute (but do not freeze)
res, err := prog.Init(thread, globals)
if err != nil {
printError(err)
}
// The global names from the previous call become
// the predeclared names of this call.
// If execution failed, some globals may be undefined.
for k, v := range res {
globals[k] = v
}
}
return nil
}
var defaultSyntaxFileOpts = &syntax.FileOptions{
Set: true,
While: true,
TopLevelControl: true,
GlobalReassign: true,
Recursion: true,
}
// evalExprOptions is a wrapper around starlark.EvalExprOptions.
// If no options are provided, it uses default options.
func evalExprOptions(opts *syntax.FileOptions, thread *starlark.Thread, expr syntax.Expr, globals starlark.StringDict) (starlark.Value, error) {
if opts == nil {
opts = defaultSyntaxFileOpts
}
return starlark.EvalExprOptions(opts, thread, expr, globals)
}
func soleExpr(f *syntax.File) syntax.Expr {
if len(f.Stmts) == 1 {
if stmt, ok := f.Stmts[0].(*syntax.ExprStmt); ok {
return stmt.X
}
}
return nil
}
// printError prints the error to stderr,
// or its backtrace if it is a Starlark evaluation error.
func printError(err error) {
if evalErr, ok := err.(*starlark.EvalError); ok {
fmt.Fprintln(os.Stderr, evalErr.Backtrace())
} else {
fmt.Fprintln(os.Stderr, err)
}
}
starlark直接调用Go函数
在这个例子中,我们将演示如何让starlark脚本调用Go函数。主要思路是:
- 定义一个Go函数映射表(GoFuncMap)来注册可供starlark调用的Go函数
- 实现一个胶水函数(callGoFunc)作为starlark和Go函数之间的桥梁
- 将胶水函数注册到starlark全局环境中,这样starlark代码就可以通过它来调用Go函数
下面是一个简单的示例,展示如何让starlark调用一个Go的加法函数:
package main
import (
...
"go.starlark.net/starlark"
"go.starlark.net/syntax"
)
// GoFuncMap stores registered Go functions
var GoFuncMap = map[string]interface{}{
"Add": Add,
}
func Add(a, b int) int {
fmt.Println("Hey! I'm a Go function!")
return a + b
}
// callGoFunc is a Starlark function that calls registered Go functions
func callGoFunc(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if len(args) < 1 {
return nil, fmt.Errorf("call_gofunc requires at least one argument (function name)")
}
funcName, ok := args[0].(starlark.String)
if !ok {
return nil, fmt.Errorf("first argument must be a string (function name)")
}
goFunc, ok := GoFuncMap[string(funcName)]
if !ok {
return nil, fmt.Errorf("function %s not found", funcName)
}
// Convert Starlark arguments to Go values
goArgs := make([]interface{}, len(args)-1)
for i, arg := range args[1:] {
switch v := arg.(type) {
case starlark.Int:
if v, ok := v.Int64(); ok {
goArgs[i] = int(v)
} else {
return nil, fmt.Errorf("integer too large")
}
case starlark.Float:
goArgs[i] = float64(v)
case starlark.String:
goArgs[i] = string(v)
case starlark.Bool:
goArgs[i] = bool(v)
default:
return nil, fmt.Errorf("unsupported argument type: %T", arg)
}
}
// Call the Go function
switch f := goFunc.(type) {
case func(int, int) int:
if len(goArgs) != 2 {
return nil, fmt.Errorf("Add function requires exactly 2 arguments")
}
a, ok1 := goArgs[0].(int)
b, ok2 := goArgs[1].(int)
if !ok1 || !ok2 {
return nil, fmt.Errorf("Add function requires integer arguments")
}
result := f(a, b)
return starlark.MakeInt(result), nil
default:
return nil, fmt.Errorf("unsupported function type: %T", goFunc)
}
}
func main() {
go func() {
// Create a new Starlark thread
thread := &starlark.Thread{
Name: "repl",
Print: func(thread *starlark.Thread, msg string) {
fmt.Println(msg)
},
}
// Create a new global environment with call_gofunc
globals := starlark.StringDict{
"call_gofunc": starlark.NewBuiltin("call_gofunc", callGoFunc),
}
// Create a scanner for reading input
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Starlark REPL (type 'exit' to quit)")
fmt.Println("Example1: starlark exprs and stmts")
fmt.Println("Example2: call_gofunc('Add', 1, 2)")
errExit := errors.New("exit")
for {
// Print prompt
fmt.Print(">>> ")
// Read input
readline := func() ([]byte, error) {
...
}
// Execute the input
if err := rep(readline, thread, globals); err != nil {
...
}
}
}()
select {}
}
调试器集成 starlark
go-delve/delve 中集成了starlark,并使用本文体积的方法来支持了对某些调试器内部函数的调用,比如:
//go:generate go run ../../../_scripts/gen-starlark-bindings.go go ./starlark_mapping.go
const (
dlvCommandBuiltinName = "dlv_command"
readFileBuiltinName = "read_file"
writeFileBuiltinName = "write_file"
commandPrefix = "command_"
dlvContextName = "dlv_context"
curScopeBuiltinName = "cur_scope"
defaultLoadConfigBuiltinName = "default_load_config"
helpBuiltinName = "help"
)
比如有下面的go源程序,我们使用 go-delve/delve 来进行自动化调试:
file: main.go (这里保留行号信息,方便与starlark脚本对应)
1 package main
2
3 import (
4 "fmt"
5 "time"
6 )
7
8 type Person struct {
9 Name string
10 Age int
11 }
12
13 func main() {
14 people := []Person{
15 {Name: "Alice", Age: 25},
16 {Name: "Bob", Age: 30},
17 {Name: "Charlie", Age: 35},
18 }
19
20 for i, p := range people {
21 fmt.Printf("Processing person %d: %s\n", i, p.Name)
22 time.Sleep(time.Second) // 添加一些延迟以便于调试
23 processPerson(p)
24 }
25 }
26
27 func processPerson(p Person) {
28 fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
29 }
starlark自动化调试脚本:
file: debug.star
# 定义一个函数来打印当前作用域的信息
def print_scope():
scope = cur_scope()
print("Current scope:", scope)
dlv_command("locals")
# 定义一个函数来设置断点并执行调试命令
def debug_person():
# 打印当前作用域
print_scope()
# 打印变量 p 的值
dlv_command("print p")
# 单步执行
dlv_command("next")
# 再次打印作用域
print_scope()
# 定义一个函数来保存调试信息到文件
def save_debug_info():
# 获取当前作用域
scope = cur_scope()
# 将调试信息写入文件
debug_info = "Debug session at " + str(time.time()) + "\n"
debug_info += "Current scope: " + str(scope) + "\n"
# 保存到文件
write_file("debug_info.txt", debug_info)
# 主函数
def main():
print("Starting debug session...")
# 设置断点
dlv_command("break main.main")
dlv_command("break main.processPerson")
# 继续执行到main.main
dlv_command("continue")
# 继续执行到main.processPerson
dlv_command("continue")
# 执行调试操作
debug_person()
# 保存调试信息
save_debug_info()
print("Debug session completed.")
# 直接调用 main 函数 (source命令会自动调用定义的 `main` 函数)
#main()
运行调试器 dlv debug main.go
,调试会话就绪后运行 source debug.star
即可。
$ tinydbg debug main.go
Type 'help' for list of commands.
(dlv) source debug.star
Starting debug session...
Breakpoint 1 set at 0x49d0f6 for main.main() ./main.go:13
Breakpoint 2 set at 0x49d40e for main.processPerson() ./main.go:27
> [Breakpoint 1] main.main() ./main.go:13 (hits goroutine(1):1 total:1) (PC: 0x49d0f6)
8: type Person struct {
9: Name string
10: Age int
11: }
12:
=> 13: func main() {
14: people := []Person{
15: {Name: "Alice", Age: 25},
16: {Name: "Bob", Age: 30},
17: {Name: "Charlie", Age: 35},
18: }
Processing person 0: Alice
> [Breakpoint 2] main.processPerson() ./main.go:27 (hits goroutine(1):1 total:1) (PC: 0x49d40e)
22: time.Sleep(time.Second) // 添加一些延迟以便于调试
23: processPerson(p)
24: }
25: }
26:
=> 27: func processPerson(p Person) {
28: fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
29: }
Current scope: api.EvalScope{GoroutineID:-1, Frame:0, DeferredCall:0}
(no locals)
main.Person {Name: "Alice", Age: 25}
> main.processPerson() ./main.go:28 (PC: 0x49d42a)
23: processPerson(p)
24: }
25: }
26:
27: func processPerson(p Person) {
=> 28: fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
29: }
Current scope: api.EvalScope{GoroutineID:-1, Frame:0, DeferredCall:0}
(no locals)
Debug session completed.
tinydbg暂时保留了go-delve/delve中的starlark实现,pkg/terminal/starlark.go + pkg/terminal/starlark_test.go 一共300行代码,starbind/ 下有近3000行代码,不过这部分代码是通过脚本自动生成的。由于这部分代码相对来说比较独立,不像ebpf-based tracing那样影响到很多地方,所以我们暂时保留这部分代码。上述测试用的源码、star脚本,您可以在路径 tinydbg/examples/starlark_demo 找到。
本文总结
我在学习bazelbuild时了解到starlark这门语言,在学习go-delve/delve时进一步了解了它。如果我们正在编写一个工具或者分析型工具,希望通过暴漏我们的底层能力,以让用户自由发挥他们的创造性用途,比如类似go-delve/delve希望用户可以按需执行自动化调试,我们其实可以将starlark解释器引擎集成到我们的程序中,然后通过一点胶水代码打通starlark与我们的程序,使得starlark解释器调用starlark函数来执行我们程序中定义的函数。这无疑会释放我们程序的底层能力,允许使用者在底层能力开放程度受控的情况下进一步去发挥、去挖掘。
本文演示了如何轻松starklark集成到您的Go程序中,starlark的更多用法请参考 bazelbuild/starlark。
本文还介绍了调试器与starlark集成以及使用示例,有自动化测试诉求,或者希望分享你的调试会话的时候,可以通过这种方式来实现。