Skip to content

Commit

Permalink
Merge pull request #107 from multiplay/call-params
Browse files Browse the repository at this point in the history
Improve method call parameter processing
  • Loading branch information
stevenh committed Dec 1, 2015
2 parents 526e6b2 + 1b2cc7f commit af6c304
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 51 deletions.
31 changes: 30 additions & 1 deletion reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ func (abs _abcStruct) Func2IntVariadic(s string, a ...int) string {
return fmt.Sprintf("%v:%v", s, t)
}

func (abs _abcStruct) Func2IntArrayVariadic(s string, a ...[]int) string {
t := 0
for _, i := range a {
for _, j := range i {
t += j
}
}
return fmt.Sprintf("%v:%v", s, t)
}

type _mnoStruct struct {
Ghi string
}
Expand Down Expand Up @@ -234,6 +244,10 @@ func Test_reflectStruct(t *testing.T) {
abc.Func1Int(1);
`, 2)

test(`
abc.Func1Int(0x01 & 0x01);
`, 2)

test(`raise:
abc.Func1Int(1.1);
`, "converting float64 to int would cause loss of precision")
Expand Down Expand Up @@ -294,6 +308,22 @@ func Test_reflectStruct(t *testing.T) {
test(`
abc.Func2IntVariadic("test", 1, 2);
`, "test:3")

test(`
abc.Func2IntVariadic("test", [1, 2]);
`, "test:3")

test(`
abc.Func2IntArrayVariadic("test", [1, 2]);
`, "test:3")

test(`
abc.Func2IntArrayVariadic("test", [1, 2], [3, 4]);
`, "test:10")

test(`
abc.Func2IntArrayVariadic("test", [[1, 2], [3, 4]]);
`, "test:10")
}
})
}
Expand Down Expand Up @@ -535,7 +565,6 @@ func Test_reflectArray(t *testing.T) {
is(abc[len(abc)-1], false)
// ...
}

// []int32
{
abc := make([]int32, 4)
Expand Down
216 changes: 166 additions & 50 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package otto

import (
"errors"
"fmt"
"math"
"reflect"
"sync"

Expand Down Expand Up @@ -191,65 +193,146 @@ func (self *_runtime) safeToValue(value interface{}) (Value, error) {
return result, err
}

// safeNumericConvert converts numeric parameter val from js to the type that the function fn expects if its safe to do so.
// This allows literals (int64) and the general js numeric form (float64) to be passed as parameters to go functions easily.
func safeNumericConvert(fn reflect.Type, i int, val interface{}) reflect.Value {
switch val.(type) {
default:
// Not a supported conversion
return reflect.ValueOf(val)
case float64, int64:
// What type is the func expecting?
var ptype reflect.Type
switch {
case fn.IsVariadic() && fn.NumIn() <= i+1:
// This argument is variadic so use the variadics element type.
ptype = fn.In(fn.NumIn() - 1).Elem()
case fn.NumIn() > i:
ptype = fn.In(i)
}
// convertNumeric converts numeric parameter val from js to that of type t if it is safe to do so, otherwise it panics.
// This allows literals (int64), bitwise values (int32) and the general form (float64) of javascript numerics to be passed as parameters to go functions easily.
func convertNumeric(val reflect.Value, t reflect.Type) reflect.Value {
if val.Kind() == t.Kind() {
return val
}

if f64, ok := val.(float64); ok {
switch ptype.Kind() {
case reflect.Float64:
return reflect.ValueOf(val)
case reflect.Float32:
if reflect.Zero(ptype).OverflowFloat(f64) {
// Not safe to convert
return reflect.ValueOf(val)
}
if val.Kind() == reflect.Interface {
val = reflect.ValueOf(val.Interface())
}

return reflect.ValueOf(val).Convert(ptype)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i64 := int64(f64)
if float64(i64) != f64 {
// Not safe to convert
return reflect.ValueOf(val)
}
switch val.Kind() {
case reflect.Float32, reflect.Float64:
f64 := val.Float()
switch t.Kind() {
case reflect.Float64:
return reflect.ValueOf(f64)
case reflect.Float32:
if reflect.Zero(t).OverflowFloat(f64) {
panic("converting float64 to float32 would overflow")
}

// The float represents an integer
val = i64
default:
// Not a supported conversion
return reflect.ValueOf(val)
return val.Convert(t)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i64 := int64(f64)
if float64(i64) != f64 {
panic(fmt.Sprintf("converting %v to %v would cause loss of precision", val.Type(), t))
}

// The float represents an integer
val = reflect.ValueOf(i64)
default:
panic(fmt.Sprintf("cannot convert %v to %v", val.Type(), t))
}
}

i64 := val.(int64)
switch ptype.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
if !reflect.Zero(ptype).OverflowInt(i64) {
return reflect.ValueOf(val).Convert(ptype)
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i64 := val.Int()
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if reflect.Zero(t).OverflowInt(i64) {
panic(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))
}
return val.Convert(t)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if i64 > 0 && !reflect.Zero(ptype).OverflowUint(uint64(i64)) {
return reflect.ValueOf(val).Convert(ptype)
if i64 < 0 {
panic(fmt.Sprintf("converting %v to %v would underflow", val.Type(), t))
}
if reflect.Zero(t).OverflowUint(uint64(i64)) {
panic(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))
}
return val.Convert(t)
}

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u64 := val.Uint()
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if u64 > math.MaxInt64 || reflect.Zero(t).OverflowInt(int64(u64)) {
panic(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))
}
return val.Convert(t)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if reflect.Zero(t).OverflowUint(u64) {
panic(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))
}
return val.Convert(t)
}
}

// Not a supported conversion
return reflect.ValueOf(val)
panic(fmt.Sprintf("unsupported type %v for numeric conversion", val.Type()))
}

// callParamConvert converts request val to type t if possible.
// If the conversion fails due to overflow or type miss-match then it panics.
// If no conversion is known then the original value is returned.
func callParamConvert(val reflect.Value, t reflect.Type) reflect.Value {
if val.Kind() == reflect.Interface {
val = reflect.ValueOf(val.Interface())
}

switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
if val.Kind() == t.Kind() {
// Types already match
return val
}
return convertNumeric(val, t)
case reflect.Slice:
if val.Kind() != reflect.Slice {
// Conversion from none slice type to slice not possible
panic(fmt.Sprintf("cannot use %v as type %v", val, t))
}
default:
// No supported conversion
return val
}

elemType := t.Elem()
switch elemType.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Slice:
// Attempt to convert to slice of the type t
s := reflect.MakeSlice(reflect.SliceOf(elemType), val.Len(), val.Len())
for i := 0; i < val.Len(); i++ {
s.Index(i).Set(callParamConvert(val.Index(i), elemType))
}

return s
}

// Not a slice type we can convert
return val
}

// callSliceRequired returns true if CallSlice is required instead of Call.
func callSliceRequired(param reflect.Type, val reflect.Value) bool {
vt := val.Type()
for param.Kind() == reflect.Slice {
if val.Kind() == reflect.Interface {
val = reflect.ValueOf(val.Interface())
vt = val.Type()
}

if vt.Kind() != reflect.Slice {
return false
}

vt = vt.Elem()
if val.Kind() != reflect.Invalid {
if val.Len() > 0 {
val = val.Index(0)
} else {
val = reflect.Value{}
}
}
param = param.Elem()
}

return true
}

func (self *_runtime) toValue(value interface{}) Value {
Expand Down Expand Up @@ -278,13 +361,46 @@ func (self *_runtime) toValue(value interface{}) Value {
case reflect.Func:
// TODO Maybe cache this?
return toValue_object(self.newNativeFunction("", func(call FunctionCall) Value {
in := make([]reflect.Value, len(call.ArgumentList))
argsCount := len(call.ArgumentList)
in := make([]reflect.Value, argsCount)
t := value.Type()
callSlice := false
paramsCount := t.NumIn()
lastParam := paramsCount - 1
lastArg := argsCount - 1
isVariadic := t.IsVariadic()
for i, value := range call.ArgumentList {
in[i] = safeNumericConvert(t, i, value.export())
var paramType reflect.Type
if isVariadic && i == lastArg && argsCount == paramsCount {
// Variadic functions last parameter and parameter numbers match incoming args
paramType = t.In(lastArg)
val := reflect.ValueOf(value.export())
callSlice = callSliceRequired(paramType, val)
if callSlice {
in[i] = callParamConvert(reflect.ValueOf(value.export()), paramType)
continue
}
}

if i >= lastParam {
if isVariadic {
paramType = t.In(lastParam).Elem()
} else {
paramType = t.In(lastParam)
}
} else {
paramType = t.In(i)
}
in[i] = callParamConvert(reflect.ValueOf(value.export()), paramType)
}

var out []reflect.Value
if callSlice {
out = value.CallSlice(in)
} else {
out = value.Call(in)
}

out := value.Call(in)
l := len(out)
switch l {
case 0:
Expand Down

0 comments on commit af6c304

Please sign in to comment.