diff --git a/Makefile b/Makefile index 869bf6e3..27d84c34 100644 --- a/Makefile +++ b/Makefile @@ -70,11 +70,8 @@ build-gobench: build-embedded: go build -tags "mrb gops" -ldflags $(LD_FLAGS) -o $(EMBEDDEDDIST) cmd/embedded-cable/main.go -download-mruby: - go mod download github.com/mitchellh/go-mruby - -prepare-mruby: download-mruby - cd $$(go list -m -f '{{.Dir}}' github.com/mitchellh/go-mruby) && \ +prepare-mruby: + cd ./vendorlib/go-mruby && \ MRUBY_COMMIT=$(MRUBY_VERSION) MRUBY_CONFIG=$(MRUBY_CONFIG) make libmruby.a || \ (sed -i '' 's/{ :verbose => $$verbose }/verbose: $$verbose/g' ./mruby-build/mruby/Rakefile && \ MRUBY_COMMIT=$(MRUBY_VERSION) MRUBY_CONFIG=$(MRUBY_CONFIG) make libmruby.a) @@ -82,8 +79,7 @@ prepare-mruby: download-mruby upgrade-mruby: clean-mruby prepare-mruby clean-mruby: - cd $$(go list -m -f '{{.Dir}}' github.com/mitchellh/go-mruby) && \ - rm -rf vendor/mruby + rm -rf vendorlib/go-mruby/mruby-build build-all-mruby: env $(GOBUILD) -tags mrb -o "dist/anycable-go-$(VERSION)-mrb-macos-amd64" cmd/anycable-go/main.go diff --git a/go.mod b/go.mod index 9c972a40..761a3cb4 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,9 @@ require ( google.golang.org/grpc v1.60.1 ) +// use vendored go-mruby +replace github.com/mitchellh/go-mruby => ./vendorlib/go-mruby + require github.com/sony/gobreaker v0.5.0 require ( diff --git a/go.sum b/go.sum index 086e80df..95f32fba 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= -github.com/mitchellh/go-mruby v0.0.0-20200315023956-207cedc21542 h1:/MjcGU93aaORB6Mydh9Q4D/oOim9BoR4jtpaAOgVZLQ= -github.com/mitchellh/go-mruby v0.0.0-20200315023956-207cedc21542/go.mod h1:TpwfcXhxDvAzz7wUcsTWu+FCaWGGLyyVZrL6sdkvK8k= github.com/nats-io/jwt/v2 v2.5.3 h1:/9SWvzc6hTfamcgXJ3uYRpgj+QuY2aLNqRiqrKcrpEo= github.com/nats-io/jwt/v2 v2.5.3/go.mod h1:iysuPemFcc7p4IoYots3IuELSI4EDe9Y0bQMe+I3Bf4= github.com/nats-io/nats-server/v2 v2.10.7 h1:f5VDy+GMu7JyuFA0Fef+6TfulfCs5nBTgq7MMkFJx5Y= diff --git a/vendorlib/go-mruby/.gitignore b/vendorlib/go-mruby/.gitignore new file mode 100644 index 00000000..411e13ad --- /dev/null +++ b/vendorlib/go-mruby/.gitignore @@ -0,0 +1,3 @@ +build_config.rb +libmruby.a +mruby-build diff --git a/vendorlib/go-mruby/.travis.yml b/vendorlib/go-mruby/.travis.yml new file mode 100644 index 00000000..2abe6dde --- /dev/null +++ b/vendorlib/go-mruby/.travis.yml @@ -0,0 +1,7 @@ +language: go +sudo: required +go: + - "1.14" + - "1.13" +install: sudo apt-get install build-essential g++ bison flex +script: make all staticcheck diff --git a/vendorlib/go-mruby/LICENSE b/vendorlib/go-mruby/LICENSE new file mode 100644 index 00000000..bea9cbf3 --- /dev/null +++ b/vendorlib/go-mruby/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendorlib/go-mruby/Makefile b/vendorlib/go-mruby/Makefile new file mode 100644 index 00000000..e01c7011 --- /dev/null +++ b/vendorlib/go-mruby/Makefile @@ -0,0 +1,35 @@ +MRUBY_COMMIT ?= 1.2.0 +MRUBY_VENDOR_DIR ?= mruby-build + +all: libmruby.a test + +clean: + rm -rf ${MRUBY_VENDOR_DIR} + rm -f libmruby.a + +gofmt: + @echo "Checking code with gofmt.." + gofmt -s *.go >/dev/null + +lint: + GO111MODULE=off go get golang.org/x/lint/golint + golint ./... + +staticcheck: + GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck + staticcheck ./... + +libmruby.a: ${MRUBY_VENDOR_DIR}/mruby + cd ${MRUBY_VENDOR_DIR}/mruby && ${MAKE} + cp ${MRUBY_VENDOR_DIR}/mruby/build/host/lib/libmruby.a . + +${MRUBY_VENDOR_DIR}/mruby: + mkdir -p ${MRUBY_VENDOR_DIR} + git clone https://github.com/mruby/mruby.git ${MRUBY_VENDOR_DIR}/mruby + cd ${MRUBY_VENDOR_DIR}/mruby && git reset --hard && git clean -fdx + cd ${MRUBY_VENDOR_DIR}/mruby && git checkout ${MRUBY_COMMIT} + +test: libmruby.a gofmt lint + go test -v + +.PHONY: all clean libmruby.a test lint staticcheck diff --git a/vendorlib/go-mruby/README.md b/vendorlib/go-mruby/README.md new file mode 100644 index 00000000..809721c6 --- /dev/null +++ b/vendorlib/go-mruby/README.md @@ -0,0 +1,112 @@ +# mruby Library for Go [![Build Status](https://travis-ci.org/mitchellh/go-mruby.svg?branch=master)](https://travis-ci.org/mitchellh/go-mruby) + +go-mruby provides [mruby](https://github.com/mruby/mruby) bindings for +[Go](http://golang.org). This allows Go applications to run a lightweight +embedded Ruby VM. Using the mruby library, Go applications can call Ruby +code, and Ruby code can call Go code (that is properly exposed)! + +At the time of writing, this is the most comprehensive mruby library for +Go _by far_. It is also the only mruby library for Go that enables exposing +Go functions to Ruby as well as being able to generically convert complex +Ruby types into Go types. Our goal is to implement all of the mruby API. + +**Project Status:** The major portions of the mruby API are implemented, +but the mruby API is huge. If there is something that is missing, please +issue a pull request and I'd be happy to add it! We're also not yet ready +to promise API backwards compatibility on a Go-level, but we're getting there. + +## Installation + +Installation is a little trickier than a standard Go library, but not +by much. You can't simply `go get` this library, unfortunately. This is +because [mruby](https://github.com/mruby/mruby) must first be built. We +don't ship a pre-built version of mruby because the build step of mruby +is important in customizing what aspects of the standard library you want +available, as well as any other extensions. + +To build mruby, we've made it very easy. You will need the following packages +available on your host operating system: + +* bison +* flex +* ruby 2.x + +Then just type: + +``` +$ make +``` + +This will download mruby, compile it, and run the tests for go-mruby, +verifying that your build is functional. By default, go-mruby will download +and build a default version of mruby, but this is customizable. + +Compiling/installing the go-mruby library should work on Linux, Mac OS X, +and Windows. On Windows, msys is the only supported build toolchain (same +as Go itself). + +**Due to this linking, it is strongly recommended that you vendor this +repository and bake our build system into your process.** + +### Customizing the mruby Compilation + +You can customize the mruby compilation by setting a couple environmental +variables prior to calling `make`: + + * `MRUBY_COMMIT` is the git ref that will be checked out for mruby. This + defaults to to a recently tagged version. Many versions before 1.2.0 do not + work with go-mruby. It is recommend you explicitly set this to a ref that + works for you to avoid any changes in this library later. + + * `MRUBY_CONFIG` is the path to a `build_config.rb` file used to configure + how mruby is built. If this is not set, go-mruby will use the default + build config that comes with mruby. You can learn more about configuring + the mruby build [here](https://github.com/mruby/mruby/tree/master/doc/guides/compile.md). + +## Usage + +go-mruby exposes the mruby API in a way that is idiomatic Go, so that it +is comfortable to use by a standard Go programmer without having intimate +knowledge of how mruby works. + +For usage examples and documentation, please see the +[go-mruby GoDoc](http://godoc.org/github.com/mitchellh/go-mruby), which +we keep up to date and full of examples. + +For a quick taste of what using go-mruby looks like, though, we provide +an example below: + +```go +package main + +import ( + "fmt" + "github.com/mitchellh/go-mruby" +) + +func main() { + mrb := mruby.NewMrb() + defer mrb.Close() + + // Our custom function we'll expose to Ruby. The first return + // value is what to return from the func and the second is an + // exception to raise (if any). + addFunc := func(m *mruby.Mrb, self *mruby.MrbValue) (mruby.Value, mruby.Value) { + args := m.GetArgs() + return mruby.Int(args[0].Fixnum() + args[1].Fixnum()), nil + } + + // Lets define a custom class and a class method we can call. + class := mrb.DefineClass("Example", nil) + class.DefineClassMethod("add", addFunc, mruby.ArgsReq(2)) + + // Let's call it and inspect the result + result, err := mrb.LoadString(`Example.add(12, 30)`) + if err != nil { + panic(err.Error()) + } + + // This will output "Result: 42" + fmt.Printf("Result: %s\n", result.String()) +} +``` diff --git a/vendorlib/go-mruby/args.go b/vendorlib/go-mruby/args.go new file mode 100644 index 00000000..ec88e114 --- /dev/null +++ b/vendorlib/go-mruby/args.go @@ -0,0 +1,52 @@ +package mruby + +import "sync" + +// #include "gomruby.h" +import "C" + +// ArgSpec defines how many arguments a function should take and +// what kind. Multiple ArgSpecs can be combined using the "|" +// operator. +type ArgSpec C.mrb_aspec + +// ArgsAny allows any number of arguments. +func ArgsAny() ArgSpec { + return ArgSpec(C._go_MRB_ARGS_ANY()) +} + +// ArgsArg says the given number of arguments are required and +// the second number is optional. +func ArgsArg(r, o int) ArgSpec { + return ArgSpec(C._go_MRB_ARGS_ARG(C.int(r), C.int(o))) +} + +// ArgsBlock says it takes a block argument. +func ArgsBlock() ArgSpec { + return ArgSpec(C._go_MRB_ARGS_BLOCK()) +} + +// ArgsNone says it takes no arguments. +func ArgsNone() ArgSpec { + return ArgSpec(C._go_MRB_ARGS_NONE()) +} + +// ArgsReq says that the given number of arguments are required. +func ArgsReq(n int) ArgSpec { + return ArgSpec(C._go_MRB_ARGS_REQ(C.int(n))) +} + +// ArgsOpt says that the given number of arguments are optional. +func ArgsOpt(n int) ArgSpec { + return ArgSpec(C._go_MRB_ARGS_OPT(C.int(n))) +} + +// The global accumulator when Mrb.GetArgs is called. There is a +// global lock around this so that the access to it is safe. +var getArgAccumulator []C.mrb_value +var getArgLock = new(sync.Mutex) + +//export goGetArgAppend +func goGetArgAppend(v C.mrb_value) { + getArgAccumulator = append(getArgAccumulator, v) +} diff --git a/vendorlib/go-mruby/array.go b/vendorlib/go-mruby/array.go new file mode 100644 index 00000000..60e3a56f --- /dev/null +++ b/vendorlib/go-mruby/array.go @@ -0,0 +1,31 @@ +package mruby + +// #include "gomruby.h" +import "C" + +// Array represents an MrbValue that is a Array in Ruby. +// +// A Array can be obtained by calling the Array function on MrbValue. +type Array struct { + *MrbValue +} + +// Len returns the length of the array. +func (v *Array) Len() int { + return int(C.mrb_ary_len(v.state, v.value)) +} + +// Get gets an element form the Array by index. +// +// This does not copy the element. This is a pointer/reference directly +// to the element in the array. +func (v *Array) Get(idx int) (*MrbValue, error) { + result := C.mrb_ary_entry(v.value, C.mrb_int(idx)) + + val := newValue(v.state, result) + if val.Type() == TypeNil { + val = nil + } + + return val, nil +} diff --git a/vendorlib/go-mruby/array_test.go b/vendorlib/go-mruby/array_test.go new file mode 100644 index 00000000..9df08883 --- /dev/null +++ b/vendorlib/go-mruby/array_test.go @@ -0,0 +1,43 @@ +package mruby + +import ( + "testing" +) + +func TestArray(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`["foo", "bar", "baz", false]`) + if err != nil { + t.Fatalf("err: %s", err) + } + + v := value.Array() + + // Len + if n := v.Len(); n != 4 { + t.Fatalf("bad: %d", n) + } + + // Get + value, err = v.Get(1) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "bar" { + t.Fatalf("bad: %s", value) + } + + // Get bool + value, err = v.Get(3) + if err != nil { + t.Fatalf("err: %s", err) + } + if valType := value.Type(); valType != TypeFalse { + t.Fatalf("bad type: %v", valType) + } + if value.String() != "false" { + t.Fatalf("bad: %s", value) + } +} diff --git a/vendorlib/go-mruby/class.go b/vendorlib/go-mruby/class.go new file mode 100644 index 00000000..5a165311 --- /dev/null +++ b/vendorlib/go-mruby/class.go @@ -0,0 +1,88 @@ +package mruby + +import "unsafe" + +// #include +// #include "gomruby.h" +import "C" + +// Class is a class in mruby. To obtain a Class, use DefineClass or +// one of the variants on the Mrb structure. +type Class struct { + class *C.struct_RClass + mrb *Mrb +} + +// DefineClassMethod defines a class-level method on the given class. +func (c *Class) DefineClassMethod(name string, cb Func, as ArgSpec) { + insertMethod(c.mrb.state, c.class.c, name, cb) + + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + C.mrb_define_class_method( + c.mrb.state, + c.class, + cs, + C._go_mrb_func_t(), + C.mrb_aspec(as)) +} + +// DefineConst defines a constant within this class. +func (c *Class) DefineConst(name string, value Value) { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + C.mrb_define_const( + c.mrb.state, c.class, cs, value.MrbValue(c.mrb).value) +} + +// DefineMethod defines an instance method on the class. +func (c *Class) DefineMethod(name string, cb Func, as ArgSpec) { + insertMethod(c.mrb.state, c.class, name, cb) + + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + C.mrb_define_method( + c.mrb.state, + c.class, + cs, + C._go_mrb_func_t(), + C.mrb_aspec(as)) +} + +// MrbValue returns a *Value for this Class. *Values are sometimes required +// as arguments where classes should be valid. +func (c *Class) MrbValue(m *Mrb) *MrbValue { + return newValue(c.mrb.state, C.mrb_obj_value(unsafe.Pointer(c.class))) +} + +// New instantiates the class with the given args. +func (c *Class) New(args ...Value) (*MrbValue, error) { + var argv []C.mrb_value + var argvPtr *C.mrb_value + if len(args) > 0 { + // Make the raw byte slice to hold our arguments we'll pass to C + argv = make([]C.mrb_value, len(args)) + for i, arg := range args { + argv[i] = arg.MrbValue(c.mrb).value + } + + argvPtr = &argv[0] + } + + result := C.mrb_obj_new(c.mrb.state, c.class, C.mrb_int(len(argv)), argvPtr) + if exc := checkException(c.mrb.state); exc != nil { + return nil, exc + } + + return newValue(c.mrb.state, result), nil +} + +func newClass(mrb *Mrb, c *C.struct_RClass) *Class { + return &Class{ + class: c, + mrb: mrb, + } +} diff --git a/vendorlib/go-mruby/class_test.go b/vendorlib/go-mruby/class_test.go new file mode 100644 index 00000000..09c63632 --- /dev/null +++ b/vendorlib/go-mruby/class_test.go @@ -0,0 +1,102 @@ +package mruby + +import ( + "testing" +) + +func TestClassDefineClassMethod(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("foo", testCallback, ArgsNone()) + value, err := mrb.LoadString("Hello.foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + testCallbackResult(t, value) +} + +func TestClassDefineConst(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineConst("FOO", String("bar")) + value, err := mrb.LoadString("Hello::FOO") + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "bar" { + t.Fatalf("bad: %s", value) + } +} + +func TestClassDefineMethod(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineMethod("foo", testCallback, ArgsNone()) + value, err := mrb.LoadString("Hello.new.foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + testCallbackResult(t, value) +} + +func TestClassNew(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineMethod("foo", testCallback, ArgsNone()) + + instance, err := class.New() + if err != nil { + t.Fatalf("err: %s", err) + } + + value, err := instance.Call("foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + testCallbackResult(t, value) +} + +func TestClassNewException(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineMethod("initialize", testCallbackException, ArgsNone()) + + _, err := class.New() + if err == nil { + t.Fatalf("expected exception") + } + + // Verify exception is cleared + val, err := mrb.LoadString(`"test"`) + if err != nil { + t.Fatalf("unexpected exception: %#v", err) + } + + if val.String() != "test" { + t.Fatalf("expected val 'test', got %#v", val) + } +} + +func TestClassValue(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + value := class.MrbValue(mrb) + if value.Type() != TypeClass { + t.Fatalf("bad: %d", value.Type()) + } +} diff --git a/vendorlib/go-mruby/context.go b/vendorlib/go-mruby/context.go new file mode 100644 index 00000000..a8ce9cc3 --- /dev/null +++ b/vendorlib/go-mruby/context.go @@ -0,0 +1,55 @@ +package mruby + +// #include "gomruby.h" +import "C" + +// CompileContext represents a context for code compilation. +// +// CompileContexts keep track of things such as filenames, line numbers, +// as well as some settings for how to parse and execute code. +type CompileContext struct { + ctx *C.mrbc_context + filename string + mrb *Mrb +} + +// NewCompileContext constructs a *CompileContext from a *Mrb. +func NewCompileContext(m *Mrb) *CompileContext { + return &CompileContext{ + ctx: C.mrbc_context_new(m.state), + mrb: m, + } +} + +// Close the context, freeing any resources associated with it. +// +// This is safe to call once the context has been used for parsing/loading +// any Ruby code. +func (c *CompileContext) Close() { + C.mrbc_context_free(c.mrb.state, c.ctx) +} + +// Filename returns the filename associated with this context. +func (c *CompileContext) Filename() string { + return C.GoString(c.ctx.filename) +} + +// SetFilename sets the filename associated with this compilation context. +// +// Code parsed under this context will be from this file. +func (c *CompileContext) SetFilename(f string) { + c.filename = f + c.ctx.filename = C.CString(c.filename) +} + +// CaptureErrors toggles the capture errors feature of the parser, which +// swallows errors. This allows repls and other partial parsing tools +// (formatters, f.e.) to function. +func (c *CompileContext) CaptureErrors(yes bool) { + state := 0 + if yes { + state = 1 + } + + C._go_mrb_context_set_capture_errors(c.ctx, C.int(state)) +} diff --git a/vendorlib/go-mruby/context_test.go b/vendorlib/go-mruby/context_test.go new file mode 100644 index 00000000..bbd6ee2b --- /dev/null +++ b/vendorlib/go-mruby/context_test.go @@ -0,0 +1,23 @@ +package mruby + +import ( + "testing" +) + +func TestCompileContextFilename(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + ctx := NewCompileContext(mrb) + defer ctx.Close() + + if ctx.Filename() != "" { + t.Fatalf("bad filename: %s", ctx.Filename()) + } + + ctx.SetFilename("foo") + + if ctx.Filename() != "foo" { + t.Fatalf("bad filename: %s", ctx.Filename()) + } +} diff --git a/vendorlib/go-mruby/decode.go b/vendorlib/go-mruby/decode.go new file mode 100644 index 00000000..a367c307 --- /dev/null +++ b/vendorlib/go-mruby/decode.go @@ -0,0 +1,511 @@ +package mruby + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// This is the tag to use with structures to have settings for mruby +const tagName = "mruby" + +// Decode converts the Ruby value to a Go value. +// +// The Decode process may call Ruby code and may generate Ruby garbage, +// but it collects all of its own garbage. You don't need to GC around this. +// +// See the tests (decode_test.go) for detailed and specific examples of +// how this function decodes. Basic examples are also available here and +// in the README. +// +// For primitives, the decoding process is likely what you expect. For Ruby, +// this is booleans, strings, fixnums, and floats. These map directly to +// effectively equivalent Go types: bool, string, int, float64. +// Hash and Arrays can map directly to maps and slices in Go, and Decode +// will handle this as you expect. +// +// The only remaining data type in Go is a struct. A struct in Go can map +// to any object in Ruby. If the data in Ruby is a hash, then the struct keys +// will map directly to the hash keys. If the data in Ruby is an object, then +// one of two things will be done. First: if the object responds to the +// `to_gomruby` function, then this will be called and the resulting value +// is expected to be a Hash and will be used to decode into the struct. If +// the object does NOT respond to that function, then any struct fields will +// invoke the corresponding Ruby method to attain the value. +// +// Note that with structs you can use the `mruby` tag to specify the +// Hash key or method name to call. Example: +// +// type Foo struct { +// Field string `mruby:"read_field"` +// } +// +func Decode(out interface{}, v *MrbValue) error { + // The out parameter must be a pointer since we must be + // able to write to it. + val := reflect.ValueOf(out) + if val.Kind() != reflect.Ptr { + return errors.New("result must be a pointer") + } + + var d decoder + return d.decode("root", v, val.Elem()) +} + +type decoder struct { + stack []reflect.Kind +} + +type decodeStructGetter func(string) (*MrbValue, error) + +func (d *decoder) decode(name string, v *MrbValue, result reflect.Value) error { + k := result + + // If we have an interface with a valid value, we use that + // for the check. + if result.Kind() == reflect.Interface { + elem := result.Elem() + if elem.IsValid() { + k = elem + } + } + + // Push current onto stack unless it is an interface. + if k.Kind() != reflect.Interface { + d.stack = append(d.stack, k.Kind()) + + // Schedule a pop + defer func() { + d.stack = d.stack[:len(d.stack)-1] + }() + } + + switch k.Kind() { + case reflect.Bool: + return d.decodeBool(name, v, result) + case reflect.Float64: + return d.decodeFloat(name, v, result) + case reflect.Int: + return d.decodeInt(name, v, result) + case reflect.Interface: + // When we see an interface, we make our own thing + return d.decodeInterface(name, v, result) + case reflect.Map: + return d.decodeMap(name, v, result) + case reflect.Ptr: + return d.decodePtr(name, v, result) + case reflect.Slice: + return d.decodeSlice(name, v, result) + case reflect.String: + return d.decodeString(name, v, result) + case reflect.Struct: + return d.decodeStruct(name, v, result) + default: + } + + return fmt.Errorf( + "%s: unknown kind to decode into: %s", name, k.Kind()) +} + +func (d *decoder) decodeBool(name string, v *MrbValue, result reflect.Value) error { + switch t := v.Type(); t { + case TypeFalse: + result.Set(reflect.ValueOf(false)) + case TypeTrue: + result.Set(reflect.ValueOf(true)) + default: + return fmt.Errorf("%s: unknown type %v", name, t) + } + + return nil +} + +func (d *decoder) decodeFloat(name string, v *MrbValue, result reflect.Value) error { + switch t := v.Type(); t { + case TypeFloat: + result.Set(reflect.ValueOf(v.Float())) + default: + return fmt.Errorf("%s: unknown type %v", name, t) + } + + return nil +} + +func (d *decoder) decodeInt(name string, v *MrbValue, result reflect.Value) error { + switch t := v.Type(); t { + case TypeFixnum: + result.Set(reflect.ValueOf(v.Fixnum())) + case TypeString: + v, err := strconv.ParseInt(v.String(), 0, 0) + if err != nil { + return err + } + + result.SetInt(int64(v)) + default: + return fmt.Errorf("%s: unknown type %v", name, t) + } + + return nil +} + +func (d *decoder) decodeInterface(name string, v *MrbValue, result reflect.Value) error { + var set reflect.Value + redecode := true + + switch t := v.Type(); t { + case TypeHash: + var temp map[string]interface{} + tempVal := reflect.ValueOf(temp) + result := reflect.MakeMap( + reflect.MapOf( + reflect.TypeOf(""), + tempVal.Type().Elem())) + + set = result + case TypeArray: + var temp []interface{} + tempVal := reflect.ValueOf(temp) + result := reflect.MakeSlice( + reflect.SliceOf(tempVal.Type().Elem()), 0, 0) + set = result + case TypeFalse: + fallthrough + case TypeTrue: + var result bool + set = reflect.Indirect(reflect.New(reflect.TypeOf(result))) + case TypeFixnum: + var result int + set = reflect.Indirect(reflect.New(reflect.TypeOf(result))) + case TypeFloat: + var result float64 + set = reflect.Indirect(reflect.New(reflect.TypeOf(result))) + case TypeString: + set = reflect.Indirect(reflect.New(reflect.TypeOf(""))) + default: + return fmt.Errorf( + "%s: cannot decode into interface: %v", + name, t) + } + + // Set the result to what its supposed to be, then reset + // result so we don't reflect into this method anymore. + result.Set(set) + + if redecode { + // Revisit the node so that we can use the newly instantiated + // thing and populate it. + if err := d.decode(name, v, result); err != nil { + return err + } + } + + return nil +} + +func (d *decoder) decodeMap(name string, v *MrbValue, result reflect.Value) error { + if v.Type() != TypeHash { + return fmt.Errorf("%s: not a hash type for map (%v)", name, v.Type()) + } + + // If we have an interface, then we can address the interface, + // but not the slice itself, so get the element but set the interface + set := result + if result.Kind() == reflect.Interface { + result = result.Elem() + } + + resultType := result.Type() + resultElemType := resultType.Elem() + resultKeyType := resultType.Key() + if resultKeyType.Kind() != reflect.String { + return fmt.Errorf( + "%s: map must have string keys", name) + } + + // Make a map if it is nil + resultMap := result + if result.IsNil() { + resultMap = reflect.MakeMap( + reflect.MapOf(resultKeyType, resultElemType)) + } + + // We're going to be allocating some garbage, so set the arena + // so it is cleared properly. + mrb := v.Mrb() + defer mrb.ArenaRestore(mrb.ArenaSave()) + + // Get the hash of the value + hash := v.Hash() + keysRaw, err := hash.Keys() + if err != nil { + return err + } + keys := keysRaw.Array() + + for i := 0; i < keys.Len(); i++ { + // Get the key and value in Ruby. This should do no allocations. + rbKey, err := keys.Get(i) + if err != nil { + return err + } + + rbVal, err := hash.Get(rbKey) + if err != nil { + return err + } + + // Make the field name + fieldName := fmt.Sprintf("%s.", name, i) + + // Decode the key into the key type + keyVal := reflect.Indirect(reflect.New(resultKeyType)) + if err := d.decode(fieldName, rbKey, keyVal); err != nil { + return err + } + + // Decode the value + val := reflect.Indirect(reflect.New(resultElemType)) + if err := d.decode(fieldName, rbVal, val); err != nil { + return err + } + + // Set the value on the map + resultMap.SetMapIndex(keyVal, val) + + } + + // Set the final map if we can + set.Set(resultMap) + return nil +} + +func (d *decoder) decodePtr(name string, v *MrbValue, result reflect.Value) error { + // Create an element of the concrete (non pointer) type and decode + // into that. Then set the value of the pointer to this type. + resultType := result.Type() + resultElemType := resultType.Elem() + val := reflect.New(resultElemType) + if err := d.decode(name, v, reflect.Indirect(val)); err != nil { + return err + } + + result.Set(val) + return nil +} + +func (d *decoder) decodeSlice(name string, v *MrbValue, result reflect.Value) error { + // If we have an interface, then we can address the interface, + // but not the slice itself, so get the element but set the interface + set := result + if result.Kind() == reflect.Interface { + result = result.Elem() + } + + // Create the slice if it isn't nil + resultType := result.Type() + resultElemType := resultType.Elem() + if result.IsNil() { + resultSliceType := reflect.SliceOf(resultElemType) + result = reflect.MakeSlice( + resultSliceType, 0, 0) + } + + // Get the hash of the value + array := v.Array() + + for i := 0; i < array.Len(); i++ { + // Get the key and value in Ruby. This should do no allocations. + rbVal, err := array.Get(i) + if err != nil { + return err + } + + // Make the field name + fieldName := fmt.Sprintf("%s[%d]", name, i) + + // Decode the value + val := reflect.Indirect(reflect.New(resultElemType)) + if err := d.decode(fieldName, rbVal, val); err != nil { + return err + } + + // Append it onto the slice + result = reflect.Append(result, val) + } + + set.Set(result) + return nil +} + +func (d *decoder) decodeString(name string, v *MrbValue, result reflect.Value) error { + switch t := v.Type(); t { + case TypeFixnum: + result.Set(reflect.ValueOf( + strconv.FormatInt(int64(v.Fixnum()), 10)).Convert(result.Type())) + case TypeString: + result.Set(reflect.ValueOf(v.String()).Convert(result.Type())) + default: + return fmt.Errorf("%s: unknown type to string: %v", name, t) + } + + return nil +} + +func (d *decoder) decodeStruct(name string, v *MrbValue, result reflect.Value) error { + var get decodeStructGetter + + // We're going to be allocating some garbage, so set the arena + // so it is cleared properly. + mrb := v.Mrb() + defer mrb.ArenaRestore(mrb.ArenaSave()) + + // Depending on the type, we need to generate a getter + switch t := v.Type(); t { + case TypeHash: + get = decodeStructHashGetter(mrb, v.Hash()) + case TypeObject: + get = decodeStructObjectMethods(mrb, v) + default: + return fmt.Errorf("%s: not an object type for struct (%v)", name, t) + } + + // This slice will keep track of all the structs we'll be decoding. + // There can be more than one struct if there are embedded structs + // that are squashed. + structs := make([]reflect.Value, 1, 5) + structs[0] = result + + // Compile the list of all the fields that we're going to be decoding + // from all the structs. + fields := make(map[*reflect.StructField]reflect.Value) + for len(structs) > 0 { + structVal := structs[0] + structs = structs[1:] + + structType := structVal.Type() + for i := 0; i < structType.NumField(); i++ { + fieldType := structType.Field(i) + + if fieldType.Anonymous { + fieldKind := fieldType.Type.Kind() + if fieldKind != reflect.Struct { + return fmt.Errorf( + "%s: unsupported type to struct: %s", + fieldType.Name, fieldKind) + } + + // We have an embedded field. We "squash" the fields down + // if specified in the tag. + squash := false + tagParts := strings.Split(fieldType.Tag.Get(tagName), ",") + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + + if squash { + structs = append( + structs, result.FieldByName(fieldType.Name)) + continue + } + } + + // Normal struct field, store it away + fields[&fieldType] = structVal.Field(i) + } + } + + var ( + decodedFields = make([]string, 0, len(fields)) + decodedFieldsVal = []reflect.Value{} + usedKeys = make(map[string]struct{}) + ) + + for fieldType, field := range fields { + if !field.IsValid() { + // This should never happen + panic("field is not valid") + } + + // If we can't set the field, then it is unexported or something, + // and we just continue onwards. + if !field.CanSet() { + continue + } + + fieldName := strings.ToLower(fieldType.Name) + + tagValue := fieldType.Tag.Get(tagName) + tagParts := strings.SplitN(tagValue, ",", 2) + if len(tagParts) >= 2 { + switch tagParts[1] { + case "decodedFields": + decodedFieldsVal = append(decodedFieldsVal, field) + continue + } + } + + if tagParts[0] != "" { + fieldName = tagParts[0] + } + + // We move the arena for every value here so we don't + // generate too much intermediate garbage. + idx := mrb.ArenaSave() + + // Get the Ruby string value + value, err := get(fieldName) + if err != nil { + mrb.ArenaRestore(idx) + return err + } + + // Track the used key + usedKeys[fieldName] = struct{}{} + + // Create the field name and decode. We range over the elements + // because we actually want the value. + fieldName = fmt.Sprintf("%s.%s", name, fieldName) + err = d.decode(fieldName, value, field) + mrb.ArenaRestore(idx) + if err != nil { + return err + } + + decodedFields = append(decodedFields, fieldType.Name) + } + + if len(decodedFieldsVal) > 0 { + // Sort it so that it is deterministic + sort.Strings(decodedFields) + + for _, v := range decodedFieldsVal { + v.Set(reflect.ValueOf(decodedFields)) + } + } + + return nil +} + +// decodeStructHashGetter is a decodeStructGetter that reads values from +// a hash. +func decodeStructHashGetter(mrb *Mrb, h *Hash) decodeStructGetter { + return func(key string) (*MrbValue, error) { + rbKey := mrb.StringValue(key) + return h.Get(rbKey) + } +} + +// decodeStructObjectMethods is a decodeStructGetter that reads values from +// an object by calling methods. +func decodeStructObjectMethods(mrb *Mrb, v *MrbValue) decodeStructGetter { + return func(key string) (*MrbValue, error) { + return v.Call(key) + } +} diff --git a/vendorlib/go-mruby/decode_test.go b/vendorlib/go-mruby/decode_test.go new file mode 100644 index 00000000..9fdd495b --- /dev/null +++ b/vendorlib/go-mruby/decode_test.go @@ -0,0 +1,221 @@ +package mruby + +import ( + "reflect" + "testing" +) + +func TestDecode(t *testing.T) { + type structString struct { + Foo string + } + + var outBool bool + var outFloat64 float64 + var outInt int + var outMap, outMap2 map[string]string + var outPtrInt *int + var outSlice []string + var outString string + var outStructString structString + + cases := []struct { + Input string + Output interface{} + Expected interface{} + }{ + // Booleans + { + "true", + &outBool, + true, + }, + + { + "false", + &outBool, + false, + }, + + // Float + { + "1.2", + &outFloat64, + float64(1.2000000476837158), + }, + + // Int + { + "32", + &outInt, + int(32), + }, + + { + `"32"`, + &outInt, + int(32), + }, + + // Map + { + `{"foo" => "bar"}`, + &outMap, + map[string]string{"foo": "bar"}, + }, + + { + `{32 => "bar"}`, + &outMap2, + map[string]string{"32": "bar"}, + }, + + // Slice + { + `["foo", "bar"]`, + &outSlice, + []string{"foo", "bar"}, + }, + + // Ptr + { + `32`, + &outPtrInt, + 32, + }, + + // String + { + `32`, + &outString, + "32", + }, + + { + `"32"`, + &outString, + "32", + }, + + // Struct from Hash + { + `{"foo" => "bar"}`, + &outStructString, + structString{Foo: "bar"}, + }, + + // Struct from object with methods + { + testDecodeObjectMethods, + &outStructString, + structString{Foo: "bar"}, + }, + } + + for _, tc := range cases { + mrb := NewMrb() + value, err := mrb.LoadString(tc.Input) + if err != nil { + mrb.Close() + t.Fatalf("err: %s\n\n%s", err, tc.Input) + } + + err = Decode(tc.Output, value) + mrb.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + val := reflect.ValueOf(tc.Output) + for val.Kind() == reflect.Ptr { + val = reflect.Indirect(val) + } + actual := val.Interface() + if !reflect.DeepEqual(actual, tc.Expected) { + t.Fatalf("bad: %#v\n\n%#v", actual, tc.Expected) + } + } +} + +func TestDecodeInterface(t *testing.T) { + cases := []struct { + Input string + Expected interface{} + }{ + // Booleans + { + "true", + true, + }, + + { + "false", + false, + }, + + // Float + { + "1.2", + float64(1.2000000476837158), + }, + + // Int + { + "32", + int(32), + }, + + // Map + { + `{"foo" => "bar"}`, + map[string]interface{}{"foo": "bar"}, + }, + + { + `{32 => "bar"}`, + map[string]interface{}{"32": "bar"}, + }, + + // Slice + { + `["foo", "bar"]`, + []interface{}{"foo", "bar"}, + }, + + // String + { + `"32"`, + "32", + }, + } + + for _, tc := range cases { + mrb := NewMrb() + value, err := mrb.LoadString(tc.Input) + if err != nil { + mrb.Close() + t.Fatalf("err: %s\n\n%s", err, tc.Input) + } + + var result interface{} + err = Decode(&result, value) + mrb.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(result, tc.Expected) { + t.Fatalf("bad: \n\n%s\n\n%#v\n\n%#v", tc.Input, result, tc.Expected) + } + } +} + +const testDecodeObjectMethods = ` +class Foo + def foo + "bar" + end +end + +Foo.new +` diff --git a/vendorlib/go-mruby/examples_test.go b/vendorlib/go-mruby/examples_test.go new file mode 100644 index 00000000..d20cc190 --- /dev/null +++ b/vendorlib/go-mruby/examples_test.go @@ -0,0 +1,102 @@ +package mruby + +import ( + "fmt" +) + +func Example_CustomFunction() { + mrb := NewMrb() + defer mrb.Close() + + // Our custom function we'll expose to Ruby + addFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + args := m.GetArgs() + return Int(args[0].Fixnum() + args[1].Fixnum()), nil + } + + // Lets define a custom class and a class method we can call. + class := mrb.DefineClass("Example", nil) + class.DefineClassMethod("add", addFunc, ArgsReq(2)) + + // Let's call it and inspect the result + result, err := mrb.LoadString(`Example.add(12, 30)`) + if err != nil { + panic(err.Error()) + } + + fmt.Printf("Result: %s\n", result.String()) + // Output: + // Result: 42 +} + +func Example_Decode() { + mrb := NewMrb() + defer mrb.Close() + + // Our custom function we'll expose to Ruby + var logData interface{} + logFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + args := m.GetArgs() + if err := Decode(&logData, args[0]); err != nil { + panic(err) + } + + return nil, nil + } + + // Lets define a custom class and a class method we can call. + class := mrb.DefineClass("Example", nil) + class.DefineClassMethod("log", logFunc, ArgsReq(1)) + + // Let's call it and inspect the result + if _, err := mrb.LoadString(`Example.log({"foo" => "bar"})`); err != nil { + panic(err.Error()) + } + + fmt.Printf("Result: %v\n", logData) + // Output: + // Result: map[foo:bar] +} + +func Example_SimulateFiles() { + mrb := NewMrb() + defer mrb.Close() + + ctx1 := NewCompileContext(mrb) + defer ctx1.Close() + ctx1.SetFilename("foo.rb") + + ctx2 := NewCompileContext(mrb) + defer ctx2.Close() + ctx2.SetFilename("bar.rb") + + parser := NewParser(mrb) + defer parser.Close() + + if _, err := parser.Parse("def foo; bar; end", ctx1); err != nil { + panic(err.Error()) + } + code1 := parser.GenerateCode() + + if _, err := parser.Parse("def bar; 42; end", ctx2); err != nil { + panic(err.Error()) + } + code2 := parser.GenerateCode() + + if _, err := mrb.Run(code1, nil); err != nil { + panic(err.Error()) + } + + if _, err := mrb.Run(code2, nil); err != nil { + panic(err.Error()) + } + + result, err := mrb.LoadString("foo") + if err != nil { + panic(err.Error()) + } + + fmt.Printf("Result: %s\n", result) + // Output: + // Result: 42 +} diff --git a/vendorlib/go-mruby/func.go b/vendorlib/go-mruby/func.go new file mode 100644 index 00000000..19c3dd9a --- /dev/null +++ b/vendorlib/go-mruby/func.go @@ -0,0 +1,121 @@ +package mruby + +import ( + "fmt" + "sync" + "unsafe" +) + +// #include +// #include "gomruby.h" +import "C" + +// Func is the signature of a function in Go that you use to expose to Ruby +// code. +// +// The first return value is the actual return value for the code. +// +// The second return value is an exception, if any. This will be raised. +type Func func(m *Mrb, self *MrbValue) (Value, Value) + +type classMethodMap map[*C.struct_RClass]*methods +type methodMap map[C.mrb_sym]Func +type stateMethodMap map[*C.mrb_state]*classMethods + +type classMethods struct { + Map classMethodMap + Mutex *sync.Mutex +} + +type methods struct { + Map methodMap + Mutex *sync.Mutex +} + +type stateMethods struct { + Map stateMethodMap + Mutex *sync.Mutex +} + +// stateMethodTable is the lookup table for methods that we define in Go and +// expose in Ruby. This is cleaned up by Mrb.Close. +var stateMethodTable *stateMethods + +func init() { + stateMethodTable = &stateMethods{ + Mutex: new(sync.Mutex), + Map: make(stateMethodMap), + } +} + +//export goMRBFuncCall +func goMRBFuncCall(s *C.mrb_state, v C.mrb_value) C.mrb_value { + // Lookup the classes that we've registered methods for in this state + stateMethodTable.Mutex.Lock() + classTable := stateMethodTable.Map[s] + stateMethodTable.Mutex.Unlock() + if classTable == nil { + panic(fmt.Sprintf("func call from unknown state: %p", s)) + } + + // Get the call info, which we use to lookup the proc + ci := s.c.ci + + // Lookup the class itself + classTable.Mutex.Lock() + methodTable := classTable.Map[ci.proc.target_class] + classTable.Mutex.Unlock() + if methodTable == nil { + panic("func call on unknown class") + } + + // Lookup the method + methodTable.Mutex.Lock() + f := methodTable.Map[ci.mid] + methodTable.Mutex.Unlock() + if f == nil { + panic("func call on unknown method") + } + + // Call the method to get our *Value + // TODO(mitchellh): reuse the Mrb instead of allocating every time + mrb := &Mrb{s} + result, exc := f(mrb, newValue(s, v)) + + if result == nil { + result = mrb.NilValue() + } + + if exc != nil { + s.exc = C._go_mrb_getobj(exc.MrbValue(mrb).value) + return mrb.NilValue().value + } + + return result.MrbValue(mrb).value +} + +func insertMethod(s *C.mrb_state, c *C.struct_RClass, n string, f Func) { + stateMethodTable.Mutex.Lock() + classLookup := stateMethodTable.Map[s] + if classLookup == nil { + classLookup = &classMethods{Map: make(classMethodMap), Mutex: new(sync.Mutex)} + stateMethodTable.Map[s] = classLookup + } + stateMethodTable.Mutex.Unlock() + + classLookup.Mutex.Lock() + methodLookup := classLookup.Map[c] + if methodLookup == nil { + methodLookup = &methods{Map: make(methodMap), Mutex: new(sync.Mutex)} + classLookup.Map[c] = methodLookup + } + classLookup.Mutex.Unlock() + + cs := C.CString(n) + defer C.free(unsafe.Pointer(cs)) + + sym := C.mrb_intern_cstr(s, cs) + methodLookup.Mutex.Lock() + methodLookup.Map[sym] = f + methodLookup.Mutex.Unlock() +} diff --git a/vendorlib/go-mruby/func_test.go b/vendorlib/go-mruby/func_test.go new file mode 100644 index 00000000..938b0c65 --- /dev/null +++ b/vendorlib/go-mruby/func_test.go @@ -0,0 +1,23 @@ +package mruby + +import "testing" + +func testCallback(m *Mrb, self *MrbValue) (Value, Value) { + return Int(42), nil +} + +func testCallbackResult(t *testing.T, v *MrbValue) { + if v.Type() != TypeFixnum { + t.Fatalf("bad type: %d", v.Type()) + } + + if v.Fixnum() != 42 { + t.Fatalf("bad: %d", v.Fixnum()) + } +} + +func testCallbackException(m *Mrb, self *MrbValue) (Value, Value) { + _, e := m.LoadString(`raise 'Exception'`) + v := e.(*Exception) + return nil, v.MrbValue +} diff --git a/vendorlib/go-mruby/gc_test.go b/vendorlib/go-mruby/gc_test.go new file mode 100644 index 00000000..bc9b793e --- /dev/null +++ b/vendorlib/go-mruby/gc_test.go @@ -0,0 +1,49 @@ +package mruby + +import "testing" + +func TestEnableDisableGC(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + mrb.FullGC() + mrb.DisableGC() + + _, err := mrb.LoadString("b = []; a = []; a = []") + if err != nil { + t.Fatal(err) + } + + orig := mrb.LiveObjectCount() + mrb.FullGC() + + if orig != mrb.LiveObjectCount() { + t.Fatalf("Object count was not what was expected after full GC: %d %d", orig, mrb.LiveObjectCount()) + } + + mrb.EnableGC() + mrb.FullGC() + + if orig-2 != mrb.LiveObjectCount() { + t.Fatalf("Object count was not what was expected after full GC: %d %d", orig-2, mrb.LiveObjectCount()) + } +} + +func TestIsDead(t *testing.T) { + mrb := NewMrb() + + val, err := mrb.LoadString("$a = []") + if err != nil { + t.Fatal(err) + } + + if val.IsDead() { + t.Fatal("Value is already dead and should not be") + } + + mrb.Close() + + if !val.IsDead() { + t.Fatal("Value should be dead and is not") + } +} diff --git a/vendorlib/go-mruby/go.mod b/vendorlib/go-mruby/go.mod new file mode 100644 index 00000000..7e22350c --- /dev/null +++ b/vendorlib/go-mruby/go.mod @@ -0,0 +1,3 @@ +module github.com/mitchellh/go-mruby + +go 1.14 diff --git a/vendorlib/go-mruby/go.sum b/vendorlib/go-mruby/go.sum new file mode 100644 index 00000000..e69de29b diff --git a/vendorlib/go-mruby/gomruby.h b/vendorlib/go-mruby/gomruby.h new file mode 100644 index 00000000..2319a93d --- /dev/null +++ b/vendorlib/go-mruby/gomruby.h @@ -0,0 +1,271 @@ +// vim: ft=c ts=2 sts=2 st=2 +/* + * This header exists to simplify the headers that are included within + * the Go files. This header should include all the necessary headers + * for the compilation of the Go library. + * */ + +#ifndef _GOMRUBY_H_INCLUDED +#define _GOMRUBY_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// (erikh) this can be set in mruby/mrbconfig.h so we can default it here. +// XXX I don't know how this actually plays out when the config is modified. +// I'm taking a WAG here. Either way, the default is 16 in vm.c. +#ifndef MRB_FUNCALL_ARGC_MAX + #define MRB_FUNCALL_ARGC_MAX 16 +#endif // MRB_FUNCALL_ARGC_MAX + +//------------------------------------------------------------------- +// Helpers to deal with calling back into Go. +//------------------------------------------------------------------- +// This is declard in func.go and is a way for us to call back into +// Go to execute a method. +extern mrb_value goMRBFuncCall(mrb_state*, mrb_value); + +// This method is used as a way to get a valid mrb_func_t that actually +// just calls back into Go. +static inline mrb_func_t _go_mrb_func_t() { + return &goMRBFuncCall; +} + +//------------------------------------------------------------------- +// Helpers to deal with calling into Ruby (C) +//------------------------------------------------------------------- +// These are some really horrible C macros that are used to wrap +// various mruby C API function calls so that we catch the exceptions. +// If we let exceptions through then the longjmp will cause a Go stack +// split. +#define GOMRUBY_EXC_PROTECT_START \ + struct mrb_jmpbuf *prev_jmp = mrb->jmp; \ + struct mrb_jmpbuf c_jmp; \ + mrb_value result = mrb_nil_value(); \ + MRB_TRY(&c_jmp) { \ + mrb->jmp = &c_jmp; + +#define GOMRUBY_EXC_PROTECT_END \ + mrb->jmp = prev_jmp; \ + } MRB_CATCH(&c_jmp) { \ + mrb->jmp = prev_jmp; \ + result = mrb_nil_value();\ + } MRB_END_EXC(&c_jmp); \ + mrb_gc_protect(mrb, result); \ + return result; + +static mrb_value _go_mrb_load_string(mrb_state *mrb, const char *s) { + GOMRUBY_EXC_PROTECT_START + result = mrb_load_string(mrb, s); + GOMRUBY_EXC_PROTECT_END +} + +static mrb_value _go_mrb_yield_argv(mrb_state *mrb, mrb_value b, mrb_int argc, const mrb_value *argv) { + GOMRUBY_EXC_PROTECT_START + result = mrb_yield_argv(mrb, b, argc, argv); + GOMRUBY_EXC_PROTECT_END +} + +static mrb_value _go_mrb_call(mrb_state *mrb, mrb_value b, mrb_sym method, mrb_int argc, const mrb_value *argv, mrb_value *block) { + GOMRUBY_EXC_PROTECT_START + if (block != NULL) { + result = mrb_funcall_with_block(mrb, b, method, argc, argv, *block); + } else { + result = mrb_funcall_argv(mrb, b, method, argc, argv); + } + GOMRUBY_EXC_PROTECT_END +} + +//------------------------------------------------------------------- +// Helpers to deal with getting arguments +//------------------------------------------------------------------- +// This is declard in args.go +extern void goGetArgAppend(mrb_value); + +// This gets all arguments given to a function call and adds them to +// the accumulator in Go. +static inline int _go_mrb_get_args_all(mrb_state *s) { + mrb_value *argv; + mrb_value block; + mrb_bool append; + int argc, i; + + mrb_get_args(s, "*&?", &argv, &argc, &block, &append); + + for (i = 0; i < argc; i++) { + goGetArgAppend(argv[i]); + } + + if (append == FALSE || mrb_type(block) == MRB_TT_FALSE) { + return argc; + } + + argc++; + goGetArgAppend(block); + + return argc; +} + +//------------------------------------------------------------------- +// Misc. helpers +//------------------------------------------------------------------- + +// This is used to help calculate the "send" value for the parser, +// since pointer arithmetic like this is hard in Go. +static inline const char *_go_mrb_calc_send(const char *s) { + return s + strlen(s); +} + +// Sets the capture_errors field on mrb_parser_state. Go can't access bit +// fields. +static inline void +_go_mrb_parser_set_capture_errors(struct mrb_parser_state *p, mrb_bool v) { + p->capture_errors = v; +} + +//------------------------------------------------------------------- +// Functions below here expose defines or inline functions that were +// otherwise inaccessible to Go directly. +//------------------------------------------------------------------- + +static inline mrb_aspec _go_MRB_ARGS_ANY() { + return MRB_ARGS_ANY(); +} + +static inline mrb_aspec _go_MRB_ARGS_ARG(int r, int o) { + return MRB_ARGS_ARG(r, o); +} + +static inline mrb_aspec _go_MRB_ARGS_BLOCK() { + return MRB_ARGS_BLOCK(); +} + +static inline mrb_aspec _go_MRB_ARGS_NONE() { + return MRB_ARGS_NONE(); +} + +static inline mrb_aspec _go_MRB_ARGS_OPT(int n) { + return MRB_ARGS_OPT(n); +} + +static inline mrb_aspec _go_MRB_ARGS_REQ(int n) { + return MRB_ARGS_REQ(n); +} + +static inline float _go_mrb_float(mrb_value o) { + return mrb_float(o); +} + +static inline int _go_mrb_fixnum(mrb_value o) { + return mrb_fixnum(o); +} + +static inline struct RBasic *_go_mrb_basic_ptr(mrb_value o) { + return mrb_basic_ptr(o); +} + +static inline struct RProc *_go_mrb_proc_ptr(mrb_value o) { + return mrb_proc_ptr(o); +} + +static inline enum mrb_vtype _go_mrb_type(mrb_value o) { + return mrb_type(o); +} + +static inline mrb_bool _go_mrb_nil_p(mrb_value o) { + return mrb_nil_p(o); +} + +static inline struct RClass *_go_mrb_class_ptr(mrb_value o) { + return mrb_class_ptr(o); +} + +static inline void _go_set_gc(mrb_state *m, int val) { + mrb_gc *gc = &m->gc; + gc->disabled = val; +} + +static inline void _go_disable_gc(mrb_state *m) { + _go_set_gc(m, 1); +} + +static inline void _go_enable_gc(mrb_state *m) { + _go_set_gc(m, 0); +} + +static inline int _go_get_max_funcall_args() { + return MRB_FUNCALL_ARGC_MAX; +} + +// this function returns 1 if the value is dead, aka reaped or otherwise +// terminated by the GC. +static inline int _go_isdead(mrb_state *m, mrb_value o) { + // immediate values such as Fixnums and symbols are never to be garbage + // collected, so converting them to a basic pointer yields an invalid one. + // This pattern is seen in the mruby source's gc.c. + if mrb_immediate_p(o) { + return 0; + } + + struct RBasic *ptr = mrb_basic_ptr(o); + + // I don't actually know this is a potential condition but better safe than sorry. + if (ptr == NULL) { + return 1; + } + + return mrb_object_dead_p(m, ptr); +} + +static inline int _go_gc_live(mrb_state *m) { + mrb_gc *gc = &m->gc; + return gc->live; +} + +static inline void _go_mrb_context_set_capture_errors(struct mrbc_context *ctx, int state) { + ctx->capture_errors = FALSE; + + if (state != 0) { + ctx->capture_errors = TRUE; + } +} + +static inline mrb_value _go_mrb_context_run(mrb_state *m, struct RProc *proc, mrb_value self, int *stack_keep) { + mrb_value result = mrb_context_run(m, proc, self, *stack_keep); + *stack_keep = proc->body.irep->nlocals; + return result; +} + +static inline struct RObject* _go_mrb_getobj(mrb_value v) { + return mrb_obj_ptr(v); +} + +static inline void _go_mrb_iv_set(mrb_state *m, mrb_value self, mrb_sym sym, mrb_value v) { + mrb_iv_set(m, self, sym, v); +} + +static inline mrb_value _go_mrb_iv_get(mrb_state *m, mrb_value self, mrb_sym sym) { + return mrb_iv_get(m, self, sym); +} + +static inline void _go_mrb_gv_set(mrb_state *m, mrb_sym sym, mrb_value v) { + mrb_gv_set(m, sym, v); +} + +static inline mrb_value _go_mrb_gv_get(mrb_state *m, mrb_sym sym) { + return mrb_gv_get(m, sym); +} + +#endif diff --git a/vendorlib/go-mruby/hash.go b/vendorlib/go-mruby/hash.go new file mode 100644 index 00000000..2ff59c9d --- /dev/null +++ b/vendorlib/go-mruby/hash.go @@ -0,0 +1,48 @@ +package mruby + +// #include "gomruby.h" +import "C" + +// Hash represents an MrbValue that is a Hash in Ruby. +// +// A Hash can be obtained by calling the Hash function on MrbValue. +type Hash struct { + *MrbValue +} + +// Delete deletes a key from the hash, returning its existing value, +// or nil if there wasn't a value. +func (h *Hash) Delete(key Value) (*MrbValue, error) { + keyVal := key.MrbValue(&Mrb{h.state}).value + result := C.mrb_hash_delete_key(h.state, h.value, keyVal) + + val := newValue(h.state, result) + if val.Type() == TypeNil { + val = nil + } + + return val, nil +} + +// Get reads a value from the hash. +func (h *Hash) Get(key Value) (*MrbValue, error) { + keyVal := key.MrbValue(&Mrb{h.state}).value + result := C.mrb_hash_get(h.state, h.value, keyVal) + return newValue(h.state, result), nil +} + +// Set sets a value on the hash +func (h *Hash) Set(key, val Value) error { + keyVal := key.MrbValue(&Mrb{h.state}).value + valVal := val.MrbValue(&Mrb{h.state}).value + C.mrb_hash_set(h.state, h.value, keyVal, valVal) + return nil +} + +// Keys returns the array of keys that the Hash has. This is returned +// as an *MrbValue since this is a Ruby array. You can iterate over it as +// you see fit. +func (h *Hash) Keys() (*MrbValue, error) { + result := C.mrb_hash_keys(h.state, h.value) + return newValue(h.state, result), nil +} diff --git a/vendorlib/go-mruby/hash_test.go b/vendorlib/go-mruby/hash_test.go new file mode 100644 index 00000000..73e21851 --- /dev/null +++ b/vendorlib/go-mruby/hash_test.go @@ -0,0 +1,89 @@ +package mruby + +import ( + "testing" +) + +func TestHash(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`{"foo" => "bar", "baz" => false}`) + if err != nil { + t.Fatalf("err: %s", err) + } + + h := value.Hash() + + // Get + value, err = h.Get(String("foo")) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "bar" { + t.Fatalf("bad: %s", value) + } + + // Get false type + value, err = h.Get(String("baz")) + if err != nil { + t.Fatalf("err: %s", err) + } + if valType := value.Type(); valType != TypeFalse { + t.Fatalf("bad type: %v", valType) + } + if value.String() != "false" { + t.Fatalf("bad: %s", value) + } + + // Set + err = h.Set(String("foo"), String("baz")) + if err != nil { + t.Fatalf("err: %s", err) + } + value, err = h.Get(String("foo")) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "baz" { + t.Fatalf("bad: %s", value) + } + + // Keys + value, err = h.Keys() + if err != nil { + t.Fatalf("err: %s", err) + } + if value.Type() != TypeArray { + t.Fatalf("bad: %v", value.Type()) + } + if value.String() != `["foo", "baz"]` { + t.Fatalf("bad: %s", value) + } + + // Delete + value, err = h.Delete(String("foo")) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "baz" { + t.Fatalf("bad: %s", value) + } + + value, err = h.Keys() + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != `["baz"]` { + t.Fatalf("bad: %s", value) + } + + // Delete non-existing + value, err = h.Delete(String("nope")) + if err != nil { + t.Fatalf("err: %s", err) + } + if value != nil { + t.Fatalf("bad: %s", value) + } +} diff --git a/vendorlib/go-mruby/mruby.go b/vendorlib/go-mruby/mruby.go new file mode 100644 index 00000000..4308883b --- /dev/null +++ b/vendorlib/go-mruby/mruby.go @@ -0,0 +1,400 @@ +package mruby + +import "unsafe" + +// #cgo CFLAGS: -Imruby-build/mruby/include +// #cgo LDFLAGS: ${SRCDIR}/libmruby.a -lm +// #include +// #include "gomruby.h" +import "C" + +// Mrb represents a single instance of mruby. +type Mrb struct { + state *C.mrb_state +} + +// GetGlobalVariable returns the value of the global variable by the given name. +func (m *Mrb) GetGlobalVariable(name string) *MrbValue { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + return newValue(m.state, C._go_mrb_gv_get(m.state, C.mrb_intern_cstr(m.state, cs))) +} + +// SetGlobalVariable sets the value of the global variable by the given name. +func (m *Mrb) SetGlobalVariable(name string, value Value) { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + v := value.MrbValue(m) + C._go_mrb_gv_set(m.state, C.mrb_intern_cstr(m.state, cs), v.value) +} + +// ArenaIndex represents the index into the arena portion of the GC. +// +// See ArenaSave for more information. +type ArenaIndex int + +// NewMrb creates a new instance of Mrb, representing the state of a single +// Ruby VM. +// +// When you're finished with the VM, clean up all resources it is using +// by calling the Close method. +func NewMrb() *Mrb { + state := C.mrb_open() + + return &Mrb{ + state: state, + } +} + +// ArenaRestore restores the arena index so the objects between the save and this point +// can be garbage collected in the future. +// +// See ArenaSave for more documentation. +func (m *Mrb) ArenaRestore(idx ArenaIndex) { + C.mrb_gc_arena_restore(m.state, C.int(idx)) +} + +// ArenaSave saves the index into the arena. +// +// Restore the arena index later by calling ArenaRestore. +// +// The arena is where objects returned by functions such as LoadString +// are stored. By saving the index and then later restoring it with +// ArenaRestore, these objects can be garbage collected. Otherwise, the +// objects will never be garbage collected. +// +// The recommended usage pattern for memory management is to save +// the arena index prior to any Ruby execution, to turn the resulting +// Ruby value into Go values as you see fit, then to restore the arena +// index so that GC can collect any values. +// +// Of course, when Close() is called, all objects in the arena are +// garbage collected anyways, so if you're only calling mruby for a short +// period of time, you might not have to worry about saving/restoring the +// arena. +func (m *Mrb) ArenaSave() ArenaIndex { + return ArenaIndex(C.mrb_gc_arena_save(m.state)) +} + +// EnableGC enables the garbage collector for this mruby instance. It returns +// true if garbage collection was previously disabled. +func (m *Mrb) EnableGC() { + C._go_enable_gc(m.state) +} + +// DisableGC disables the garbage collector for this mruby instance. It returns +// true if it was previously disabled. +func (m *Mrb) DisableGC() { + C._go_disable_gc(m.state) +} + +// LiveObjectCount returns the number of objects that have not been collected (aka, alive). +func (m *Mrb) LiveObjectCount() int { + return int(C._go_gc_live(m.state)) +} + +// Class returns the class with the kgiven name and superclass. Note that +// if you call this with a class that doesn't exist, mruby will abort the +// application (like a panic, but not a Go panic). +// +// super can be nil, in which case the Object class will be used. +func (m *Mrb) Class(name string, super *Class) *Class { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + var class *C.struct_RClass + if super == nil { + class = C.mrb_class_get(m.state, cs) + } else { + class = C.mrb_class_get_under(m.state, super.class, cs) + } + + return newClass(m, class) +} + +// Module returns the named module as a *Class. If the module is invalid, +// NameError is triggered within your program and SIGABRT is sent to the +// application. +func (m *Mrb) Module(name string) *Class { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + class := C.mrb_module_get(m.state, cs) + + return newClass(m, class) +} + +// Close a Mrb, this must be called to properly free resources, and +// should only be called once. +func (m *Mrb) Close() { + // Delete all the methods from the state + stateMethodTable.Mutex.Lock() + delete(stateMethodTable.Map, m.state) + stateMethodTable.Mutex.Unlock() + + // Close the state + C.mrb_close(m.state) +} + +// ConstDefined checks if the given constant is defined in the scope. +// +// This should be used, for example, before a call to Class, because a +// failure in Class will crash your program (by design). You can retrieve +// the Value of a Class by calling Value(). +func (m *Mrb) ConstDefined(name string, scope Value) bool { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + scopeV := scope.MrbValue(m).value + b := C.mrb_const_defined( + m.state, scopeV, C.mrb_intern_cstr(m.state, cs)) + return C.ushort(b) != 0 +} + +// FullGC executes a complete GC cycle on the VM. +func (m *Mrb) FullGC() { + C.mrb_full_gc(m.state) +} + +// GetArgs returns all the arguments that were given to the currnetly +// called function (currently on the stack). +func (m *Mrb) GetArgs() []*MrbValue { + getArgLock.Lock() + defer getArgLock.Unlock() + + // Clear reset the accumulator to zero length + getArgAccumulator = make([]C.mrb_value, 0, C._go_get_max_funcall_args()) + + // Get all the arguments and put it into our accumulator + count := C._go_mrb_get_args_all(m.state) + + // Convert those all to values + values := make([]*MrbValue, count) + + for i := 0; i < int(count); i++ { + values[i] = newValue(m.state, getArgAccumulator[i]) + } + + return values +} + +// IncrementalGC runs an incremental GC step. It is much less expensive +// than a FullGC, but must be called multiple times for GC to actually +// happen. +// +// This function is best called periodically when executing Ruby in +// the VM many times (thousands of times). +func (m *Mrb) IncrementalGC() { + C.mrb_incremental_gc(m.state) +} + +// LoadString loads the given code, executes it, and returns its final +// value that it might return. +func (m *Mrb) LoadString(code string) (*MrbValue, error) { + cs := C.CString(code) + defer C.free(unsafe.Pointer(cs)) + + value := C._go_mrb_load_string(m.state, cs) + if exc := checkException(m.state); exc != nil { + return nil, exc + } + + return newValue(m.state, value), nil +} + +// Run executes the given value, which should be a proc type. +// +// If you're looking to execute code directly a string, look at LoadString. +// +// If self is nil, it is set to the top-level self. +func (m *Mrb) Run(v Value, self Value) (*MrbValue, error) { + if self == nil { + self = m.TopSelf() + } + + mrbV := v.MrbValue(m) + mrbSelf := self.MrbValue(m) + + proc := C._go_mrb_proc_ptr(mrbV.value) + value := C.mrb_run(m.state, proc, mrbSelf.value) + + if exc := checkException(m.state); exc != nil { + return nil, exc + } + + return newValue(m.state, value), nil +} + +// RunWithContext is a context-aware parser (aka, it does not discard state +// between runs). It returns a magic integer that describes the stack in place, +// so that it can be re-used on the next call. This is how local variables can +// traverse ruby parse invocations. +// +// Otherwise, it is very similar in function to Run() +func (m *Mrb) RunWithContext(v Value, self Value, stackKeep int) (int, *MrbValue, error) { + if self == nil { + self = m.TopSelf() + } + + mrbV := v.MrbValue(m) + mrbSelf := self.MrbValue(m) + proc := C._go_mrb_proc_ptr(mrbV.value) + + i := C.int(stackKeep) + + value := C._go_mrb_context_run(m.state, proc, mrbSelf.value, &i) + + if exc := checkException(m.state); exc != nil { + return stackKeep, nil, exc + } + + return int(i), newValue(m.state, value), nil +} + +// Yield yields to a block with the given arguments. +// +// This should be called within the context of a Func. +func (m *Mrb) Yield(block Value, args ...Value) (*MrbValue, error) { + mrbBlock := block.MrbValue(m) + + var argv []C.mrb_value + var argvPtr *C.mrb_value + + if len(args) > 0 { + // Make the raw byte slice to hold our arguments we'll pass to C + argv = make([]C.mrb_value, len(args)) + for i, arg := range args { + argv[i] = arg.MrbValue(m).value + } + + argvPtr = &argv[0] + } + + result := C._go_mrb_yield_argv( + m.state, + mrbBlock.value, + C.mrb_int(len(argv)), + argvPtr) + + if exc := checkException(m.state); exc != nil { + return nil, exc + } + + return newValue(m.state, result), nil +} + +//------------------------------------------------------------------- +// Functions handling defining new classes/modules in the VM +//------------------------------------------------------------------- + +// DefineClass defines a new top-level class. +// +// If super is nil, the class will be defined under Object. +func (m *Mrb) DefineClass(name string, super *Class) *Class { + if super == nil { + super = m.ObjectClass() + } + + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + return newClass( + m, C.mrb_define_class(m.state, cs, super.class)) +} + +// DefineClassUnder defines a new class under another class. +// +// This is, for example, how you would define the World class in +// `Hello::World` where Hello is the "outer" class. +func (m *Mrb) DefineClassUnder(name string, super *Class, outer *Class) *Class { + if super == nil { + super = m.ObjectClass() + } + if outer == nil { + outer = m.ObjectClass() + } + + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + return newClass(m, C.mrb_define_class_under( + m.state, outer.class, cs, super.class)) +} + +// DefineModule defines a top-level module. +func (m *Mrb) DefineModule(name string) *Class { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + return newClass(m, C.mrb_define_module(m.state, cs)) +} + +// DefineModuleUnder defines a module under another class/module. +func (m *Mrb) DefineModuleUnder(name string, outer *Class) *Class { + if outer == nil { + outer = m.ObjectClass() + } + + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + + return newClass(m, + C.mrb_define_module_under(m.state, outer.class, cs)) +} + +//------------------------------------------------------------------- +// Functions below return Values or constant Classes +//------------------------------------------------------------------- + +// ObjectClass returns the Object top-level class. +func (m *Mrb) ObjectClass() *Class { + return newClass(m, m.state.object_class) +} + +// KernelModule returns the Kernel top-level module. +func (m *Mrb) KernelModule() *Class { + return newClass(m, m.state.kernel_module) +} + +// TopSelf returns the top-level `self` value. +func (m *Mrb) TopSelf() *MrbValue { + return newValue(m.state, C.mrb_obj_value(unsafe.Pointer(m.state.top_self))) +} + +// FalseValue returns a Value for "false" +func (m *Mrb) FalseValue() *MrbValue { + return newValue(m.state, C.mrb_false_value()) +} + +// NilValue returns "nil" +func (m *Mrb) NilValue() *MrbValue { + return newValue(m.state, C.mrb_nil_value()) +} + +// TrueValue returns a Value for "true" +func (m *Mrb) TrueValue() *MrbValue { + return newValue(m.state, C.mrb_true_value()) +} + +// FixnumValue returns a Value for a fixed number. +func (m *Mrb) FixnumValue(v int) *MrbValue { + return newValue(m.state, C.mrb_fixnum_value(C.mrb_int(v))) +} + +// StringValue returns a Value for a string. +func (m *Mrb) StringValue(s string) *MrbValue { + cs := C.CString(s) + defer C.free(unsafe.Pointer(cs)) + return newValue(m.state, C.mrb_str_new_cstr(m.state, cs)) +} + +func checkException(state *C.mrb_state) error { + if state.exc == nil { + return nil + } + + err := newExceptionValue(state) + state.exc = nil + + return err +} diff --git a/vendorlib/go-mruby/mruby_test.go b/vendorlib/go-mruby/mruby_test.go new file mode 100644 index 00000000..a0fa2603 --- /dev/null +++ b/vendorlib/go-mruby/mruby_test.go @@ -0,0 +1,664 @@ +package mruby + +import ( + "fmt" + "reflect" + "testing" +) + +func TestNewMrb(t *testing.T) { + mrb := NewMrb() + mrb.Close() +} + +func TestMrbArena(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + idx := mrb.ArenaSave() + mrb.ArenaRestore(idx) +} + +func TestMrbModule(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + module := mrb.Module("Kernel") + if module == nil { + t.Fatal("module was nil and should not be") + } +} + +func TestMrbClass(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + class := mrb.Class("Object", nil) + if class == nil { + t.Fatal("class should not be nil") + } + + mrb.DefineClass("Hello", mrb.ObjectClass()) + class = mrb.Class("Hello", mrb.ObjectClass()) + if class == nil { + t.Fatal("class should not be nil") + } +} + +func TestMrbConstDefined(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + if !mrb.ConstDefined("Object", mrb.ObjectClass()) { + t.Fatal("Object should be defined") + } + + mrb.DefineClass("Hello", mrb.ObjectClass()) + if !mrb.ConstDefined("Hello", mrb.ObjectClass()) { + t.Fatal("Hello should be defined") + } +} + +func TestMrbDefineClass(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + mrb.DefineClass("Hello", mrb.ObjectClass()) + _, err := mrb.LoadString("Hello") + if err != nil { + t.Fatalf("err: %s", err) + } + + mrb.DefineClass("World", nil) + _, err = mrb.LoadString("World") + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestMrbDefineClass_methodException(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + cb := func(m *Mrb, self *MrbValue) (Value, Value) { + v, err := m.LoadString(`raise "exception"`) + if err != nil { + exc := err.(*Exception) + return nil, exc.MrbValue + } + + return v, nil + } + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("foo", cb, ArgsNone()) + _, err := mrb.LoadString(`Hello.foo`) + if err == nil { + t.Fatal("should error") + } +} + +func TestMrbDefineClassUnder(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + // Define an outer + hello := mrb.DefineClass("Hello", mrb.ObjectClass()) + _, err := mrb.LoadString("Hello") + if err != nil { + t.Fatalf("err: %s", err) + } + + // Inner + mrb.DefineClassUnder("World", nil, hello) + _, err = mrb.LoadString("Hello::World") + if err != nil { + t.Fatalf("err: %s", err) + } + + // Inner defaults + mrb.DefineClassUnder("Another", nil, nil) + _, err = mrb.LoadString("Another") + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestMrbDefineModule(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + mrb.DefineModule("Hello") + _, err := mrb.LoadString("Hello") + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestMrbDefineModuleUnder(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + // Define an outer + hello := mrb.DefineModule("Hello") + _, err := mrb.LoadString("Hello") + if err != nil { + t.Fatalf("err: %s", err) + } + + // Inner + mrb.DefineModuleUnder("World", hello) + _, err = mrb.LoadString("Hello::World") + if err != nil { + t.Fatalf("err: %s", err) + } + + // Inner defaults + mrb.DefineModuleUnder("Another", nil) + _, err = mrb.LoadString("Another") + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestMrbFixnumValue(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value := mrb.FixnumValue(42) + if value.Type() != TypeFixnum { + t.Fatalf("should be fixnum") + } +} + +func TestMrbFullGC(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + ai := mrb.ArenaSave() + value := mrb.StringValue("foo") + if value.IsDead() { + t.Fatal("should not be dead") + } + + mrb.ArenaRestore(ai) + mrb.FullGC() + if !value.IsDead() { + t.Fatal("should be dead") + } +} + +type testcase struct { + args string + types []ValueType + result []string +} + +func TestMrbGetArgs(t *testing.T) { + cases := []testcase{ + { + `("foo")`, + []ValueType{TypeString}, + []string{`"foo"`}, + }, + + { + `(true)`, + []ValueType{TypeTrue}, + []string{`true`}, + }, + + { + `(Hello)`, + []ValueType{TypeClass}, + []string{`Hello`}, + }, + + { + `() { }`, + []ValueType{TypeProc}, + nil, + }, + + { + `(Hello, "bar", true)`, + []ValueType{TypeClass, TypeString, TypeTrue}, + []string{`Hello`, `"bar"`, "true"}, + }, + + { + `("bar", true) {}`, + []ValueType{TypeString, TypeTrue, TypeProc}, + nil, + }, + } + + // lots of this effort is centered around testing multithreaded behavior. + + for i := 0; i < 1000; i++ { + + errChan := make(chan error, len(cases)) + + for _, tc := range cases { + go func(tc testcase) { + var actual []*MrbValue + testFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + actual = m.GetArgs() + return self, nil + } + + mrb := NewMrb() + defer mrb.Close() + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("test", testFunc, ArgsAny()) + _, err := mrb.LoadString(fmt.Sprintf("Hello.test%s", tc.args)) + if err != nil { + errChan <- fmt.Errorf("err: %s", err) + return + } + + if tc.result != nil { + if len(actual) != len(tc.result) { + errChan <- fmt.Errorf("%s: expected %d, got %d", + tc.args, len(tc.result), len(actual)) + return + } + } + + actualStrings := make([]string, len(actual)) + actualTypes := make([]ValueType, len(actual)) + for i, v := range actual { + str, err := v.Call("inspect") + if err != nil { + errChan <- err + } + + actualStrings[i] = str.String() + actualTypes[i] = v.Type() + } + + if !reflect.DeepEqual(actualTypes, tc.types) { + errChan <- fmt.Errorf("code: %s\nexpected: %#v\nactual: %#v", + tc.args, tc.types, actualTypes) + return + } + + if tc.result != nil { + if !reflect.DeepEqual(actualStrings, tc.result) { + errChan <- fmt.Errorf("expected: %#v\nactual: %#v", + tc.result, actualStrings) + return + } + } + + errChan <- nil + }(tc) + } + + for range cases { + if err := <-errChan; err != nil { + t.Fatal(err) + } + } + } +} + +func TestMrbGlobalVariable(t *testing.T) { + const ( + TestValue = "HELLO" + ) + mrb := NewMrb() + defer mrb.Close() + if _, err := mrb.LoadString(fmt.Sprintf(`$a = "%s"`, TestValue)); err != nil { + t.Fatalf("err: %s", err) + } + value := mrb.GetGlobalVariable("$a") + if value.String() != TestValue { + t.Fatalf("wrong value for $a: expected '%s', found '%s'", TestValue, value.String()) + } + mrb.SetGlobalVariable("$b", mrb.StringValue(TestValue)) + value, err := mrb.LoadString(`$b`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != TestValue { + t.Fatalf("wrong value for $b: expected '%s', found '%s'", TestValue, value.String()) + } +} + +func TestMrbInstanceVariable(t *testing.T) { + const ( + GoldenRetriever = "golden retriever" + Husky = "Husky" + ) + mrb := NewMrb() + defer mrb.Close() + _, err := mrb.LoadString(` + class Dog + def initialize(breed) + @breed = breed + end + def breed + "cocker spaniel" # this line exists to ensure that it's not invoking the accessor method + end + def real_breed + @breed + end + end + `) + if err != nil { + t.Fatalf("err: %s", err) + } + dogClass := mrb.Class("Dog", nil) + if dogClass == nil { + t.Fatalf("dog class not found") + } + inst, err := dogClass.New(mrb.StringValue(GoldenRetriever)) + if err != nil { + t.Fatalf("err: %s", err) + } + value := inst.GetInstanceVariable("@breed") + if value.String() != GoldenRetriever { + t.Fatalf("wrong value for Dog.@breed. expected: '%s', found: '%s'", GoldenRetriever, value.String()) + } + inst.SetInstanceVariable("@breed", mrb.StringValue(Husky)) + value = inst.GetInstanceVariable("@breed") + if value.String() != Husky { + t.Fatalf("wrong value for Dog.@breed. expected: '%s', found: '%s'", Husky, value.String()) + } +} +func TestMrbLoadString(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`"HELLO"`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value == nil { + t.Fatalf("should have value") + } +} + +func TestMrbLoadString_twice(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`"HELLO"`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value == nil { + t.Fatalf("should have value") + } + + value, err = mrb.LoadString(`"WORLD"`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "WORLD" { + t.Fatalf("bad: %s", value) + } +} + +func TestMrbLoadStringException(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + _, err := mrb.LoadString(`raise "An exception"`) + + if err == nil { + t.Fatal("exception expected") + } + + value, err := mrb.LoadString(`"test"`) + if err != nil { + t.Fatal("exception should have been cleared") + } + + if value.String() != "test" { + t.Fatal("bad test value returned") + } +} + +func TestMrbRaise(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + cb := func(m *Mrb, self *MrbValue) (Value, Value) { + return nil, m.GetArgs()[0] + } + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("foo", cb, ArgsReq(1)) + _, err := mrb.LoadString(`Hello.foo(ArgumentError.new("ouch"))`) + if err == nil { + t.Fatal("should have error") + } + if err.Error() != "ouch" { + t.Fatalf("bad: %s", err) + } +} + +func TestMrbYield(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + cb := func(m *Mrb, self *MrbValue) (Value, Value) { + result, err := m.Yield(m.GetArgs()[0], Int(12), Int(30)) + if err != nil { + t.Fatalf("err: %s", err) + } + + return result, nil + } + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("foo", cb, ArgsBlock()) + value, err := mrb.LoadString(`Hello.foo { |a, b| a + b }`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.Fixnum() != 42 { + t.Fatalf("bad: %s", value) + } +} + +func TestMrbYieldException(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + cb := func(m *Mrb, self *MrbValue) (Value, Value) { + result, err := m.Yield(m.GetArgs()[0]) + if err != nil { + exc := err.(*Exception) + return nil, exc.MrbValue + } + + return result, nil + } + + class := mrb.DefineClass("Hello", mrb.ObjectClass()) + class.DefineClassMethod("foo", cb, ArgsBlock()) + _, err := mrb.LoadString(`Hello.foo { raise "exception" }`) + if err == nil { + t.Fatal("should error") + } + + _, err = mrb.LoadString(`Hello.foo { 1 }`) + if err != nil { + t.Fatal("exception should have been cleared") + } +} + +func TestMrbRun(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + parser := NewParser(mrb) + defer parser.Close() + context := NewCompileContext(mrb) + defer context.Close() + + parser.Parse(` + if $do_raise + raise "exception" + else + "rval" + end`, + context, + ) + + proc := parser.GenerateCode() + + // Enable proc exception raising & verify + mrb.LoadString(`$do_raise = true`) + _, err := mrb.Run(proc, nil) + + if err == nil { + t.Fatalf("expected exception, %#v", err) + } + + // Disable proc exception raising + // If we still have an exception, it wasn't cleared from the previous invocation. + mrb.LoadString(`$do_raise = false`) + rval, err := mrb.Run(proc, nil) + if err != nil { + t.Fatalf("unexpected exception, %#v", err) + } + + if rval.String() != "rval" { + t.Fatalf("expected return value 'rval', got %#v", rval) + } + + parser.Parse(`a = 10`, context) + proc = parser.GenerateCode() + + stackKeep, _, err := mrb.RunWithContext(proc, nil, 0) + if err != nil { + t.Fatal(err) + } + + if stackKeep != 2 { + t.Fatalf("stack value was %d not 2; some variables may not have been captured", stackKeep) + } + + parser.Parse(`a`, context) + proc = parser.GenerateCode() + + var ret *MrbValue + _, ret, err = mrb.RunWithContext(proc, nil, stackKeep) + if err != nil { + t.Fatal(err) + } + + if ret.String() != "10" { + t.Fatalf("Captured variable was not expected value: was %q", ret.String()) + } +} + +func TestMrbDefineMethodConcurrent(t *testing.T) { + concurrency := 100 + numFuncs := 100 + + cb := func(m *Mrb, self *MrbValue) (Value, Value) { + return m.GetArgs()[0], nil + } + + syncChan := make(chan struct{}, concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + mrb := NewMrb() + defer mrb.Close() + for i := 0; i < numFuncs; i++ { + mrb.TopSelf().SingletonClass().DefineMethod(fmt.Sprintf("test%d", i), cb, ArgsAny()) + } + + syncChan <- struct{}{} + }() + } + + for i := 0; i < concurrency; i++ { + <-syncChan + } +} + +func TestMrbStackedException(t *testing.T) { + var testClass *Class + + createException := func(m *Mrb, msg string) Value { + val, err := m.Class("Exception", nil).New(String(msg)) + if err != nil { + panic(fmt.Sprintf("could not construct exception for return: %v", err)) + } + + return val + } + + testFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + args := m.GetArgs() + + t, err := testClass.New() + if err != nil { + return nil, createException(m, err.Error()) + } + + argv := []Value{} + for _, arg := range args { + argv = append(argv, Value(arg)) + } + v, err := t.Call("dotest!", argv...) + if err != nil { + return nil, createException(m, err.Error()) + } + + return v, nil + } + + doTestFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + err := createException(m, "Fail us!") + return nil, err + } + + mrb := NewMrb() + + testClass = mrb.DefineClass("TestClass", nil) + testClass.DefineMethod("dotest!", doTestFunc, ArgsReq(0)|ArgsOpt(3)) + + mrb.TopSelf().SingletonClass().DefineMethod("test", testFunc, ArgsReq(0)|ArgsOpt(3)) + + _, err := mrb.LoadString("test") + if err == nil { + t.Fatal("No exception when one was expected") + return + } + + mrb.Close() + mrb = NewMrb() + + evalFunc := func(m *Mrb, self *MrbValue) (Value, Value) { + arg := m.GetArgs()[0] + result, err := self.CallBlock("instance_eval", arg) + if err != nil { + return result, createException(m, err.Error()) + } + + return result, nil + } + + mrb.TopSelf().SingletonClass().DefineMethod("myeval", evalFunc, ArgsBlock()) + + result, err := mrb.LoadString("myeval { raise 'foo' }") + if err == nil { + t.Fatal("did not error") + return + } + + if result != nil { + t.Fatal("result was not cleared") + return + } + + mrb.Close() +} diff --git a/vendorlib/go-mruby/parser.go b/vendorlib/go-mruby/parser.go new file mode 100644 index 00000000..54cd4378 --- /dev/null +++ b/vendorlib/go-mruby/parser.go @@ -0,0 +1,127 @@ +package mruby + +import ( + "bytes" + "fmt" + "unsafe" +) + +// #include +// #include "gomruby.h" +import "C" + +// Parser is a parser for Ruby code. +type Parser struct { + code string + mrb *Mrb + parser *C.struct_mrb_parser_state +} + +// NewParser initializes the resources for a parser. +// +// Make sure to Close the parser when you're done with it. +func NewParser(m *Mrb) *Parser { + p := C.mrb_parser_new(m.state) + + // Set capture_errors to true so we don't go just printing things + // out to stdout. + C._go_mrb_parser_set_capture_errors(p, 1) + + return &Parser{ + mrb: m, + parser: p, + } +} + +// Close releases any resources associated with the parser. +func (p *Parser) Close() { + C.mrb_parser_free(p.parser) + + // Empty out the code so the other string can get GCd + p.code = "" +} + +// GenerateCode takes all the internal parser state and generates +// executable Ruby code, returning the callable proc. +func (p *Parser) GenerateCode() *MrbValue { + proc := C.mrb_generate_code(p.mrb.state, p.parser) + return newValue(p.mrb.state, C.mrb_obj_value(unsafe.Pointer(proc))) +} + +// Parse parses the code in the given context, and returns any warnings +// or errors from parsing. +// +// The CompileContext can be nil to not set a context. +func (p *Parser) Parse(code string, c *CompileContext) ([]*ParserMessage, error) { + // We set p.code so that the string doesn't get garbage collected + var s *C.char = C.CString(code) + p.code = code + p.parser.s = s + p.parser.send = C._go_mrb_calc_send(s) + + var ctx *C.mrbc_context + if c != nil { + ctx = c.ctx + } + C.mrb_parser_parse(p.parser, ctx) + + var warnings []*ParserMessage + if p.parser.nwarn > 0 { + nwarn := int(p.parser.nwarn) + warnings = make([]*ParserMessage, nwarn) + for i := 0; i < nwarn; i++ { + msg := p.parser.warn_buffer[i] + + warnings[i] = &ParserMessage{ + Col: int(msg.column), + Line: int(msg.lineno), + Message: C.GoString(msg.message), + } + } + } + + if p.parser.nerr > 0 { + nerr := int(p.parser.nerr) + errors := make([]*ParserMessage, nerr) + for i := 0; i < nerr; i++ { + msg := p.parser.error_buffer[i] + + errors[i] = &ParserMessage{ + Col: int(msg.column), + Line: int(msg.lineno), + Message: C.GoString(msg.message), + } + } + + return warnings, &ParserError{Errors: errors} + } + + return warnings, nil +} + +// ParserMessage represents a message from parsing code: a warning or +// error. +type ParserMessage struct { + Col int + Line int + Message string +} + +// ParserError is an error from the parser. +type ParserError struct { + Errors []*ParserMessage +} + +func (p ParserError) Error() string { + return p.String() +} + +func (p ParserError) String() string { + var buf bytes.Buffer + buf.WriteString("Ruby parse error!\n\n") + for _, e := range p.Errors { + buf.WriteString(fmt.Sprintf("line %d:%d: %s\n", e.Line, e.Col, e.Message)) + } + + return buf.String() +} diff --git a/vendorlib/go-mruby/parser_test.go b/vendorlib/go-mruby/parser_test.go new file mode 100644 index 00000000..31307183 --- /dev/null +++ b/vendorlib/go-mruby/parser_test.go @@ -0,0 +1,63 @@ +package mruby + +import ( + "testing" +) + +func TestParserGenerateCode(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + p := NewParser(mrb) + defer p.Close() + + warns, err := p.Parse(`"foo"`, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if warns != nil { + t.Fatalf("warnings: %v", warns) + } + + proc := p.GenerateCode() + result, err := mrb.Run(proc, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if result.String() != "foo" { + t.Fatalf("bad: %s", result.String()) + } +} + +func TestParserParse(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + p := NewParser(mrb) + defer p.Close() + + warns, err := p.Parse(`"foo"`, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if warns != nil { + t.Fatalf("warnings: %v", warns) + } +} + +func TestParserParse_error(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + p := NewParser(mrb) + defer p.Close() + + _, err := p.Parse(`def foo`, nil) + if err == nil { + t.Fatal("should have errors") + } +} + +func TestParserError_error(t *testing.T) { + var _ error = new(ParserError) +} diff --git a/vendorlib/go-mruby/value.go b/vendorlib/go-mruby/value.go new file mode 100644 index 00000000..dd6af23f --- /dev/null +++ b/vendorlib/go-mruby/value.go @@ -0,0 +1,295 @@ +package mruby + +import ( + "fmt" + "strconv" + "strings" + "unsafe" +) + +// #include +// #include "gomruby.h" +import "C" + +// Value is an interface that should be implemented by anything that can +// be represents as an mruby value. +type Value interface { + MrbValue(*Mrb) *MrbValue +} + +// Int is the basic ruby Integer type. +type Int int + +// NilType is the object representation of NilClass +type NilType [0]byte + +// String is objects of the type String. +type String string + +// Nil is a constant that can be used as a Nil Value +var Nil NilType + +// MrbValue is a "value" internally in mruby. A "value" is what mruby calls +// basically anything in Ruby: a class, an object (instance), a variable, +// etc. +type MrbValue struct { + value C.mrb_value + state *C.mrb_state +} + +func init() { + Nil = [0]byte{} +} + +// SetInstanceVariable sets an instance variable on this value. +func (v *MrbValue) SetInstanceVariable(variable string, value *MrbValue) { + cs := C.CString(variable) + defer C.free(unsafe.Pointer(cs)) + C._go_mrb_iv_set(v.state, v.value, C.mrb_intern_cstr(v.state, cs), value.value) +} + +// GetInstanceVariable gets an instance variable on this value. +func (v *MrbValue) GetInstanceVariable(variable string) *MrbValue { + cs := C.CString(variable) + defer C.free(unsafe.Pointer(cs)) + return newValue(v.state, C._go_mrb_iv_get(v.state, v.value, C.mrb_intern_cstr(v.state, cs))) +} + +// Call calls a method with the given name and arguments on this +// value. +func (v *MrbValue) Call(method string, args ...Value) (*MrbValue, error) { + return v.call(method, args, nil) +} + +// CallBlock is the same as call except that it expects the last +// argument to be a Proc that will be passed into the function call. +// It is an error if args is empty or if there is no block on the end. +func (v *MrbValue) CallBlock(method string, args ...Value) (*MrbValue, error) { + if len(args) == 0 { + return nil, fmt.Errorf("args must be non-empty and have a proc at the end") + } + + n := len(args) + return v.call(method, args[:n-1], args[n-1]) +} + +func (v *MrbValue) call(method string, args []Value, block Value) (*MrbValue, error) { + var argv []C.mrb_value + var argvPtr *C.mrb_value + + mrb := &Mrb{v.state} + + if len(args) > 0 { + // Make the raw byte slice to hold our arguments we'll pass to C + argv = make([]C.mrb_value, len(args)) + for i, arg := range args { + argv[i] = arg.MrbValue(mrb).value + } + + argvPtr = &argv[0] + } + + var blockV *C.mrb_value + if block != nil { + val := block.MrbValue(mrb).value + blockV = &val + } + + cs := C.CString(method) + defer C.free(unsafe.Pointer(cs)) + + // If we have a block, we have to call a separate function to + // pass a block in. Otherwise, we just call it directly. + result := C._go_mrb_call( + v.state, + v.value, + C.mrb_intern_cstr(v.state, cs), + C.mrb_int(len(argv)), + argvPtr, + blockV) + + if exc := checkException(v.state); exc != nil { + return nil, exc + } + + return newValue(v.state, result), nil +} + +// IsDead tells you if an object has been collected by the GC or not. +func (v *MrbValue) IsDead() bool { + return C.ushort(C._go_isdead(v.state, v.value)) != 0 +} + +// MrbValue so that *MrbValue implements the "Value" interface. +func (v *MrbValue) MrbValue(*Mrb) *MrbValue { + return v +} + +// Mrb returns the Mrb state for this value. +func (v *MrbValue) Mrb() *Mrb { + return &Mrb{v.state} +} + +// GCProtect protects this value from being garbage collected. +func (v *MrbValue) GCProtect() { + C.mrb_gc_protect(v.state, v.value) +} + +// SetProcTargetClass sets the target class where a proc will be executed +// when this value is a proc. +func (v *MrbValue) SetProcTargetClass(c *Class) { + proc := C._go_mrb_proc_ptr(v.value) + proc.target_class = c.class +} + +// Type returns the ValueType of the MrbValue. See the constants table. +func (v *MrbValue) Type() ValueType { + if C._go_mrb_nil_p(v.value) == 1 { + return TypeNil + } + + return ValueType(C._go_mrb_type(v.value)) +} + +// Exception is a special type of value that represents an error +// and implements the Error interface. +type Exception struct { + *MrbValue + File string + Line int + Message string + Backtrace []string +} + +func (e *Exception) Error() string { + return e.Message +} + +func (e *Exception) String() string { + return e.Message +} + +//------------------------------------------------------------------- +// Type conversions to Go types +//------------------------------------------------------------------- + +// Array returns the Array value of this value. If the Type of the MrbValue +// is not a TypeArray, then this will panic. If the MrbValue has a +// `to_a` function, you must call that manually prior to calling this +// method. +func (v *MrbValue) Array() *Array { + return &Array{v} +} + +// Fixnum returns the numeric value of this object if the Type() is +// TypeFixnum. Calling this with any other type will result in undefined +// behavior. +func (v *MrbValue) Fixnum() int { + return int(C._go_mrb_fixnum(v.value)) +} + +// Float returns the numeric value of this object if the Type() is +// TypeFloat. Calling this with any other type will result in undefined +// behavior. +func (v *MrbValue) Float() float64 { + return float64(C._go_mrb_float(v.value)) +} + +// Hash returns the Hash value of this value. If the Type of the MrbValue +// is not a ValueTypeHash, then this will panic. If the MrbValue has a +// `to_h` function, you must call that manually prior to calling this +// method. +func (v *MrbValue) Hash() *Hash { + return &Hash{v} +} + +// String returns the "to_s" result of this value. +func (v *MrbValue) String() string { + value := C.mrb_obj_as_string(v.state, v.value) + result := C.GoString(C.mrb_string_value_ptr(v.state, value)) + return result +} + +// Class returns the *Class of a value. +func (v *MrbValue) Class() *Class { + mrb := &Mrb{v.state} + return newClass(mrb, C.mrb_class(v.state, v.value)) +} + +// SingletonClass returns the singleton class (a class isolated just for the +// scope of the object) for the given value. +func (v *MrbValue) SingletonClass() *Class { + mrb := &Mrb{v.state} + sclass := C._go_mrb_class_ptr(C.mrb_singleton_class(v.state, v.value)) + return newClass(mrb, sclass) +} + +//------------------------------------------------------------------- +// Native Go types implementing the Value interface +//------------------------------------------------------------------- + +// MrbValue returns the native MRB value +func (i Int) MrbValue(m *Mrb) *MrbValue { + return m.FixnumValue(int(i)) +} + +// MrbValue returns the native MRB value +func (NilType) MrbValue(m *Mrb) *MrbValue { + return m.NilValue() +} + +// MrbValue returns the native MRB value +func (s String) MrbValue(m *Mrb) *MrbValue { + return m.StringValue(string(s)) +} + +//------------------------------------------------------------------- +// Internal Functions +//------------------------------------------------------------------- + +func newExceptionValue(s *C.mrb_state) *Exception { + if s.exc == nil { + panic("exception value init without exception") + } + + arenaIndex := C.mrb_gc_arena_save(s) + defer C.mrb_gc_arena_restore(s, C.int(arenaIndex)) + + // Convert the RObject* to an mrb_value + value := C.mrb_obj_value(unsafe.Pointer(s.exc)) + + // Retrieve and convert backtrace to []string (avoiding reflection in Decode) + var backtrace []string + mrbBacktrace := newValue(s, C.mrb_exc_backtrace(s, value)).Array() + for i := 0; i < mrbBacktrace.Len(); i++ { + ln, _ := mrbBacktrace.Get(i) + backtrace = append(backtrace, ln.String()) + } + + // Extract file + line from first backtrace line + file := "Unknown" + line := 0 + if len(backtrace) > 0 { + fileAndLine := strings.Split(backtrace[0], ":") + if len(fileAndLine) >= 2 { + file = fileAndLine[0] + line, _ = strconv.Atoi(fileAndLine[1]) + } + } + + result := newValue(s, value) + return &Exception{ + MrbValue: result, + Message: result.String(), + File: file, + Line: line, + Backtrace: backtrace, + } +} + +func newValue(s *C.mrb_state, v C.mrb_value) *MrbValue { + return &MrbValue{ + state: s, + value: v, + } +} diff --git a/vendorlib/go-mruby/value_test.go b/vendorlib/go-mruby/value_test.go new file mode 100644 index 00000000..4102bcb7 --- /dev/null +++ b/vendorlib/go-mruby/value_test.go @@ -0,0 +1,305 @@ +package mruby + +import ( + "reflect" + "testing" +) + +func TestExceptionString_afterClose(t *testing.T) { + mrb := NewMrb() + _, err := mrb.LoadString(`clearly a syntax error`) + mrb.Close() + // This panics before the bug fix that this test tests + err.Error() +} + +func TestExceptionBacktrace(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + parser := NewParser(mrb) + defer parser.Close() + context := NewCompileContext(mrb) + context.SetFilename("hello.rb") + defer context.Close() + + parser.Parse(` + def do_error + raise "Exception" + end + + def hop1 + do_error + end + + def hop2 + hop1 + end + + hop2 + `, context) + + proc := parser.GenerateCode() + _, err := mrb.Run(proc, nil) + if err == nil { + t.Fatalf("expected exception") + } + + exc := err.(*Exception) + if exc.Message != "Exception" { + t.Fatalf("bad exception message: %s", exc.Message) + } + + if exc.File != "hello.rb" { + t.Fatalf("bad file: %s", exc.File) + } + + if exc.Line != 3 { + t.Fatalf("bad line: %d", exc.Line) + } + + if len(exc.Backtrace) != 4 { + t.Fatalf("bad backtrace: %#v", exc.Backtrace) + } +} + +func TestMrbValueCall(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`"foo"`) + if err != nil { + t.Fatalf("err: %s", err) + } + + _, err = value.Call("some_function_that_doesnt_exist") + if err == nil { + t.Fatalf("expected exception") + } + + result, err := value.Call("==", String("foo")) + if err != nil { + t.Fatalf("err: %s", err) + } + if result.Type() != TypeTrue { + t.Fatalf("bad type") + } +} + +func TestMrbValueCallBlock(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`"foo"`) + if err != nil { + t.Fatalf("err: %s", err) + } + + block, err := mrb.LoadString(`Proc.new { |_| "bar" }`) + if err != nil { + t.Fatalf("err: %s", err) + } + + result, err := value.CallBlock("gsub", String("foo"), block) + if err != nil { + t.Fatalf("err: %s", err) + } + if result.Type() != TypeString { + t.Fatalf("bad type") + } + if result.String() != "bar" { + t.Fatalf("bad: %s", result) + } +} + +func TestMrbValueValue(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + falseV := mrb.FalseValue() + if falseV.MrbValue(mrb) != falseV { + t.Fatal("should be the same") + } +} + +func TestMrbValueValue_impl(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + var _ Value = mrb.FalseValue() +} + +func TestMrbValueFixnum(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString("42") + if err != nil { + t.Fatalf("err: %s", err) + } + if value.Fixnum() != 42 { + t.Fatalf("bad fixnum") + } +} + +func TestMrbValueString(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + value, err := mrb.LoadString(`"foo"`) + if err != nil { + t.Fatalf("err: %s", err) + } + if value.String() != "foo" { + t.Fatalf("bad string") + } +} + +func TestMrbValueType(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + cases := []struct { + Input string + Expected ValueType + }{ + { + `false`, + TypeFalse, + }, + // TypeFree - Type of value after GC collection + { + `true`, + TypeTrue, + }, + { + `1`, + TypeFixnum, + }, + { + `:test`, + TypeSymbol, + }, + // TypeUndef - Internal value used by mruby for undefined things (instance vars etc) + // These all seem to get converted to exceptions before hitting userland + { + `1.1`, + TypeFloat, + }, + // TypeCptr + { + `Object.new`, + TypeObject, + }, + { + `Object`, + TypeClass, + }, + { + `module T; end; T`, + TypeModule, + }, + // TypeIClass + // TypeSClass + { + `Proc.new { 1 }`, + TypeProc, + }, + { + `[]`, + TypeArray, + }, + { + `{}`, + TypeHash, + }, + { + `"string"`, + TypeString, + }, + { + `1..2`, + TypeRange, + }, + { + `Exception.new`, + TypeException, + }, + // TypeFile + // TypeEnv + // TypeData + // TypeFiber + // TypeMaxDefine + { + `nil`, + TypeNil, + }, + } + + for _, c := range cases { + r, err := mrb.LoadString(c.Input) + if err != nil { + t.Fatalf("loadstring failed for case %#v: %s", c, err) + } + if cType := r.Type(); cType != c.Expected { + t.Fatalf("bad type: got %v, expected %v", cType, c.Expected) + } + } +} + +func TestIntMrbValue(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + var value Value = Int(42) + v := value.MrbValue(mrb) + if v.Fixnum() != 42 { + t.Fatalf("bad value") + } +} + +func TestStringMrbValue(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + var value Value = String("foo") + v := value.MrbValue(mrb) + if v.String() != "foo" { + t.Fatalf("bad value") + } +} + +func TestValueClass(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + val, err := mrb.ObjectClass().New() + if err != nil { + t.Fatalf("Error constructing object of type Object: %v", err) + } + + if !reflect.DeepEqual(val.Class(), mrb.ObjectClass()) { + t.Fatal("Class of value was not equivalent to constructed class") + } +} + +func TestValueSingletonClass(t *testing.T) { + mrb := NewMrb() + defer mrb.Close() + + fn := func(m *Mrb, self *MrbValue) (Value, Value) { + args := m.GetArgs() + return Int(args[0].Fixnum() + args[1].Fixnum()), nil + } + + mrb.TopSelf().SingletonClass().DefineMethod("add", fn, ArgsReq(2)) + + result, err := mrb.LoadString(`add(46, 2)`) + if err != nil { + t.Fatalf("Error parsing ruby code: %v", err) + } + + if result.String() != "48" { + t.Fatalf("Result %q was not equal to the target value of 48", result.String()) + } +} diff --git a/vendorlib/go-mruby/value_type.go b/vendorlib/go-mruby/value_type.go new file mode 100644 index 00000000..833ab3f0 --- /dev/null +++ b/vendorlib/go-mruby/value_type.go @@ -0,0 +1,58 @@ +package mruby + +// ValueType is an enum of types that a Value can be and is returned by +// Value.Type(). +type ValueType uint32 + +const ( + // TypeFalse is `false` + TypeFalse ValueType = iota + // TypeFree is ? + TypeFree + // TypeTrue is `true` + TypeTrue + // TypeFixnum is fixnums, or integers for this case. + TypeFixnum + // TypeSymbol is for entities in ruby that look like `:this` + TypeSymbol + // TypeUndef is a value internal to ruby for uninstantiated vars. + TypeUndef + // TypeFloat is any floating point number such as 1.2, etc. + TypeFloat + // TypeCptr is a void* + TypeCptr + // TypeObject is a standard ruby object, base class of most instantiated objects. + TypeObject + // TypeClass is the base class of all classes. + TypeClass + // TypeModule is the base class of all Modules. + TypeModule + // TypeIClass is ? + TypeIClass + // TypeSClass is ? + TypeSClass + // TypeProc are procs (concrete block definitons) + TypeProc + // TypeArray is [] + TypeArray + // TypeHash is { } + TypeHash + // TypeString is "" + TypeString + // TypeRange is (0..x) + TypeRange + // TypeException is raised when using the raise keyword + TypeException + // TypeFile is for objects of the File class + TypeFile + // TypeEnv is for getenv/setenv etc + TypeEnv + // TypeData is ? + TypeData + // TypeFiber is for members of the Fiber class + TypeFiber + // TypeMaxDefine is ? + TypeMaxDefine + // TypeNil is nil + TypeNil ValueType = 0xffffffff +)