一. gossa 项目简介

 gossa 是一个基于 SSA 实现的 Go 语言解释器,可以直接从 Go/Go+ 源码运行程序。下面是一些相关的项目资料:

- 项目地址

github.com/goplus/gossa

- 项目文档

pkg.go.dev/github.com/goplus/gossa

- 代码示例

github.com/visualfc/gossa_demo

- 项目应用:使用 gossa 实现的 spx 游戏解释器

github.com/goplus/ispx

- 项目应用:使用 gossa 实现的 JS 版本 Go+ Playground

jsplay.goplus.org

其中,JS 版本的 Go+ Playground(页面效果如下图所示) 是由 GopherJS 和 gossa 设计完成,最大的特点是仅由前端网页和 js 文件实现,并没有使用服务器。下载后可以在本地用浏览器直接打开使用。

之所以要开发 gossa 而不是利用现有的如 yaegi 脚本引擎项目,最大的原因是出于对 Go runtime 内存布局兼容性的考虑。如 func / var / const / struct / interface / methods 等,都需要与 Go runtime 保持内存布局兼容性(尤其是 interface 和 methods),而 yaegi 等脚本引擎项目无法与 Go runtime 保持内存布局兼容,实现我们的需求。

 这里我们举例说明。

 这是 Go 版本的 interface 示例代码,go run main.go:

package main
import "fmt"
type T struct { n int}
func (t *T) String() string { return fmt.Sprintf("T:%v", t.n)}func main() { fmt.Println("Hello World", &T{100})}

上述代码中 T 包含了 String 方法,T 实现了 fmt.Stringer 接口,打印的结果为:

Hello World T:100
Program exited.

这里  fmt.Println 调用了 T.String() 。假如修改 String() 的命名,不实现 Stringer 接口 ,那么打印出的将是如下的原始值:

Hello World &{100}
Program exited.

使用现有的 yaegi 版本引擎,运行相同源码时可发现 yaegi 中的 T 不兼容 fmt.Stringer 接口,只能打印原始值。这里并不是指 yaegi 引擎不支持 interface,而是它的 interface 支持仅限于 main.go 源码本身,调用外部库时无法提供 Go runtime 级别的内存布局兼容。

使用 gossa 版本引擎运行源码 gossa run main.go,打印的结果如下:

Hello World T:100
Program exited.

这里我们发现,gossa 除了实现 Stringer 接口,也正确的通过了外部库的调用。假如修改 String() 的命名,则和 go run 的情况保持一致打印原始值。

gossa 实现的主要功能,有如下几点:

1)支持 Go 语言规范

  • 主要通过 ast、types 以及 SSA 实现

  • https://go.dev/ref/spec

2)内建支持 Go+ 源码运行

3)与 Go runtime 保持内存布局兼容

  • func / var / const / struct / interface / methods

4)无依赖执行

  • 编译后不需要 Go 平台支持,可独立运行和执行 Go/Go+ 源码。

5)自定义导入包

  • 根据需要自定义允许使用的导入包,标准库可剪裁。提供 qexp 工具,方便自动生成需要的导入包。

6)多平台支持

  • Native 平台 Go1.16/Go1.17 (amd64 下需要设置 GOEXPERIMENT=noregabi)

  • WASM/GopherJS 平台支持

关于 gossa 的使用限制,有如下几点:

 

1)目前只支持单个 package 的读取和运行

  • 比如调用 main 包,支持标准库调用,但不支持在 main 中调用其他包。若想引入其他包目前需采用 qexp 导入库的方式(后续会优化改进)

2)只允许 gossa/interp 单个实例运行

3)不支持 ASM,CGO,go:linkname symbol

4)Go1.17 amd64 需要设置 GOEXPERIMENT=noregabi 编译

  • GOEXPERIMENT=noregabi go build demo

5)Go1.18 amd64 默认使用 regabi 目前无法运行

  • typed methods 数量限制,预分配 256 个。如有需要可借助 import "github.com/reflectx/icall/icall[2^n]" 增加数量,或使用 reflectx/cmd/icallgen 生成需要的 methods

6)GopherJS 支持 ( go 1.12 ~ go1.16 )

  • 需使用该版本:github.com/goplusjs/gopherjs

二. gossa 使用示例

首先来看 Go 版本 的 Hello World,运行方式 go run main.go,代码如下:

package mainimport "fmt"func main() { fmt.Println("Hello, World")}

运行结果如下:

Hello, World
Program exited.

Go+ 版本的 Hello world,运行方式 gop run main.go,输出结果相同输入的代码则简洁很多:

println "Hello, World"

那么。go run 的运行过程发生了什么?我们加一个参数 -x 试验一下,使用 go run -x main.go 运行:

package mainimport "fmt"func main() { fmt.Println("Hello, World")}

输出结果为:WORK=/var/folders/q1/k7thlrnj2xx_6x47yzw13m900000gn/T/go-build4007070415mkdir -p $WORK/b001/cat >$WORK/b001/_gomod_.go << 'EOF' # internalpackage mainimport _ "unsafe"//go:linkname __debug_modinfo__ runtime.modinfovar __debug_modinfo__ = "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nmod\tcommand-line-arguments\t(devel)\t\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"EOFcat >$WORK/b001/importcfg << 'EOF' # internal# import configpackagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.apackagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.aEOFcd /private/var/folders/q1/k7thlrnj2xx_6x47yzw13m900000gn/T/present-065041032/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid 5CqKmyHwwXn8flUXi2Za/5CqKmyHwwXn8flUXi2Za -dwarf=false -goversion go1.16.7 -D _/private/var/folders/q1/k7thlrnj2xx_6x47yzw13m900000gn/T/present-065041032 -importcfg $WORK/b001/importcfg -pack -c=4 ./hello.go $WORK/b001/_gomod_.go/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internalcp $WORK/b001/_pkg_.a Caches/go-build/9c/9c56507db9a2f8f5d963f4079085652d4b828a72b5d4c9345e13f706a01d3f15-d # internalcat >$WORK/b001/importcfg.link << 'EOF' # internalpackagefile command-line-arguments=$WORK/b001/_pkg_.apackagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.apackagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.apackagefile errors=/usr/local/go/pkg/darwin_amd64/errors.apackagefile internal/fmtsort=/usr/local/go/pkg/darwin_amd64/internal/fmtsort.apackagefile io=/usr/local/go/pkg/darwin_amd64/io.apackagefile math=/usr/local/go/pkg/darwin_amd64/math.apackagefile os=/usr/local/go/pkg/darwin_amd64/os.apackagefile reflect=/usr/local/go/pkg/darwin_amd64/reflect.apackagefile strconv=/usr/local/go/pkg/darwin_amd64/strconv.apackagefile sync=/usr/local/go/pkg/darwin_amd64/sync.apackagefile unicode/utf8=/usr/local/go/pkg/darwin_amd64/unicode/utf8.apackagefile internal/bytealg=/usr/local/go/pkg/darwin_amd64/internal/bytealg.apackagefile internal/cpu=/usr/local/go/pkg/darwin_amd64/internal/cpu.apackagefile runtime/internal/atomic=/usr/local/go/pkg/darwin_amd64/runtime/internal/atomic.apackagefile runtime/internal/math=/usr/local/go/pkg/darwin_amd64/runtime/internal/math.apackagefile runtime/internal/sys=/usr/local/go/pkg/darwin_amd64/runtime/internal/sys.apackagefile internal/reflectlite=/usr/local/go/pkg/darwin_amd64/internal/reflectlite.apackagefile sort=/usr/local/go/pkg/darwin_amd64/sort.apackagefile math/bits=/usr/local/go/pkg/darwin_amd64/math/bits.apackagefile internal/oserror=/usr/local/go/pkg/darwin_amd64/internal/oserror.apackagefile internal/poll=/usr/local/go/pkg/darwin_amd64/internal/poll.apackagefile internal/syscall/execenv=/usr/local/go/pkg/darwin_amd64/internal/syscall/execenv.apackagefile internal/syscall/unix=/usr/local/go/pkg/darwin_amd64/internal/syscall/unix.apackagefile internal/testlog=/usr/local/go/pkg/darwin_amd64/internal/testlog.apackagefile io/fs=/usr/local/go/pkg/darwin_amd64/io/fs.apackagefile sync/atomic=/usr/local/go/pkg/darwin_amd64/sync/atomic.apackagefile syscall=/usr/local/go/pkg/darwin_amd64/syscall.apackagefile time=/usr/local/go/pkg/darwin_amd64/time.apackagefile internal/unsafeheader=/usr/local/go/pkg/darwin_amd64/internal/unsafeheader.apackagefile unicode=/usr/local/go/pkg/darwin_amd64/unicode.apackagefile internal/race=/usr/local/go/pkg/darwin_amd64/internal/race.apackagefile path=/usr/local/go/pkg/darwin_amd64/path.aEOFmkdir -p $WORK/b001/exe/cd ./usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=w1zDV2gjBLCV5hGTPRDW/5CqKmyHwwXn8flUXi2Za/XmsgdfkERMQ_ns_ncZDK/w1zDV2gjBLCV5hGTPRDW -extld=clang $WORK/b001/_pkg_.a$WORK/b001/exe/helloHello, WorldProgram exited.

通过运行结果可以发现,go run 时在后台先进行了编译,之后再运行,而不是直接运行。。gossa 版本引擎运行 Go/Go+ 的 Hello world 则不需要编译过程,直接运行源码。

目前,gossa 的组件包含如下几类:

• gossa package

github.com/goplus/gossa

• Go+ 编译支持

github.com/goplus/gossa/gopbuild

•gossa 导入包

github.com/goplus/gossa/pkg/...

github.com/goplus/gossa/tree/main/pkg(其中的包与 Go 标准库中的包一一对应)

• gossa 命令程序,测试用

github.com/goplus/gossa/cmd/goss

•gossa 导入库生成工具

github.com/goplus/gossa/cmd/qexp

通常情况下,不会直接使用 cdm/gossa 命令,而是基于 github.com/goplus/gossa 做自定义应用程序。大致流程如下:

import ( "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt" _ "github.com/goplus/gossa/pkg/io" _ "github.com/goplus/gossa/pkg/os" 。。。)

我们来看几个示例:

1. gossa demo 1

  • 运行 Go 版本 Hello World:

package mainimport ( "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World")}`func main() { _, err := gossa.RunFile("main.go", source, nil, 0) if err != nil { panic(err) }}

如上述代码所示,首先 import gossa,因为我们需要执行的 Go 源码 source 中导入了 "fmt" 包。那么代码中我们要对应导入  github.com/goplus/gossa/pkg/fmt。之后使用 gossa.RunFile 运行 Go 源码 ,编译运行后如下图所示,执行了 go 的源码,并调用 fmt.Println 打印 "Hello, World"。

go: creating new go.mod: module proggo: to add module requirements and sums: go mod tidygo: finding module for package github.com/goplus/gossago: finding module for package github.com/goplus/gossa/pkg/fmtgo: found github.com/goplus/gossa in github.com/goplus/gossa v0.2.14go: found github.com/goplus/gossa/pkg/fmt in github.com/goplus/gossa v0.2.14go build -o prog./progHello, WorldProgram exited.

2. gossa demo 2

  • 运行 Go+ 版本 Hello World

package mainimport ( "github.com/goplus/gossa" _ "github.com/goplus/gossa/gopbuild" _ "github.com/goplus/gossa/pkg/fmt")var source = `println "Hello, World"`func main() { _, err := gossa.RunFile("main.gop", source, nil, 0) if err != nil { panic(err) }}

代码与 Go 版本的类似,在这里我们加入一行代码以支持 Go+ 编译:

_ "github.com/goplus/gossa/gopbuild"

gossa.Runfile 中的 "main.go" 也替换为"main.gop"。这就是 gossa 的一个注册机制,将扩展名的处理函数由 gobuild 注册进去,在 gopbuild 中实现 Go+ 源码到 Go 源码的转化。当gossa.RunFile 运行"main.gop"而不是 "main.go" 时,内部就会做相应的调用处理。

3. gossa demo 3

  • 运行 interface 版本 Hello World ( 实现 Go runtime 内存布局兼容)

package mainimport ( "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"type T struct {}func (t T) String() string { return "Hello, World" }func main() { fmt.Println(&T{})}`func main() { _, err := gossa.RunFile("main.go", source, nil, 0) if err != nil { panic(err) }}

该段代码展示的主要是 T 实现 fmt 的 String 接口,也就是说 gossa 运行源码和 fmt.Stinger 接口完全兼容。

 4. gossa demo 4

  • main.go 源码与外部包 fmt.Stringer/fmt.GoStringer 兼容

package mainimport ( "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"type M struct { N int}type T struct { N int}func (t T) String() string { return "T:String" }type U struct { T}func (u U) GoString() string { return "U:GoString"}func main() { m := &M{100} t := &T{100} u := &U{T{100}} fmt.Printf("%v %#v\n",m,m) fmt.Printf("%v %#v\n",t,t) fmt.Printf("%v %#v\n",u,u)}`func main() { _, err := gossa.RunFile("main.go", source, nil, 0) if err != nil { panic(err) }}

上述代码中 M 没有实现任何接口,T 实现了 Stringer 接口,U 实现了 GoStringer 接口,同时因为 U 嵌入了 T,所以 U 也支持 Stringer 接口;fmt.Printf 中的 %v 对应的是 Stinger 接口,%#v 对应的是 GoStringer 接口。将 M、T、U 分别打印,结果如下:

&{100} &main.M{N:100}T:String &main.T{N:100}T:Sting U:GoString

M 因为没有实现任何接口,所以打印出的是原始值与结构体的值;T 打印出 Stringer 接口与结构体的值;U 打印出 Stringer 接口与 GoStringer 接口值。

5. gossa demo 5

  • Go 程序退出状态值 exit status code

package mainimport ( "fmt" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt" _ "github.com/goplus/gossa/pkg/os")var source = `package mainimport ( "fmt" "os")func main() { fmt.Println("Hello, World") os.Exit(2)}`func main() { code, err := gossa.RunFile("main.go", source, nil, 0) if err != nil { panic(err) } fmt.Println("exit", code)}

对于应用程序来说,一般都有退出状态。上述代码便是在 gossa 中如何获得退出状态。代码首先导入两个库 "fmt" 与 '"os",运行完代码块后,借助 os.Exit(2) 退出。其中,RunFile 的参数 code 可以获取返回值,即 os.Exit(2)中设置的返回值 2。代码运行结果如下:

go: creating new go.mod: module proggo: to add module requirements and sums: go mod tidygo: finding module for package github.com/goplus/gossa/pkg/osgo: finding module for package github.com/goplus/gossago: finding module for package github.com/goplus/gossa/pkg/fmtgo: found github.com/goplus/gossa in github.com/goplus/gossa v0.2.14go: found github.com/goplus/gossa/pkg/fmt in github.com/goplus/gossa v0.2.14go: found github.com/goplus/gossa/pkg/os in github.com/goplus/gossa v0.2.14go build -o prog./progHello, Worldexit 2Program exited.

6. gossa demo 6

  • Go+ 程序退出状态值 exit status code

package mainimport ( "fmt" "github.com/goplus/gossa" _ "github.com/goplus/gossa/gopbuild" _ "github.com/goplus/gossa/pkg/os")var source = `import "os"println "Hello, World"os.exit(2)`func main() { code, err := gossa.RunFile("main.gop", source, nil, 0) if err != nil { panic(err) } fmt.Println("exit code", code)}

在 Go+ 中,需额外增加下述代码以支持 Go+ 编译:

_ "github.com/goplus/gossa/gopbuild"

 运行结果与上例相同。

7. gossa demo 7

  • Go panic 处理

package mainimport ( "fmt" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World") panic("error")}`func main() { _, err := gossa.RunFile("main.go", source, nil, 0) if err != nil { fmt.Println("PANIC:", err) }}

如果主动发出一个 panic,RunFile 的参数 err 就会给出 panic 的值,也就是 RunFile 时自动执行了 recover(恢复操作),使得不会因为 source Go+源码的 panic 使得整个调用程序也出现 panic,获得的只是一个错误值。代码运行结果如下:

Hello,WorldPANIC:error

8. gossa demo 8

  • Go+ panic 处理

package mainimport ( "fmt" "github.com/goplus/gossa" _ "github.com/goplus/gossa/gopbuild" _ "github.com/goplus/gossa/pkg/fmt")var source = `var i intprintln 100/i`func main() { _, err := gossa.RunFile("main.gop", source, nil, 0) if err != nil { fmt.Println("PANIC:", err) }}

 这是 Go+ 的源码,运行结果是一个 runtime 的除零错误。

go: creating new go.mod: module proggo: to add module requirements and sums: go mod tidygo: finding module for package github.com/goplus/gossago: finding module for package github.com/goplus/gossa/gopbuildgo: finding module for package github.com/goplus/gossa/pkg/fmtgo: found github.com/goplus/gossa in github.com/goplus/gossa v0.2.14go: found github.com/goplus/gossa/gopbuild in github.com/goplus/gossa v0.2.14go: found github.com/goplus/gossa/pkg/fmt in github.com/goplus/gossa v0.2.14go build -o prog./progPANIC: runtime error: integer divide by zeroProgram exited.

 如果我们将上述代码中的 i 设置为 i1,运行的结果将是:

PANIC:main.gop:3:13:undefined:i
Pogram exited.

意思是在第 3 行没有找到 i。这里就有一个问题,在 RunFile 时无法判断是编译时的错误还是运行时的错误,如果将 i 设置为 i1,便是编译时的错误,而之前则是运行时的错误。

对于这种情况,我们可以使用 gossa 的底层控制功能:

- gossa.Context 上下文/环境控制

包含一系列的函数,如:LoadFile/LoadAst/RunPkg/TestPkg/NewInterp...

- gossa.Interp 执行引擎

Run/RunFunc/GetFunc...

9. gossa demo 9

  • 文件加载

package mainimport ( "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World")}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } _, err = ctx.RunPkg(pkg, "main.go", nil) if err != nil { log.Panicln("run", err) }}

这个示例是使用 ssa.Context 加载运行源码,上述代码中 source 与之前示例完全一致,这里我们使用 gossa.Context 建立上下文控制,通过 ctx.LoadFile 获得 ssa.Package,然后使用ctx.RunPkg 运行。运行结果如下:

Hello,World

 这里运行结果与之前一样,但示例代码中可以区分编译器错误和运行时错误。

10. gossa demo 10

  • AST 加载

package mainimport ( "go/parser" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World")}`func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "main.go", source, parser.ParseComments) if err != nil { log.Panicln("parse", err) } ctx := gossa.NewContext(0) pkg, err := ctx.LoadAstFile(fset, f) if err != nil { log.Panicln("load", err) } _, err = ctx.RunPkg(pkg, "main.go", nil) if err != nil { log.Panicln("run", err) }}

这个示例里先使用 Go 语言提供的 parser.ParserFile 获取 AST ,之后通过 ctx.LoadAstFile 函数加载,从 AST 中直接生成 ssa.Package。运行结果和之前一致。

11. gossa demo 11

  • Interp 执行引擎

package mainimport ( "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World")}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } _, err = interp.Run("main") if err != nil { log.Panicln("run", err) }}

 这个与之前示例一样,首先 ctx.LoadFile 生成 ssa.Package,但 这次我们通过 gossa.NewInterp 函数生成 Interp 执行引擎来运行。执行结果与之前示例一致。

 12. gossa demo 12

  • 函数调用

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package mainimport "fmt"func main() { fmt.Println("Hello, World")}func add(i, j int) int { return i+j}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } v, err := interp.RunFunc("add", 100, 200) fmt.Println(v, err)}

 main 中新建了函数 add,应该如何调用 add 函数?首先建立执行引擎  NewInterp,然后 RunFunc 调用,传用对应参数来调用和获取结果,如果出现错误, RunFunc 内部也会进行  recover 操作。运行结果如下:

300 <nil>

13. gossa demo 13

  • 可变参数函数调用

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package barfunc sum(n ...int) (r int) { for _, v := range n { r += v } return}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } v1, err := interp.RunFunc("sum", []int{100, 200}) fmt.Println(v1, err) v2, err := interp.RunFunc("sum", []int{}) fmt.Println(v2, err) // error call v3, err := interp.RunFunc("sum") fmt.Println(v3, err)}

在可变参数函数调用中,代码中的参数有所区别,需要使用切片 ( slice ) 操作。sum(n ...int) 实际上是个语法糖,需要将其转换为对应的切片类型,如果切片内容为空,也需要传入一个空的切片,如果什么也不传,调用是则错误的,代码运行结果如下:

300 <nil>0 <nil><nil> runtime error: index out of range [0] with length 0

14. gossa demo 14

  • 多返回值处理

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package barfunc call(i, j int) (int,int) { return i+j, i*j}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } v, err := interp.RunFunc("call", 100, 200) fmt.Println(v, err) if t, ok := v.(gossa.Tuple); ok { fmt.Println(len(t), t[0], t[1]) }}

 同 demo 13,如果是函数有多个返回值,那么实际类型为  gossa.Tuple,运行结果如下:

[300 20000] <nil>2 300 20000

 对于 Interp 类型 ,包含以下函数:

  • Run/RunFunc 函数带有错误处理功能(如 demo 14 所示)

  • GetFunc 函数

  • GetVarAddr 变量地址

  • GetConst 常量

  • GetType 类型

下面我们看一下使用 Interp 使用示例。

15. gossa demo 15

  • 获取函数

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package barfunc add(i, j int) int { return i+j}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } if v, ok := interp.GetFunc("add"); ok { if fn, ok := v.(func(int, int) int); ok { r := fn(100, 200) fmt.Println(r) } }}

 代码中,我们使用 interp.GetFunc("add") 获取函数并转换,返回值是一个 interface{} 接口类型,需要转换为对应的函数类型,如果转换正确我们 两个参数调用。代码运行结果如下:

300

 结果正确,成功调用了函数 Add。

16. gossa demo 16

  • 获取变量地址

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")var source = `package barvar index int = 100var pindex *int = &indexfunc show() { println(index)}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } if v, ok := interp.GetVarAddr("index"); ok { if p, ok := v.(*int); ok { fmt.Println(*p) *p = 200 } } if v, ok := interp.GetVarAddr("pindex"); ok { if p, ok := v.(**int); ok { fmt.Println(**p) **p = 300 } } if fn, ok := interp.GetFunc("show"); ok { fn.(func())() }}

首先是 ctx.LoadFile ,之后 ctx.NewInterp 建立一个 Interp 执行引擎,通过 GetVarAddr 函数获取 index 变量的地址,这是一个 interface{} 类型。index 本身是 int 型,但因为获取了地址,所以得到的是 int 类型;pindex 的类型本身是指针,得到的结果便是指针的指针 *int。代码运行结果如下:

100 200 300

17. gossa demo 17

  • 获取常量值

package mainimport ( "fmt" "go/token" "log" "github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt" _ "github.com/goplus/gossa/pkg/math")var source = `package barimport "math"const Pi = math.Pifunc add(i, j int) int { return i+j}`func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } // go/constant.Value if v, ok := interp.GetConst("Pi"); ok { fmt.Printf("%v %T\n", v, v) fmt.Println(v.ExactString()) }}

 函数和变量的返回值,都是 interface 类型,但常量我们不能用  interface 类型表示,可能会超出范围,在上段代码中返回的是 go/constant.Value。代码执行结果如下:3.14159 constant.ratVal314159265358979323846264338327950288419716939937510582097494459/100000000000000000000000000000000000000000000000000000000000000

在这里我们发现 Pi 在 Go 内部使用分数的形式实现,没有使用 float64。

18. gossa demo 18

  • 获取类型

package main
import ( "fmt" "go/token" "log" "reflect"

"github.com/goplus/gossa" _ "github.com/goplus/gossa/pkg/fmt")
var source = `package bar
import "fmt"
type Point struct { x, y int}
func (p *Point) Set(x, y int) { p.x,p.y = x,y}
func (p *Point) String() string { return fmt.Sprintf("(%v,%v)",p.x,p.y)}`
func main() { fset := token.NewFileSet() ctx := gossa.NewContext(0) pkg, err := ctx.LoadFile(fset, "main.go", source) if err != nil { log.Panicln("load", err) } interp, err := ctx.NewInterp(pkg) if err != nil { log.Panicln("interp", err) } if typ, ok := interp.GetType("Point"); ok { v := reflect.New(typ) fn := v.MethodByName("Set") fn.Interface().(func(int, int))(100, 200) fmt.Println(v) }}

这个示例里 通过 interp.GetType 获取 Point 对应的  reflect.Type 类型,之后通过 reflect 函数来做相关调用处理,上述代码运行结果为:

(100,200)

三. gossa 实战

 写一个 ispx 程序来运行 github.com/goplus/spx 应用 ( Go+ class file )

ispx 项目地址:github.com/goplus/ispx

ispx 项目使用步骤如下:

- 安装 ispx

$ go get github.com/goplus/ispx

- 下载 FlappyCalf demo

$ git clone https://github.com/goplus/FlappyCalf

- 运行方式 1

$ ispx FlappyCalf

- 运行方式 2

$ cd FlappyCalf$ ispx .

1. ispx 实现代码

package main
//go:generate qexp -outdir pkg github.com/goplus/spximport ( "flag" "fmt" "log" "os"    "path/filepath"
"github.com/goplus/gossa" "github.com/goplus/gossa/gopbuild" "github.com/goplus/spx"
_ "github.com/goplus/gossa/pkg/fmt" _ "github.com/goplus/gossa/pkg/math" _ "github.com/goplus/ispx/pkg/github.com/goplus/spx"
_ "github.com/goplus/reflectx/icall/icall8192")
var ( flagDumpSrc bool flagDumpPkg bool flagDumpSSA bool)
func init() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "ispc [-dumpsrc|-dumppkg|-dumpssa] dir\n") flag.PrintDefaults() } flag.BoolVar(&flagDumpSrc, "dumpsrc", false, "print source code") flag.BoolVar(&flagDumpPkg, "dumppkg", false, "print import packages") flag.BoolVar(&flagDumpSSA, "dumpssa", false, "print ssa code information")}
func main() { flag.Parse() args := flag.Args() if len(args) != 1 { flag.Usage() return } path := args[0] var mode gossa.Mode if flagDumpPkg { mode |= gossa.EnableDumpPackage } if flagDumpSSA { mode |= gossa.EnableTracing } ctx := gossa.NewContext(mode) data, err := gopbuild.BuildDir(ctx, path) if err != nil { log.Panicln(err) } if flagDumpSrc { fmt.Println(string(data)) } if !filepath.IsAbs(path) { dir, _ := os.Getwd() path = filepath.Join(dir, path) } gossa.RegisterExternal("github.com/goplus/spx.Gopt_Game_Run", func(game spx.Gamer, resource interface{}, gameConf ...*spx.Config) { os.Chdir(path) spx.Gopt_Game_Run(game, resource, gameConf...) }) _, err = ctx.RunFile("main.go", data, nil) if err != nil { log.Panicln(err) }}

 因为需要引用 spx 库,我们需要使用  qexp 转换 github.com/goplus/spx ,这里我们使用 go generate qexp -outdir pkg github.com/goplus/spx 来自动生成对应的导入包。导入 github.com/goplus/gossa/gopbuild 实现 Go+ 代码支持。在 ispx 代码里我们多加了 github.com/goplus/reflectx/icall/icall8192 ,这一行代码是为了支持更多的方法集调用。

而代码里的 gossa.RegisterExternal 相当于对 spx.Gopt_Game_Run 做了一个 hook 拦截调用功能,我们在其中做了相应的处理之后,再调用实际函数。

2. ispx 依赖包

  • $ ispx -dumppkg .

$ ispx -dumppkg .2021/12/23 08:27:56 imported package spx ("github.com/goplus/spx")2021/12/23 08:27:56 imported package math ("math")2021/12/23 08:27:56 indirect package camera ("github.com/goplus/spx/internal/camera")2021/12/23 08:27:56 indirect package tools ("github.com/goplus/spx/internal/tools")2021/12/23 08:27:56 indirect package atlas ("github.com/hajimehoshi/ebiten/v2/internal/atlas")2021/12/23 08:27:56 indirect package graphicscommand ("github.com/hajimehoshi/ebiten/v2/internal/graphicscommand")2021/12/23 08:27:56 indirect package constant ("go/constant")2021/12/23 08:27:56 indirect package fs ("github.com/goplus/spx/fs")2021/12/23 08:27:56 indirect package math32 ("github.com/goplus/spx/internal/math32")2021/12/23 08:27:56 imported package fmt ("fmt")2021/12/23 08:27:56 indirect package rand ("math/rand")2021/12/23 08:27:56 indirect package audio ("github.com/hajimehoshi/ebiten/v2/audio")2021/12/23 08:27:56 indirect package reflect ("reflect")2021/12/23 08:27:56 indirect package ebiten ("github.com/hajimehoshi/ebiten/v2")2021/12/23 08:27:56 indirect package mipmap ("github.com/hajimehoshi/ebiten/v2/internal/mipmap")2021/12/23 08:27:57 indirect package restorable ("github.com/hajimehoshi/ebiten/v2/internal/restorable")2021/12/23 08:27:57 imported package strconv ("strconv")2021/12/23 08:27:57 indirect package io ("io")2021/12/23 08:27:57 imported package builtin ("github.com/goplus/gop/builtin")2021/12/23 08:27:57 indirect package driver ("github.com/hajimehoshi/ebiten/v2/internal/driver")2021/12/23 08:27:57 indirect package v2 ("github.com/hajimehoshi/oto/v2")2021/12/23 08:27:57 imported package unsafe ("unsafe")2021/12/23 08:27:57 imported package strings ("strings")2021/12/23 08:27:57 indirect package color ("image/color")2021/12/23 08:27:57 indirect package time ("time")2021/12/23 08:27:57 indirect package anim ("github.com/goplus/spx/internal/anim")2021/12/23 08:27:57 indirect package buffered ("github.com/hajimehoshi/ebiten/v2/internal/buffered")2021/12/23 08:27:57 indirect package affine ("github.com/hajimehoshi/ebiten/v2/internal/affine")2021/12/23 08:27:57 indirect package shaderir ("github.com/hajimehoshi/ebiten/v2/internal/shaderir")2021/12/23 08:27:57 indirect package packing ("github.com/hajimehoshi/ebiten/v2/internal/packing")2021/12/23 08:27:57 indirect package gdi ("github.com/goplus/spx/internal/gdi")2021/12/23 08:27:57 indirect package audiorecord ("github.com/goplus/spx/internal/audiorecord")2021/12/23 08:27:57 imported package big ("math/big")2021/12/23 08:27:57 indirect package sync ("sync")2021/12/23 08:27:57 indirect package unicode ("unicode")2021/12/23 08:27:57 indirect package image ("image")2021/12/23 08:27:57 indirect package coroutine ("github.com/goplus/spx/internal/coroutine")

从这个 log 里可以发现我们在代码中直接导入的 spx、math 等包,还有其他如 sync 等没有直接导入的包。其中 imported 标志 是直接导入包,使用 qexp 生成,对应实际包,indirect 标志是间接导入,间接导入是由 gossa 内部生成、模拟的 types.Package 包,不是真实的库。这就是 ispx 能脱离 Go/Go+ 环境运行的关键。

3. ispx 的 spx 导入包

  • 使用 qexp 生成导入包 $ qexp -outdir pkg github.com/goplus/spx

// export by github.com/goplus/gossa/cmd/qexppackage spximport ( q "github.com/goplus/spx" "go/constant" "reflect" "github.com/goplus/gossa")func init() { gossa.RegisterPackage(&gossa.Package{ Name: "spx", Path: "github.com/goplus/spx", Deps: map[string]string{ "encoding/json": "json", "errors": "errors", "flag": "flag", "fmt": "fmt", "github.com/goplus/spx/fs": "fs", "github.com/goplus/spx/fs/asset": "asset", "github.com/goplus/spx/fs/zip": "zip", "github.com/goplus/spx/internal/anim": "anim", "github.com/goplus/spx/internal/audiorecord": "audiorecord", "github.com/goplus/spx/internal/camera": "camera", "github.com/goplus/spx/internal/coroutine": "coroutine", "github.com/goplus/spx/internal/effect": "effect", "github.com/goplus/spx/internal/gdi": "gdi", "github.com/goplus/spx/internal/gdi/clrutil": "clrutil", "github.com/goplus/spx/internal/gdi/font": "font", "github.com/goplus/spx/internal/math32": "math32", "github.com/goplus/spx/internal/tools": "tools", "github.com/hajimehoshi/ebiten/v2": "ebiten", "github.com/hajimehoshi/ebiten/v2/audio": "audio", "github.com/pkg/errors": "errors", "github.com/qiniu/audio": "audio", "github.com/qiniu/audio/convert": "convert", "github.com/qiniu/audio/mp3": "mp3", "github.com/qiniu/audio/wav": "wav", "github.com/qiniu/audio/wav/adpcm": "adpcm", "golang.org/x/image/colornames": "colornames", "golang.org/x/image/font": "font", "image": "image", "image/color": "color", "image/jpeg": "jpeg", "image/png": "png", "io": "io", "log": "log", "math": "math", "math/rand": "rand", "os": "os", "path": "path", "path/filepath": "filepath", "reflect": "reflect", "strconv": "strconv", "strings": "strings", "sync": "sync", "sync/atomic": "atomic", "syscall": "syscall", "time": "time", "unsafe": "unsafe", }, Interfaces: map[string]reflect.Type{ "Gamer": reflect.TypeOf((*q.Gamer)(nil)).Elem(), "Shape": reflect.TypeOf((*q.Shape)(nil)).Elem(), }, NamedTypes: map[string]gossa.NamedType{ "Camera": {reflect.TypeOf((*q.Camera)(nil)).Elem(), "", "ChangeXYpos,On,SetXYpos,init,isWorldRange,render,screenToWorld,updateOnObj"}, "Config": {reflect.TypeOf((*q.Config)(nil)).Elem(), "", ""}, "EffectKind": {reflect.TypeOf((*q.EffectKind)(nil)).Elem(), "String", ""}, "Game": {reflect.TypeOf((*q.Game)(nil)).Elem(), "", "Answer,Ask,Broadcast__0,Broadcast__1,Broadcast__2,ChangeEffect,ChangeVolume,ClearSoundEffects,Draw,EraseAll,HideVar,KeyPressed,Layout,Loudness,MouseHitItem,MousePressed,MouseX,MouseY,NextScene,Play__0,PrevScene,ResetTimer,SceneIndex,SceneName,SetEffect,SetVolume,ShowVar,StartScene,StopAllSounds,Timer,Update,Username,Volume,Wait,activateShape,addClonedShape,addShape,addSpecialShape,addStageSprite,addStageSprites,currentTPS,doBroadcast,doFindSprite,doWhenLeftButtonDown,doWindowSize,doWorldSize,drawBackground,endLoad,eventLoop,findSprite,fireEvent,getItems,getMousePos,getSharedImgs,getTurtle,getWidth,goBackByLayers,handleEvent,initEventLoop,initGame,loadIndex,loadSound,loadSprite,movePen,objectPos,onDraw,onHit,removeShape,reset,runLoop,setStageMonitor,stampCostume,startLoad,startTick,touchingPoint,touchingSpriteBy,updateMousePos,windowSize_,worldSize_"}, "List": {reflect.TypeOf((*q.List)(nil)).Elem(), "", "Append,At,Contains,Delete,Init,InitFrom,Insert,Len,Set,String"}, "MovingInfo": {reflect.TypeOf((*q.MovingInfo)(nil)).Elem(), "", "Dx,Dy,StopMoving"}, "RotationStyle": {reflect.TypeOf((*q.RotationStyle)(nil)).Elem(), "", ""}, "Sound": {reflect.TypeOf((*q.Sound)(nil)).Elem(), "", ""}, "Sprite": {reflect.TypeOf((*q.Sprite)(nil)).Elem(), "", "Animate,Ask,BounceOffEdge,Bounds,ChangeEffect,ChangeHeading,ChangePenColor,ChangePenHue,ChangePenShade,ChangePenSize,ChangeSize,ChangeXYpos,ChangeXpos,ChangeYpos,ClearGraphEffects,CostumeHeight,CostumeIndex,CostumeName,CostumeWidth,Destroy,Die,DistanceTo,Glide__0,Glide__1,GoBackLayers,Goto,GotoBack,GotoFront,Heading,Hide,HideVar,InitFrom,IsCloned,Move__0,Move__1,NextCostume,OnCloned__0,OnCloned__1,OnMoving__0,OnMoving__1,OnTouched__0,OnTouched__1,OnTouched__2,OnTouched__3,OnTouched__4,OnTouched__5,OnTurning__0,OnTurning__1,Parent,PenDown,PenUp,Pixel,PrevCostume,Say,SetCostume,SetDying,SetEffect,SetHeading,SetPenColor,SetPenHue,SetPenShade,SetPenSize,SetRotationStyle,SetSize,SetXYpos,SetXpos,SetYpos,Show,ShowVar,Size,Stamp,Step__0,Step__1,Step__2,Think,Touching,TouchingColor,Turn,TurnTo,Visible,Xpos,Ypos,checkTouchingScreen,doDestroy,doMoveTo,doMoveToForAnim,doStopSay,doTurnTogether,doUpdatePenColor,draw,fireTouched,fixWorldRange,getDrawInfo,getFromAnToForAni,getRotatedRect,getTrackPos,getXY,goAnimate,goMoveForward,hit,init,requireGreffUniforms,sayOrThink,setDirection,setPenHue,setPenShade,setPenWidth,touchPoint,touchRotatedRect,touchedColor_,touchingSprite,waitStopSay"}, "StopKind": {reflect.TypeOf((*q.StopKind)(nil)).Elem(), "", ""}, "TurningInfo": {reflect.TypeOf((*q.TurningInfo)(nil)).Elem(), "", "Dir"}, "Value": {reflect.TypeOf((*q.Value)(nil)).Elem(), "Equal,Float,Int,String", ""}, }, AliasTypes: map[string]reflect.Type{ "Color": reflect.TypeOf((*q.Color)(nil)).Elem(), "Key": reflect.TypeOf((*q.Key)(nil)).Elem(), "Spriter": reflect.TypeOf((*q.Spriter)(nil)).Elem(), }, Vars: map[string]reflect.Value{}, Funcs: map[string]reflect.Value{ "Exit__0": reflect.ValueOf(q.Exit__0), "Exit__1": reflect.ValueOf(q.Exit__1), "Gopt_Game_Main": reflect.ValueOf(q.Gopt_Game_Main), "Gopt_Game_Reload": reflect.ValueOf(q.Gopt_Game_Reload), "Gopt_Game_Run": reflect.ValueOf(q.Gopt_Game_Run), "Gopt_Sprite_Clone__0": reflect.ValueOf(q.Gopt_Sprite_Clone__0), "Gopt_Sprite_Clone__1": reflect.ValueOf(q.Gopt_Sprite_Clone__1), "Iround": reflect.ValueOf(q.Iround), "RGB": reflect.ValueOf(q.RGB), "RGBA": reflect.ValueOf(q.RGBA), "Rand__0": reflect.ValueOf(q.Rand__0), "Rand__1": reflect.ValueOf(q.Rand__1), "Sched": reflect.ValueOf(q.Sched), "SchedNow": reflect.ValueOf(q.SchedNow), "SetDebug": reflect.ValueOf(q.SetDebug), }, TypedConsts: map[string]gossa.TypedConst{ "AllOtherScripts": {reflect.TypeOf(q.AllOtherScripts), constant.MakeInt64(int64(q.AllOtherScripts))}, "AllSprites": {reflect.TypeOf(q.AllSprites), constant.MakeInt64(int64(q.AllSprites))}, "BrightnessEffect": {reflect.TypeOf(q.BrightnessEffect), constant.MakeInt64(int64(q.BrightnessEffect))}, "ColorEffect": {reflect.TypeOf(q.ColorEffect), constant.MakeInt64(int64(q.ColorEffect))}, "Down": {reflect.TypeOf(q.Down), constant.MakeInt64(int64(q.Down))}, "Edge": {reflect.TypeOf(q.Edge), constant.MakeInt64(int64(q.Edge))}, "EdgeBottom": {reflect.TypeOf(q.EdgeBottom), constant.MakeInt64(int64(q.EdgeBottom))}, "EdgeLeft": {reflect.TypeOf(q.EdgeLeft), constant.MakeInt64(int64(q.EdgeLeft))}, "EdgeRight": {reflect.TypeOf(q.EdgeRight), constant.MakeInt64(int64(q.EdgeRight))}, "EdgeTop": {reflect.TypeOf(q.EdgeTop), constant.MakeInt64(int64(q.EdgeTop))}, "Key0": {reflect.TypeOf(q.Key0), constant.MakeInt64(int64(q.Key0))}, "Key1": {reflect.TypeOf(q.Key1), constant.MakeInt64(int64(q.Key1))}, "Key2": {reflect.TypeOf(q.Key2), constant.MakeInt64(int64(q.Key2))}, "Key3": {reflect.TypeOf(q.Key3), constant.MakeInt64(int64(q.Key3))}, "Key4": {reflect.TypeOf(q.Key4), constant.MakeInt64(int64(q.Key4))}, "Key5": {reflect.TypeOf(q.Key5), constant.MakeInt64(int64(q.Key5))}, "Key6": {reflect.TypeOf(q.Key6), constant.MakeInt64(int64(q.Key6))}, "Key7": {reflect.TypeOf(q.Key7), constant.MakeInt64(int64(q.Key7))}, "Key8": {reflect.TypeOf(q.Key8), constant.MakeInt64(int64(q.Key8))}, "Key9": {reflect.TypeOf(q.Key9), constant.MakeInt64(int64(q.Key9))}, "KeyA": {reflect.TypeOf(q.KeyA), constant.MakeInt64(int64(q.KeyA))}, "KeyAlt": {reflect.TypeOf(q.KeyAlt), constant.MakeInt64(int64(q.KeyAlt))}, "KeyAny": {reflect.TypeOf(q.KeyAny), constant.MakeInt64(int64(q.KeyAny))}, "KeyApostrophe": {reflect.TypeOf(q.KeyApostrophe), constant.MakeInt64(int64(q.KeyApostrophe))}, "KeyB": {reflect.TypeOf(q.KeyB), constant.MakeInt64(int64(q.KeyB))}, "KeyBackslash": {reflect.TypeOf(q.KeyBackslash), constant.MakeInt64(int64(q.KeyBackslash))}, "KeyBackspace": {reflect.TypeOf(q.KeyBackspace), constant.MakeInt64(int64(q.KeyBackspace))}, "KeyC": {reflect.TypeOf(q.KeyC), constant.MakeInt64(int64(q.KeyC))}, "KeyCapsLock": {reflect.TypeOf(q.KeyCapsLock), constant.MakeInt64(int64(q.KeyCapsLock))}, "KeyComma": {reflect.TypeOf(q.KeyComma), constant.MakeInt64(int64(q.KeyComma))}, "KeyControl": {reflect.TypeOf(q.KeyControl), constant.MakeInt64(int64(q.KeyControl))}, "KeyD": {reflect.TypeOf(q.KeyD), constant.MakeInt64(int64(q.KeyD))}, "KeyDelete": {reflect.TypeOf(q.KeyDelete), constant.MakeInt64(int64(q.KeyDelete))}, "KeyDown": {reflect.TypeOf(q.KeyDown), constant.MakeInt64(int64(q.KeyDown))}, "KeyE": {reflect.TypeOf(q.KeyE), constant.MakeInt64(int64(q.KeyE))}, "KeyEnd": {reflect.TypeOf(q.KeyEnd), constant.MakeInt64(int64(q.KeyEnd))}, "KeyEnter": {reflect.TypeOf(q.KeyEnter), constant.MakeInt64(int64(q.KeyEnter))}, "KeyEqual": {reflect.TypeOf(q.KeyEqual), constant.MakeInt64(int64(q.KeyEqual))}, "KeyEscape": {reflect.TypeOf(q.KeyEscape), constant.MakeInt64(int64(q.KeyEscape))}, "KeyF": {reflect.TypeOf(q.KeyF), constant.MakeInt64(int64(q.KeyF))}, "KeyF1": {reflect.TypeOf(q.KeyF1), constant.MakeInt64(int64(q.KeyF1))}, "KeyF10": {reflect.TypeOf(q.KeyF10), constant.MakeInt64(int64(q.KeyF10))}, "KeyF11": {reflect.TypeOf(q.KeyF11), constant.MakeInt64(int64(q.KeyF11))}, "KeyF12": {reflect.TypeOf(q.KeyF12), constant.MakeInt64(int64(q.KeyF12))}, "KeyF2": {reflect.TypeOf(q.KeyF2), constant.MakeInt64(int64(q.KeyF2))}, "KeyF3": {reflect.TypeOf(q.KeyF3), constant.MakeInt64(int64(q.KeyF3))}, "KeyF4": {reflect.TypeOf(q.KeyF4), constant.MakeInt64(int64(q.KeyF4))}, "KeyF5": {reflect.TypeOf(q.KeyF5), constant.MakeInt64(int64(q.KeyF5))}, "KeyF6": {reflect.TypeOf(q.KeyF6), constant.MakeInt64(int64(q.KeyF6))}, "KeyF7": {reflect.TypeOf(q.KeyF7), constant.MakeInt64(int64(q.KeyF7))}, "KeyF8": {reflect.TypeOf(q.KeyF8), constant.MakeInt64(int64(q.KeyF8))}, "KeyF9": {reflect.TypeOf(q.KeyF9), constant.MakeInt64(int64(q.KeyF9))}, "KeyG": {reflect.TypeOf(q.KeyG), constant.MakeInt64(int64(q.KeyG))}, "KeyGraveAccent": {reflect.TypeOf(q.KeyGraveAccent), constant.MakeInt64(int64(q.KeyGraveAccent))}, "KeyH": {reflect.TypeOf(q.KeyH), constant.MakeInt64(int64(q.KeyH))}, "KeyHome": {reflect.TypeOf(q.KeyHome), constant.MakeInt64(int64(q.KeyHome))}, "KeyI": {reflect.TypeOf(q.KeyI), constant.MakeInt64(int64(q.KeyI))}, "KeyInsert": {reflect.TypeOf(q.KeyInsert), constant.MakeInt64(int64(q.KeyInsert))}, "KeyJ": {reflect.TypeOf(q.KeyJ), constant.MakeInt64(int64(q.KeyJ))}, "KeyK": {reflect.TypeOf(q.KeyK), constant.MakeInt64(int64(q.KeyK))}, "KeyKP0": {reflect.TypeOf(q.KeyKP0), constant.MakeInt64(int64(q.KeyKP0))}, "KeyKP1": {reflect.TypeOf(q.KeyKP1), constant.MakeInt64(int64(q.KeyKP1))}, "KeyKP2": {reflect.TypeOf(q.KeyKP2), constant.MakeInt64(int64(q.KeyKP2))}, "KeyKP3": {reflect.TypeOf(q.KeyKP3), constant.MakeInt64(int64(q.KeyKP3))}, "KeyKP4": {reflect.TypeOf(q.KeyKP4), constant.MakeInt64(int64(q.KeyKP4))}, "KeyKP5": {reflect.TypeOf(q.KeyKP5), constant.MakeInt64(int64(q.KeyKP5))}, "KeyKP6": {reflect.TypeOf(q.KeyKP6), constant.MakeInt64(int64(q.KeyKP6))}, "KeyKP7": {reflect.TypeOf(q.KeyKP7), constant.MakeInt64(int64(q.KeyKP7))}, "KeyKP8": {reflect.TypeOf(q.KeyKP8), constant.MakeInt64(int64(q.KeyKP8))}, "KeyKP9": {reflect.TypeOf(q.KeyKP9), constant.MakeInt64(int64(q.KeyKP9))}, "KeyKPDecimal": {reflect.TypeOf(q.KeyKPDecimal), constant.MakeInt64(int64(q.KeyKPDecimal))}, "KeyKPDivide": {reflect.TypeOf(q.KeyKPDivide), constant.MakeInt64(int64(q.KeyKPDivide))}, "KeyKPEnter": {reflect.TypeOf(q.KeyKPEnter), constant.MakeInt64(int64(q.KeyKPEnter))}, "KeyKPEqual": {reflect.TypeOf(q.KeyKPEqual), constant.MakeInt64(int64(q.KeyKPEqual))}, "KeyKPMultiply": {reflect.TypeOf(q.KeyKPMultiply), constant.MakeInt64(int64(q.KeyKPMultiply))}, "KeyKPSubtract": {reflect.TypeOf(q.KeyKPSubtract), constant.MakeInt64(int64(q.KeyKPSubtract))}, "KeyL": {reflect.TypeOf(q.KeyL), constant.MakeInt64(int64(q.KeyL))}, "KeyLeft": {reflect.TypeOf(q.KeyLeft), constant.MakeInt64(int64(q.KeyLeft))}, "KeyLeftBracket": {reflect.TypeOf(q.KeyLeftBracket), constant.MakeInt64(int64(q.KeyLeftBracket))}, "KeyM": {reflect.TypeOf(q.KeyM), constant.MakeInt64(int64(q.KeyM))}, "KeyMax": {reflect.TypeOf(q.KeyMax), constant.MakeInt64(int64(q.KeyMax))}, "KeyMenu": {reflect.TypeOf(q.KeyMenu), constant.MakeInt64(int64(q.KeyMenu))}, "KeyMinus": {reflect.TypeOf(q.KeyMinus), constant.MakeInt64(int64(q.KeyMinus))}, "KeyN": {reflect.TypeOf(q.KeyN), constant.MakeInt64(int64(q.KeyN))}, "KeyNumLock": {reflect.TypeOf(q.KeyNumLock), constant.MakeInt64(int64(q.KeyNumLock))}, "KeyO": {reflect.TypeOf(q.KeyO), constant.MakeInt64(int64(q.KeyO))}, "KeyP": {reflect.TypeOf(q.KeyP), constant.MakeInt64(int64(q.KeyP))}, "KeyPageDown": {reflect.TypeOf(q.KeyPageDown), constant.MakeInt64(int64(q.KeyPageDown))}, "KeyPageUp": {reflect.TypeOf(q.KeyPageUp), constant.MakeInt64(int64(q.KeyPageUp))}, "KeyPause": {reflect.TypeOf(q.KeyPause), constant.MakeInt64(int64(q.KeyPause))}, "KeyPeriod": {reflect.TypeOf(q.KeyPeriod), constant.MakeInt64(int64(q.KeyPeriod))}, "KeyPrintScreen": {reflect.TypeOf(q.KeyPrintScreen), constant.MakeInt64(int64(q.KeyPrintScreen))}, "KeyQ": {reflect.TypeOf(q.KeyQ), constant.MakeInt64(int64(q.KeyQ))}, "KeyR": {reflect.TypeOf(q.KeyR), constant.MakeInt64(int64(q.KeyR))}, "KeyRight": {reflect.TypeOf(q.KeyRight), constant.MakeInt64(int64(q.KeyRight))}, "KeyRightBracket": {reflect.TypeOf(q.KeyRightBracket), constant.MakeInt64(int64(q.KeyRightBracket))}, "KeyS": {reflect.TypeOf(q.KeyS), constant.MakeInt64(int64(q.KeyS))}, "KeyScrollLock": {reflect.TypeOf(q.KeyScrollLock), constant.MakeInt64(int64(q.KeyScrollLock))}, "KeySemicolon": {reflect.TypeOf(q.KeySemicolon), constant.MakeInt64(int64(q.KeySemicolon))}, "KeyShift": {reflect.TypeOf(q.KeyShift), constant.MakeInt64(int64(q.KeyShift))}, "KeySlash": {reflect.TypeOf(q.KeySlash), constant.MakeInt64(int64(q.KeySlash))}, "KeySpace": {reflect.TypeOf(q.KeySpace), constant.MakeInt64(int64(q.KeySpace))}, "KeyT": {reflect.TypeOf(q.KeyT), constant.MakeInt64(int64(q.KeyT))}, "KeyTab": {reflect.TypeOf(q.KeyTab), constant.MakeInt64(int64(q.KeyTab))}, "KeyU": {reflect.TypeOf(q.KeyU), constant.MakeInt64(int64(q.KeyU))}, "KeyUp": {reflect.TypeOf(q.KeyUp), constant.MakeInt64(int64(q.KeyUp))}, "KeyV": {reflect.TypeOf(q.KeyV), constant.MakeInt64(int64(q.KeyV))}, "KeyW": {reflect.TypeOf(q.KeyW), constant.MakeInt64(int64(q.KeyW))}, "KeyX": {reflect.TypeOf(q.KeyX), constant.MakeInt64(int64(q.KeyX))}, "KeyY": {reflect.TypeOf(q.KeyY), constant.MakeInt64(int64(q.KeyY))}, "KeyZ": {reflect.TypeOf(q.KeyZ), constant.MakeInt64(int64(q.KeyZ))}, "Left": {reflect.TypeOf(q.Left), constant.MakeInt64(int64(q.Left))}, "Mouse": {reflect.TypeOf(q.Mouse), constant.MakeInt64(int64(q.Mouse))}, "Next": {reflect.TypeOf(q.Next), constant.MakeInt64(int64(q.Next))}, "OtherScriptsInSprite": {reflect.TypeOf(q.OtherScriptsInSprite), constant.MakeInt64(int64(q.OtherScriptsInSprite))}, "Prev": {reflect.TypeOf(q.Prev), constant.MakeInt64(int64(q.Prev))}, "Right": {reflect.TypeOf(q.Right), constant.MakeInt64(int64(q.Right))}, "ThisScript": {reflect.TypeOf(q.ThisScript), constant.MakeInt64(int64(q.ThisScript))}, "ThisSprite": {reflect.TypeOf(q.ThisSprite), constant.MakeInt64(int64(q.ThisSprite))}, "Up": {reflect.TypeOf(q.Up), constant.MakeInt64(int64(q.Up))}, }, UntypedConsts: map[string]gossa.UntypedConst{ "All": {"untyped int", constant.MakeInt64(int64(q.All))}, "DbgFlagAll": {"untyped int", constant.MakeInt64(int64(q.DbgFlagAll))}, "DbgFlagEvent": {"untyped int", constant.MakeInt64(int64(q.DbgFlagEvent))}, "DbgFlagInstr": {"untyped int", constant.MakeInt64(int64(q.DbgFlagInstr))}, "DbgFlagLoad": {"untyped int", constant.MakeInt64(int64(q.DbgFlagLoad))}, "GopPackage": {"untyped bool", constant.MakeBool(bool(q.GopPackage))}, "Gop_sched": {"untyped string", constant.MakeString(string(q.Gop_sched))}, "Invalid": {"untyped int", constant.MakeInt64(int64(q.Invalid))}, "Last": {"untyped int", constant.MakeInt64(int64(q.Last))}, "LeftRight": {"untyped int", constant.MakeInt64(int64(q.LeftRight))}, "None": {"untyped int", constant.MakeInt64(int64(q.None))}, "Normal": {"untyped int", constant.MakeInt64(int64(q.Normal))}, "Random": {"untyped int", constant.MakeInt64(int64(q.Random))}, }, })}

上述代码中的 RegisterPackage 只是注册并没有实际加载,实际加载是在 Go 源码生成  ssa.Package 阶段,只有需要的时候才进行加载。

Deps 是指包的依赖,包是有加载顺序的,通过 Deps 实现对加载顺序的支持;NamedTypes 对应的是方法集,它和 reflect.Type 的区别在于其包含被导出的函数名称 (包含小写的未导出函数),主要是为了实现隐含接口转换的支持,如果直接用 reflect.Type 则无法实现。

4. ispx 的 methods 设置

如果注释掉 import _ "github.com/goplus/reflectx/icall/icall8192" 这一行,在 ispx 运行时会出现失败,显示类似下面的代码,表示预分配方法集 methods 不够。

cannot alloc method 3522 > 256, import _ "github.com/goplus/reflectx/icall/icall[2^n]"fatal error: runtime: text offset base pointer out of ranges

我们可以使用 reflectx 预置的分配表

github.com/goplus/reflectx/icall/icall[2^n] 1024 2048 4096 。。。65536

我们也可使用 icall_gen 程序自动生成需要的分配表

$ go get github.com/goplus/reflectx/cmd/icall_gen

示例:生成 20000 个预分配表

//go:generate icall_gen -o icall.go -pkg main -size 20000

5. gossa 实现原理

- golang.org/x/tools/go/ssa 实现 types.Package -> ssa.Package 转换

Package ssa defines a representation of the elements of Go programs (packages, types, functions, variables and constants) using a static single-assignment (SSA) form intermediate representation (IR) for the bodies of functions.

pkg.go.dev/golang.org/x/tools/go/ssa

- reflectx package ( Go runtime 内存布局兼容)

github.com/goplus/reflectx

github.com/goplusjs/reflectx

其中,goplusjs/reflectx 是提供给 GopherJS 使用,内部用 js 实现了方法集动态分配调用,不需要 icall methods 分配表。 

6. gossa 执行流程

1)预注册导入包 imports -> reflect package -> register pkg

import _ "github.com/goplus/gossa/pkg/fmt"

2)Go+ Source -> Go Source

import _ "github.com/goplus/gopbuild"

3)Go Source -> ast.Package

4)ast.Package -> types.Package

  • 查找和安装导入包 reflect package -> types.Package

  • 直接依赖包/间接依赖包 ( qexp 生成的导入包 )

5)types.Package -> ssa.Program / ssa.Package

6)gossa.Interp 加载 ssa.Program / ssa.Package

7. gossa.Interp 执行

- 类型转换

ssa.Type -> reflect.Type

ssa.Value -> gossa.value/interface{}

- 指令执行

ssa.Instruction

包含 Function / Block / Var / BinOp 。。。

- Go runtime 内存布局兼容 methods

reflectx NamedTypeOf / InterfaceOf / SetMethodSet / ... ...

 四. gossa 改进空间,已知问题和解决方案

- typed methods 数量限制,预分配 256 个

import _ "github.com/reflectx/icall/icall[2^n]"

使用 reflectx/cmd/icall_gen 生成需要的 methods

- 多源码包加载

需要 gossa 自行实现 go module 加载机制。

- Go1.17 引入的 regabi 寄存器调用规范

Go1.17 amd64 需要设置 GOEXPERIMENT=noregabi 编译。

GOEXPERIMENT=noregabi go build demo

- Go1.18 amd64 默认使用 regabi 目前无法运行

reflectx 需要使用 ASM 编写 regabi 兼容 method provider

五. Go+ Playground

  • go.dev/play

  • play.goplus.org

  • jsplay.goplus.org(不借助服务器)

1. jsplay.goplus.org 实现

  • 源码:https://github.com/goplusjs/play

module github.com/goplusjs/play
go 1.16
require ( github.com/goplus/gop v1.0.33 github.com/goplus/gossa v0.2.6 github.com/goplus/gox v1.8.1 github.com/goplus/reflectx v0.6.8 github.com/goplusjs/gopherjs v1.2.5 golang.org/x/tools v0.1.8)
replace github.com/goplus/reflectx => github.com/goplusjs/reflectx v0.5.6

2. jsplay.goplus.org 引用库

  • github.com/goplus/gop

  • github.com/goplus/gossa

  • github.com/goplus/reflectx

  • github.com/goplusjs/reflectx

  • github.com/goplusjs/gopherjs