如何在运行时发现所有的软件包类型?

huangapple go评论71阅读模式
英文:

How to discover all package types at runtime?

问题

据我所知(参见这里这里),在reflect包中没有类型发现机制,它期望你已经有了要检查的类型或值的实例。

是否有其他方法可以发现运行中的Go包中的所有导出类型(特别是结构体)?

以下是我希望有的(但实际上不存在的)功能:

import "time"
import "fmt"

func main() {
    var types []reflect.Type
    types = reflect.DiscoverTypes(time)
    fmt.Println(types)
}

最终目标是能够发现满足特定条件的包中的所有结构体,然后能够实例化这些结构体的新实例。

顺便说一句,对于我的用例来说,识别类型的注册函数不是一个有效的方法。


无论你认为这是一个好主意还是不是,以下是我想要这个功能的原因(因为我知道你会问):

我编写了一个代码生成工具,它加载Go源文件并构建AST以扫描嵌入指定类型的类型。该工具的输出是一组基于发现的类型的Go测试函数。我使用go generate来调用该工具以创建测试函数,然后运行go test来执行生成的测试函数。每当测试发生变化(或添加新类型)时,我必须重新运行go generate,然后再运行go test。这就是为什么注册函数不是一个有效的选项。我想避免go generate步骤,但这就需要我的工具成为一个被运行包导入的库。库代码需要在init()期间以某种方式扫描运行命名空间,以查找嵌入了预期库类型的类型。

英文:

As far as I'm aware (see here, and here) there is no type discovery mechanism in the reflect package, which expects that you already have an instance of the type or value you want to inspect.

Is there any other way to discover all exported types (especially the structs) in a running go package?

Here's what I wish I had (but it doesn't exist):

import "time"
import "fmt"

func main() {
    var types []reflect.Type
    types = reflect.DiscoverTypes(time)
    fmt.Println(types)
}

The end goal is to be able to discover all the structs of a package that meet certain criteria, then be able to instantiate new instances of those structs.

BTW, a registration function that identifies the types is not a valid approach for my use case.


Whether you think it's a good idea or not, here's why I want this capability (because I know you're going to ask):

I've written a code generation utility that loads go source files and builds an AST to scan for types that embed a specified type. The output of the utility is a set of go test functions based on the discovered types. I invoke this utility using go generate to create the test functions then run go test to execute the generated test functions. Every time the tests change (or a new type is added) I must re-run go generate before re-running go test. This is why a registration function is not a valid option. I'd like to avoid the go generate step but that would require my utility to become a library that is imported by the running package. The library code would need to somehow scan the running namespace during init() for types that embed the expected library type.

答案1

得分: 32

在Go 1.5中,你可以使用新的包typesimporter来检查二进制和源代码包。例如:

package main

import (
	"fmt"
	"go/importer"
)

func main() {
	pkg, err := importer.Default().Import("time")
	if err != nil {
		fmt.Printf("error: %s\n", err.Error())
		return
	}
	for _, declName := range pkg.Scope().Names() {
		fmt.Println(declName)
	}
}

你可以使用包go/build来提取所有已安装的包。或者你可以配置Lookup导入器来检查环境之外的二进制文件。

在1.5之前,唯一没有hack的方法是使用包ast来编译源代码。

英文:

In Go 1.5, you can use the new package types and importer to inspect binary and source packages. For example:

package main

import (
	"fmt"
	"go/importer"
)

func main() {
	pkg, err := importer.Default().Import("time")
	if err != nil {
		fmt.Printf("error: %s\n", err.Error())
		return
	}
	for _, declName := range pkg.Scope().Names() {
		fmt.Println(declName)
	}
}

You can use the package go/build to extract all the packages installed. Or you can configure the Lookup importer to inspect binaries outside the environment.

Before 1.5, the only no-hacky way is to use the package ast to compile the source code.

答案2

得分: 19

警告:未经测试且有些破解。可能会在发布新版本的Go时出现问题。

通过对Go的运行时进行一些破解,可以获取运行时所知道的所有类型。在你自己的包中包含一个小的汇编文件,内容如下:

TEXT yourpackage·typelinks(SB), NOSPLIT, $0-0
    JMP	reflect·typelinks(SB)

yourpackage中,声明函数原型(不包含函数体):

func typelinks() []*typeDefDummy

以及类型定义:

type typeDefDummy struct {
    _      uintptr           // 填充
    _      uint64            // 填充
    _      [3]uintptr        // 填充
    StrPtr *string           
}

然后只需调用typelinks,遍历切片并读取每个StrPtr的名称。寻找以yourpackage开头的名称。请注意,如果在不同路径下存在两个名为yourpackage的包,此方法将无法明确地工作。

我是否可以以某种方式连接到reflect包,实例化这些名称的新实例?

是的,假设d是类型为*typeDefDummy的值(注意星号,非常重要):

t := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&d)))

现在,t是一个reflect.Type值,您可以使用它来实例化reflect.Value


编辑:我已经成功测试并执行了此代码,并将其上传为一个gist

根据需要调整包名称和包含路径。

2019年更新

自我最初发布这个答案以来,发生了很多变化。以下是如何在2019年使用Go 1.11执行相同操作的简短描述。

$GOPATH/src/tl/tl.go

package tl

import (
    "unsafe"
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
    return typelinks()
}

func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return add(p, x, whySafe)
}

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

$GOPATH/src/tl/tl.s

TEXT tl·typelinks(SB), $0-0
    JMP reflect·typelinks(SB)

TEXT tl·add(SB), $0-0
    JMP reflect·add(SB)

main.go

package main

import (
    "fmt"
    "reflect"
    "tl"
    "unsafe"
)

func main() {
    sections, offsets := tl.Typelinks()
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := tl.Add(base, uintptr(offset), "")
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
            fmt.Println(typ)
        }
    }
}

愉快的编程!

英文:

(see bottom for 2019 update)

Warning: untested and hacky. Can break whenever a new version of Go is released.

It is possible to get all types the runtime knows of by hacking around Go's runtime a little. Include a small assembly file in your own package, containing:

TEXT yourpackage·typelinks(SB), NOSPLIT, $0-0
    JMP	reflect·typelinks(SB)

In yourpackage, declare the function prototype (without body):

func typelinks() []*typeDefDummy

Alongside a type definition:

type typeDefDummy struct {
    _      uintptr           // padding
    _      uint64            // padding
    _      [3]uintptr        // padding
    StrPtr *string           
}

Then just call typelinks, iterate over the slice and read each StrPtr for the name. Seek those starting with yourpackage. Note that if there are two packages called yourpackage in different paths, this method won't work unambiguously.

> can I somehow hook into the reflect package to instantiate new instances of those names?

Yeah, assuming d is a value of type *typeDefDummy (note the asterisk, very important):

t := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&d)))

Now t is a reflect.Type value which you can use to instantiate reflect.Values.

<hr>

Edit: I tested and executed this code successfully and have uploaded it as a gist.

Adjust package names and include paths as necessary.

Update 2019

A lot has changed since I originally posted this answer. Here's a short description of how the same can be done with Go 1.11 in 2019.

$GOPATH/src/tl/tl.go

package tl

import (
	&quot;unsafe&quot;
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
	return typelinks()
}

func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
	return add(p, x, whySafe)
}

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

$GOPATH/src/tl/tl.s

TEXT tl&#183;typelinks(SB), $0-0
    JMP reflect&#183;typelinks(SB)

TEXT tl&#183;add(SB), $0-0
    JMP reflect&#183;add(SB)

main.go

package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
	&quot;tl&quot;
	&quot;unsafe&quot;
)

func main() {
	sections, offsets := tl.Typelinks()
	for i, base := range sections {
		for _, offset := range offsets[i] {
			typeAddr := tl.Add(base, uintptr(offset), &quot;&quot;)
			typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&amp;typeAddr)))
			fmt.Println(typ)
		}
	}
}

Happy hacking!

答案3

得分: 3

使用 Go 1.18 更新

在 Go 1.18 中,接受的答案 不再适用,但我可以适应使用 go:linkname。使用这个指令和 unsafe 包,这些内部函数现在可以在没有额外汇编代码的情况下访问。

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

func main() {
	sections, offsets := typelinks()
	for i, base := range sections {
		for _, offset := range offsets[i] {
			typeAddr := add(base, uintptr(offset), "")
			typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
			fmt.Println(typ)
		}
	}
}
英文:

Update 2022 with Go 1.18

With Go 1.18 the accepted answer doesn't work anymore, but I could adapt it to use go:linkname. Using this directive and the unsafe package these internal functions can now be accessed without any extra assembly code.

package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
	&quot;unsafe&quot;
)

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

func main() {
	sections, offsets := typelinks()
	for i, base := range sections {
		for _, offset := range offsets[i] {
			typeAddr := add(base, uintptr(offset), &quot;&quot;)
			typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&amp;typeAddr)))
			fmt.Println(typ)
		}
	}
}

答案4

得分: 2

很抱歉,我不认为这是可能的。在Go语言中,包是不可“执行”的,你不能在其上“调用函数”。你也不能在类型上调用函数,但你可以在类型的实例上调用reflect.TypeOf,并获得reflect.Type,它是类型的运行时抽象。对于包来说,就没有这样的机制,也没有reflect.Package

话虽如此,你可以提交一个问题,关于缺少reflect.PackageOf等功能的问题以及是否有实际添加的可行性。

英文:

Unfortunately, I don't think this is possible. Packages are not "actionable" in Go, you can't "call a function" on it. You can't call a function on a type either, but you can call reflect.TypeOf on an instance of the type and get reflect.Type which is a runtime abstraction of a type. There just isn't such mechanism for packages, there isn't a reflect.Package.

With that said, you could file an issue about the absence of (and practicality of adding) reflect.PackageOf etc.

答案5

得分: 2

感谢@thwd和@icio的指导,按照你们的方法,今天在1.13.6上仍然有效。

按照你们的方法,tl.s将会是:

TEXT ·typelinks(SB), $0-0
    JMP reflect·typelinks(SB)

是的,没有包名,也没有"add"函数。

然后按照@icio的方法,将"add"函数改为:

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return unsafe.Pointer(uintptr(p) + x)
}

现在一切都正常工作了。 如何在运行时发现所有的软件包类型?

英文:

Thanks @thwd and @icio, follow your direction it still worked on 1.13.6 today.

Follow your way the tl.s will be:

TEXT &#183;typelinks(SB), $0-0
    JMP reflect&#183;typelinks(SB)

yes, no package name and no "add" function in it.

then follow @icio's way change "add" function to:

func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return unsafe.Pointer(uintptr(p) + x)
}

then all worked now. 如何在运行时发现所有的软件包类型?

答案6

得分: 1

适用于go 1.16版本(已在go版本go1.16.7 linux/amd64上进行了测试)

  • 这只能生成代码和字符串。您需要将生成的代码粘贴到某个地方,然后再次编译它。
  • 仅在源代码可用时有效。
import (
  "fmt"
  "go/ast"
  "golang.org/x/tools/go/packages"
  "reflect"
  "time"
  "unicode"
)

func printTypes(){
  config := &packages.Config{
    Mode:  packages.NeedSyntax,
  }
  pkgs, _ := packages.Load(config, "package_name")
  pkg := pkgs[0]

  for _, s := range pkg.Syntax {
    for n, o := range s.Scope.Objects {
      if o.Kind == ast.Typ {
        // 检查类型是否是导出的(仅适用于非局部类型)
        if unicode.IsUpper([]rune(n)[0]) {
          // 请注意,reflect.ValueOf(*new(%s))在接口上不起作用
          fmt.Printf("ProcessType(new(package_name.%s)),\n", n)
        }
      }
    }
  }
}

可能用例的完整示例:https://pastebin.com/ut0zNEAc(在线repl不起作用,但在本地起作用)

英文:

Version for go 1.16(tested for go version go1.16.7 linux/amd64)

  • This can only generate code and strings. You have to paste generated code somewhere then compile it again
  • Works if only sources are available.
import (
  &quot;fmt&quot;
  &quot;go/ast&quot;
  &quot;golang.org/x/tools/go/packages&quot;
  &quot;reflect&quot;
  &quot;time&quot;
  &quot;unicode&quot;
)

func printTypes(){
  config := &amp;packages.Config{
    Mode:  packages.NeedSyntax,
  }
  pkgs, _ := packages.Load(config, &quot;package_name&quot;)
  pkg := pkgs[0]

  for _, s := range pkg.Syntax {
    for n, o := range s.Scope.Objects {
      if o.Kind == ast.Typ {
        // check if type is exported(only need for non-local types)
        if unicode.IsUpper([]rune(n)[0]) {
          // note that reflect.ValueOf(*new(%s)) won&#39;t work with interfaces
          fmt.Printf(&quot;ProcessType(new(package_name.%s)),\n&quot;, n)
        }
      }
    }
  }
}

full example of possible use case: https://pastebin.com/ut0zNEAc (doesn't work in online repls, but works locally)

答案7

得分: 0

在Go 1.11之后,通过添加运行时类型信息,可以使用地址DW_AT_go_runtime_type来获取运行时类型。你可以在gort中查看更多内容。

package main

import (
	"debug/dwarf"
	"fmt"
	"log"
	"os"
	"reflect"
	"runtime"
	"unsafe"

	"github.com/go-delve/delve/pkg/dwarf/godwarf"
	"github.com/go-delve/delve/pkg/proc"
)

func main() {
	path, err := os.Executable()
	if err != nil {
		log.Fatalln(err)
	}

	bi := proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
	err = bi.LoadBinaryInfo(path, 0, nil)
	if err != nil {
		log.Fatalln(err)
	}
	mds, err := loadModuleData(bi, new(localMemory))
	if err != nil {
		log.Fatalln(err)
	}

	types, err := bi.Types()
	if err != nil {
		log.Fatalln(err)
	}

	for _, name := range types {
		dwarfType, err := findType(bi, name)
		if err != nil {
			continue
		}

		typeAddr, err := dwarfToRuntimeType(bi, mds, dwarfType, name)
		if err != nil {
			continue
		}

		typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
		log.Printf("load type name:%s type:%s\n", name, typ)
	}
}

// delve counterpart to runtime.moduledata
type moduleData struct {
	text, etext   uint64
	types, etypes uint64
	typemapVar    *proc.Variable
}

//go:linkname findType github.com/go-delve/delve/pkg/proc.(*BinaryInfo).findType
func findType(bi *proc.BinaryInfo, name string) (godwarf.Type, error)

//go:linkname loadModuleData github.com/go-delve/delve/pkg/proc.loadModuleData
func loadModuleData(bi *proc.BinaryInfo, mem proc.MemoryReadWriter) ([]moduleData, error)

//go:linkname imageToModuleData github.com/go-delve/delve/pkg/proc.(*BinaryInfo).imageToModuleData
func imageToModuleData(bi *proc.BinaryInfo, image *proc.Image, mds []moduleData) *moduleData

type localMemory int

func (mem *localMemory) ReadMemory(data []byte, addr uint64) (int, error) {
	buf := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: uintptr(addr), Len: len(data), Cap: len(data)}))
	copy(data, buf)
	return len(data), nil
}

func (mem *localMemory) WriteMemory(addr uint64, data []byte) (int, error) {
	return 0, fmt.Errorf("not support")
}

func dwarfToRuntimeType(bi *proc.BinaryInfo, mds []moduleData, typ godwarf.Type, name string) (typeAddr uint64, err error) {
	if typ.Common().Index >= len(bi.Images) {
		return 0, fmt.Errorf("could not find image for type %s", name)
	}
	img := bi.Images[typ.Common().Index]
	rdr := img.DwarfReader()
	rdr.Seek(typ.Common().Offset)
	e, err := rdr.Next()
	if err != nil {
		return 0, fmt.Errorf("could not find dwarf entry for type:%s err:%s", name, err)
	}
	entryName, ok := e.Val(dwarf.AttrName).(string)
	if !ok || entryName != name {
		return 0, fmt.Errorf("could not find name for type:%s entry:%s", name, entryName)
	}
	off, ok := e.Val(godwarf.AttrGoRuntimeType).(uint64)
	if !ok || off == 0 {
		return 0, fmt.Errorf("could not find runtime type for type:%s", name)
	}

	md := imageToModuleData(bi, img, mds)
	if md == nil {
		return 0, fmt.Errorf("could not find module data for type %s", name)
	}

	typeAddr = md.types + off
	if typeAddr < md.types || typeAddr >= md.etypes {
		return off, nil
	}
	return typeAddr, nil
}
英文:
  • After go 1.11 dwarf debugging symbols added runtime type information, you can get the runtime type by using this address
  • DW_AT_go_runtime_type
  • gort You can see more content
package main

import (
	&quot;debug/dwarf&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;os&quot;
	&quot;reflect&quot;
	&quot;runtime&quot;
	&quot;unsafe&quot;

	&quot;github.com/go-delve/delve/pkg/dwarf/godwarf&quot;
	&quot;github.com/go-delve/delve/pkg/proc&quot;
)

func main() {
	path, err := os.Executable()
	if err != nil {
		log.Fatalln(err)
	}

	bi := proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
	err = bi.LoadBinaryInfo(path, 0, nil)
	if err != nil {
		log.Fatalln(err)
	}
	mds, err := loadModuleData(bi, new(localMemory))
	if err != nil {
		log.Fatalln(err)
	}

	types, err := bi.Types()
	if err != nil {
		log.Fatalln(err)
	}

	for _, name := range types {
		dwarfType, err := findType(bi, name)
		if err != nil {
			continue
		}

		typeAddr, err := dwarfToRuntimeType(bi, mds, dwarfType, name)
		if err != nil {
			continue
		}

		typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&amp;typeAddr)))
		log.Printf(&quot;load type name:%s type:%s\n&quot;, name, typ)
	}
}

// delve counterpart to runtime.moduledata
type moduleData struct {
	text, etext   uint64
	types, etypes uint64
	typemapVar    *proc.Variable
}

//go:linkname findType github.com/go-delve/delve/pkg/proc.(*BinaryInfo).findType
func findType(bi *proc.BinaryInfo, name string) (godwarf.Type, error)

//go:linkname loadModuleData github.com/go-delve/delve/pkg/proc.loadModuleData
func loadModuleData(bi *proc.BinaryInfo, mem proc.MemoryReadWriter) ([]moduleData, error)

//go:linkname imageToModuleData github.com/go-delve/delve/pkg/proc.(*BinaryInfo).imageToModuleData
func imageToModuleData(bi *proc.BinaryInfo, image *proc.Image, mds []moduleData) *moduleData

type localMemory int

func (mem *localMemory) ReadMemory(data []byte, addr uint64) (int, error) {
	buf := *(*[]byte)(unsafe.Pointer(&amp;reflect.SliceHeader{Data: uintptr(addr), Len: len(data), Cap: len(data)}))
	copy(data, buf)
	return len(data), nil
}

func (mem *localMemory) WriteMemory(addr uint64, data []byte) (int, error) {
	return 0, fmt.Errorf(&quot;not support&quot;)
}

func dwarfToRuntimeType(bi *proc.BinaryInfo, mds []moduleData, typ godwarf.Type, name string) (typeAddr uint64, err error) {
	if typ.Common().Index &gt;= len(bi.Images) {
		return 0, fmt.Errorf(&quot;could not find image for type %s&quot;, name)
	}
	img := bi.Images[typ.Common().Index]
	rdr := img.DwarfReader()
	rdr.Seek(typ.Common().Offset)
	e, err := rdr.Next()
	if err != nil {
		return 0, fmt.Errorf(&quot;could not find dwarf entry for type:%s err:%s&quot;, name, err)
	}
	entryName, ok := e.Val(dwarf.AttrName).(string)
	if !ok || entryName != name {
		return 0, fmt.Errorf(&quot;could not find name for type:%s entry:%s&quot;, name, entryName)
	}
	off, ok := e.Val(godwarf.AttrGoRuntimeType).(uint64)
	if !ok || off == 0 {
		return 0, fmt.Errorf(&quot;could not find runtime type for type:%s&quot;, name)
	}

	md := imageToModuleData(bi, img, mds)
	if md == nil {
		return 0, fmt.Errorf(&quot;could not find module data for type %s&quot;, name)
	}

	typeAddr = md.types + off
	if typeAddr &lt; md.types || typeAddr &gt;= md.etypes {
		return off, nil
	}
	return typeAddr, nil
}

答案8

得分: -2

不,没有。

如果你想要“知道”你的类型,你需要注册它们。

英文:

No there is not.

If you want to 'know' your types you'll have to register them.

huangapple
  • 本文由 发表于 2015年8月21日 11:48:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/32132064.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定