From f19b7563ea92b5c467c9e5e325a0a5b559712473 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 18 Jun 2020 18:14:03 +0200 Subject: [PATCH] feature: unsafe type conversion --- Makefile | 10 +++++++++- _test/unsafe0.go | 15 ++++++++++++++ _test/unsafe1.go | 20 +++++++++++++++++++ _test/unsafe2.go | 20 +++++++++++++++++++ _test/unsafe3.go | 26 ++++++++++++++++++++++++ _test/unsafe4.go | 29 +++++++++++++++++++++++++++ cmd/yaegi/yaegi.go | 6 ++++++ interp/interp.go | 25 ++++++++++++++++++++++- interp/interp_consistent_test.go | 2 ++ interp/interp_file_test.go | 2 ++ interp/run.go | 16 +++++++++++++++ stdlib/unsafe/unsafe.go | 34 +++++++++++++++++++++++++++++++- 12 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 _test/unsafe0.go create mode 100644 _test/unsafe1.go create mode 100644 _test/unsafe2.go create mode 100644 _test/unsafe3.go create mode 100644 _test/unsafe4.go diff --git a/Makefile b/Makefile index 8437b846..9701bf2d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +GO_VERSION=$(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1,2) + +RACE_TEST_GCFLAGS= +ifneq ($(GO_VERSION), 1.13) +# support for unsafe means we cannot do unsafe pointer arithmetic checks +RACE_TEST_GCFLAGS=-gcflags=all=-d=checkptr=0 +endif + # Static linting of source files. See .golangci.toml for options check: golangci-lint run @@ -18,6 +26,6 @@ generate: gen_all_syscall tests: GO111MODULE=off go test -v ./... - GO111MODULE=off go test -race ./interp + GO111MODULE=off go test -race $(RACE_TEST_GCFLAGS) -short ./interp .PHONY: check gen_all_syscall gen_tests diff --git a/_test/unsafe0.go b/_test/unsafe0.go new file mode 100644 index 00000000..d173202d --- /dev/null +++ b/_test/unsafe0.go @@ -0,0 +1,15 @@ +package main + +import "unsafe" + +func main() { + str := "foobar" + + p := unsafe.Pointer(&str) + str2 := *(*string)(p) + + println(str2) +} + +// Output: +// foobar diff --git a/_test/unsafe1.go b/_test/unsafe1.go new file mode 100644 index 00000000..d662064b --- /dev/null +++ b/_test/unsafe1.go @@ -0,0 +1,20 @@ +package main + +import "unsafe" + +type S struct { + Name string +} + +func main() { + s := &S{Name: "foobar"} + + p := unsafe.Pointer(s) + + s2 := (*S)(p) + + println(s2.Name) +} + +// Output: +// foobar diff --git a/_test/unsafe2.go b/_test/unsafe2.go new file mode 100644 index 00000000..f73f2aa5 --- /dev/null +++ b/_test/unsafe2.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "unsafe" +) + +func main() { + str := "foobar" + + ptr := unsafe.Pointer(&str) + p := uintptr(ptr) + + s1 := fmt.Sprintf("%x", ptr) + s2 := fmt.Sprintf("%x", p) + println(s1 == s2) +} + +// Output: +// true diff --git a/_test/unsafe3.go b/_test/unsafe3.go new file mode 100644 index 00000000..ffe8d1dd --- /dev/null +++ b/_test/unsafe3.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "unsafe" +) + +const SSize = 16 + +type S struct { + X int + Y int +} + +func main() { + var sBuf [SSize]byte + s := (*S)(unsafe.Pointer(&sBuf[0])) + + s.X = 2 + s.Y = 4 + + fmt.Println(sBuf) +} + +// Output: +// [2 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0] diff --git a/_test/unsafe4.go b/_test/unsafe4.go new file mode 100644 index 00000000..2e09a832 --- /dev/null +++ b/_test/unsafe4.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "unsafe" +) + +const SSize = 24 + +type S struct { + X int + Y int + Z int +} + +func main() { + arr := []S{ + {X: 1}, + {X: 2}, + {X: 3}, + } + addr := unsafe.Pointer(&arr[0]) + s := *(*S)(unsafe.Pointer(uintptr(addr) + SSize*2)) + + fmt.Println(s.X) +} + +// Output: +// 3 diff --git a/cmd/yaegi/yaegi.go b/cmd/yaegi/yaegi.go index 58184b6e..7646ef80 100644 --- a/cmd/yaegi/yaegi.go +++ b/cmd/yaegi/yaegi.go @@ -91,13 +91,16 @@ import ( "github.com/containous/yaegi/interp" "github.com/containous/yaegi/stdlib" + "github.com/containous/yaegi/stdlib/unsafe" ) func main() { var interactive bool + var useUnsafe bool var tags string var cmd string flag.BoolVar(&interactive, "i", false, "start an interactive REPL") + flag.BoolVar(&useUnsafe, "unsafe", false, "include usafe symbols") flag.StringVar(&tags, "tags", "", "set a list of build tags") flag.StringVar(&cmd, "e", "", "set the command to be executed (instead of script or/and shell)") flag.Usage = func() { @@ -112,6 +115,9 @@ func main() { i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")}) i.Use(stdlib.Symbols) i.Use(interp.Symbols) + if useUnsafe { + i.Use(unsafe.Symbols) + } if cmd != `` { i.REPL(strings.NewReader(cmd), os.Stderr) diff --git a/interp/interp.go b/interp/interp.go index 6111a73a..52380a6c 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -96,6 +96,9 @@ func (f *frame) clone() *frame { } } +// convertFn is the signature of a symbol converter +type convertFn func(from, to reflect.Type) func(src, dest reflect.Value) + // Exports stores the map of binary packages per package path type Exports map[string]map[string]reflect.Value @@ -138,6 +141,8 @@ type Interpreter struct { srcPkg imports // source packages used in interpreter, indexed by path pkgNames map[string]string // package names, indexed by path done chan struct{} // for cancellation of channel operations + + convert []convertFn // converters from symbols } const ( @@ -466,10 +471,28 @@ func (interp *Interpreter) getWrapper(t reflect.Type) reflect.Type { // they can be used in interpreted code func (interp *Interpreter) Use(values Exports) { for k, v := range values { - interp.binPkg[k] = v + if k != "github.com/containous/yaegi" { + interp.binPkg[k] = v + continue + } + + if con, ok := extractConverter(v["convert"]); ok { + interp.convert = append(interp.convert, con) + } } } +func extractConverter(v reflect.Value) (convertFn, bool) { + if !v.IsValid() { + return nil, false + } + fn, ok := v.Interface().(func(from, to reflect.Type) func(src, dest reflect.Value)) + if !ok { + return nil, false + } + return fn, true +} + // REPL performs a Read-Eval-Print-Loop on input reader. // Results are printed on output writer. func (interp *Interpreter) REPL(in io.Reader, out io.Writer) { diff --git a/interp/interp_consistent_test.go b/interp/interp_consistent_test.go index 1b5d25cd..c23d1db0 100644 --- a/interp/interp_consistent_test.go +++ b/interp/interp_consistent_test.go @@ -11,6 +11,7 @@ import ( "github.com/containous/yaegi/interp" "github.com/containous/yaegi/stdlib" + "github.com/containous/yaegi/stdlib/unsafe" ) func TestInterpConsistencyBuild(t *testing.T) { @@ -104,6 +105,7 @@ func TestInterpConsistencyBuild(t *testing.T) { i.Name = filePath i.Use(stdlib.Symbols) i.Use(interp.Symbols) + i.Use(unsafe.Symbols) _, err = i.Eval(string(src)) if err != nil { diff --git a/interp/interp_file_test.go b/interp/interp_file_test.go index 45dc4731..700b4bc3 100644 --- a/interp/interp_file_test.go +++ b/interp/interp_file_test.go @@ -12,6 +12,7 @@ import ( "github.com/containous/yaegi/interp" "github.com/containous/yaegi/stdlib" + "github.com/containous/yaegi/stdlib/unsafe" ) func TestFile(t *testing.T) { @@ -56,6 +57,7 @@ func runCheck(t *testing.T, p string) { i.Name = p i.Use(interp.Symbols) i.Use(stdlib.Symbols) + i.Use(unsafe.Symbols) _, err = i.Eval(string(src)) if errWanted { diff --git a/interp/run.go b/interp/run.go index 5b89ecd1..5f053e9f 100644 --- a/interp/run.go +++ b/interp/run.go @@ -329,6 +329,22 @@ func convert(n *node) { value = genValue(c) } + for _, con := range n.interp.convert { + if c.typ.rtype == nil { + continue + } + + fn := con(c.typ.rtype, typ) + if fn == nil { + continue + } + n.exec = func(f *frame) bltn { + fn(value(f), dest(f)) + return next + } + return + } + n.exec = func(f *frame) bltn { dest(f).Set(value(f).Convert(typ)) return next diff --git a/stdlib/unsafe/unsafe.go b/stdlib/unsafe/unsafe.go index 6190a725..e364f34f 100644 --- a/stdlib/unsafe/unsafe.go +++ b/stdlib/unsafe/unsafe.go @@ -3,7 +3,10 @@ // Package unsafe provides wrapper of standard library unsafe package to be imported natively in Yaegi. package unsafe -import "reflect" +import ( + "reflect" + "unsafe" +) // Symbols stores the map of unsafe package symbols var Symbols = map[string]map[string]reflect.Value{} @@ -12,6 +15,35 @@ func init() { Symbols["github.com/containous/yaegi/stdlib/unsafe"] = map[string]reflect.Value{ "Symbols": reflect.ValueOf(Symbols), } + Symbols["github.com/containous/yaegi"] = map[string]reflect.Value{ + "convert": reflect.ValueOf(convert), + } +} + +func convert(from, to reflect.Type) func(src, dest reflect.Value) { + switch { + case to.Kind() == reflect.UnsafePointer && from.Kind() == reflect.Uintptr: + return func(src, dest reflect.Value) { + dest.SetPointer(unsafe.Pointer(src.Interface().(uintptr))) //nolint:govet + } + case to.Kind() == reflect.UnsafePointer: + return func(src, dest reflect.Value) { + dest.SetPointer(unsafe.Pointer(src.Pointer())) + } + case to.Kind() == reflect.Uintptr && from.Kind() == reflect.UnsafePointer: + return func(src, dest reflect.Value) { + ptr := src.Interface().(unsafe.Pointer) + dest.Set(reflect.ValueOf(uintptr(ptr))) + } + case from.Kind() == reflect.UnsafePointer: + return func(src, dest reflect.Value) { + ptr := src.Interface().(unsafe.Pointer) + v := reflect.NewAt(dest.Type().Elem(), ptr) + dest.Set(v) + } + default: + return nil + } } //go:generate ../../cmd/goexports/goexports unsafe