diff --git a/muon.go b/muon.go index 383a96f..90c5f32 100644 --- a/muon.go +++ b/muon.go @@ -1,6 +1,8 @@ package muon import ( + "encoding/json" + "fmt" "net" "net/http" "reflect" @@ -90,12 +92,28 @@ func (w *Window) Bind(name string, function interface{}) { f.ReturnTypes = make([]reflect.Type, t.NumOut()) for i := 0; i < t.NumOut(); i++ { - f.ParamTypes[i] = t.Out(i) + f.ReturnTypes[i] = t.Out(i) } addFunctionToView(w.view, name, w.makeIPCCallback(f)) } +// Eval evaluates a given JavaScript string in the given Window view. `ret` is necessary for JSON serialization if an object is returned. +func (w *Window) Eval(js string, ret reflect.Type) (interface{}, error) { + us := UlCreateString(js) + defer UlDestroyString(us) + + ref := UlViewEvaluateScript(w.view, us) + ctx := UlViewGetJSContext(w.view) + val, err := fromJSValue(ctx, ref, ret) + + if err != nil { + return nil, err + } + + return val.Interface(), nil +} + func (w *Window) makeIPCCallback(f *ipf) func(JSContextRef, JSObjectRef, JSObjectRef, uint, []JSValueRef, []JSValueRef) JSValueRef { return func( ctx JSContextRef, @@ -108,39 +126,128 @@ func (w *Window) makeIPCCallback(f *ipf) func(JSContextRef, JSObjectRef, JSObjec params := make([]reflect.Value, argumentCount) for i := uint(0); i < argumentCount; i++ { - switch JSValueGetType(ctx, arguments[i]) { - case KJSTypeBoolean: - params[i] = reflect.ValueOf(JSValueToBoolean(ctx, arguments[i])) - case KJSTypeNumber: - params[i] = reflect.ValueOf(JSValueToNumber(ctx, arguments[i], exception)) - case KJSTypeString: - ref := JSValueToStringCopy(ctx, arguments[i], exception) - params[i] = reflect.ValueOf(*(*string)(unsafe.Pointer(&ref))) - JSStringRelease(ref) - default: - panic("Not implemented!") // TODO: Object JSON + val, err := fromJSValue(ctx, arguments[i], f.ParamTypes[i]) + + if err != nil { + panic(err) } + + params[i] = val } val := f.Function.Call(params) if len(val) > 1 { - panic("Not implemented!") // TODO: more than 1 return type + panic("Javascript does not support more than 1 return value!") } - switch val[0].Kind() { - case reflect.Float64: - return JSValueMakeNumber(ctx, val[0].Float()) - case reflect.Bool: - return JSValueMakeBoolean(ctx, val[0].Bool()) - case reflect.String: - return JSValueMakeString(ctx, JSStringCreateWithUTF8CString(val[0].String())) - default: - panic("Not implemented!") // TODO: Object JSON - } + return toJSValue(ctx, val[0]) + } } +func fromJSValue(ctx JSContextRef, value JSValueRef, rtype reflect.Type) (reflect.Value, error) { + var rv reflect.Value + var err error + + if JSValueIsArray(ctx, value) { + l := JSStringCreateWithUTF8CString("length") + defer JSStringRelease(l) + + obj := *(*JSObjectRef)(unsafe.Pointer(&value)) + + prop := JSObjectGetProperty(ctx, obj, l, nil) + length := int(JSValueToNumber(ctx, prop, nil)) + values := reflect.MakeSlice(rtype, length, length) + + for i := 0; i < length; i++ { + ref := JSObjectGetPropertyAtIndex(ctx, obj, uint32(i), nil) + + val, err := fromJSValue(ctx, ref, rtype.Elem()) + + if err != nil { + return reflect.Value{}, err + } + + values.Index(i).Set(val) + } + + return values, nil + } + + switch JSValueGetType(ctx, value) { + case KJSTypeBoolean: + rv = reflect.ValueOf(JSValueToBoolean(ctx, value)) + case KJSTypeNumber: + rv = reflect.ValueOf(JSValueToNumber(ctx, value, nil)) + case KJSTypeString: + ref := JSValueToStringCopy(ctx, value, nil) + rv = reflect.ValueOf(fromJSString(ref)) + JSStringRelease(ref) + case KJSTypeObject: + ref := JSValueCreateJSONString(ctx, value, 0, nil) + obj := reflect.New(rtype).Interface() + + if err = json.Unmarshal([]byte(fromJSString(ref)), &obj); err == nil { + rv = reflect.Indirect(reflect.ValueOf(obj)) + } + + JSStringRelease(ref) + default: + panic("Not implemented") + } + + return rv, err +} + +func fromJSString(str JSStringRef) string { + len := JSStringGetMaximumUTF8CStringSize(str) + data := make([]byte, len) + written := JSStringGetUTF8CString(str, data, len) + + return string(data[:written-1]) +} + +func toJSValue(ctx JSContextRef, value reflect.Value) JSValueRef { + var jsv JSValueRef + var err error + + switch value.Kind() { + case reflect.Float64: + jsv = JSValueMakeNumber(ctx, value.Float()) + case reflect.Bool: + jsv = JSValueMakeBoolean(ctx, value.Bool()) + case reflect.String: + str := JSStringCreateWithUTF8CString(value.String()) + jsv = JSValueMakeString(ctx, str) + JSStringRelease(str) + case reflect.Ptr: + return toJSValue(ctx, reflect.Indirect(value)) + case reflect.Struct: + if json, err := json.Marshal(value.Interface()); err == nil { + str := JSStringCreateWithUTF8CString(string(json)) + jsv = JSValueMakeFromJSONString(ctx, str) + JSStringRelease(str) + } + case reflect.Slice, reflect.Array: + rets := make([]JSValueRef, value.Len()) + + for i := 0; i < value.Len(); i++ { + rets[i] = toJSValue(ctx, value.Index(i)) + } + arr := JSObjectMakeArray(ctx, uint(len(rets)), rets, nil) + jsv = *(*JSValueRef)(unsafe.Pointer(&arr)) + default: + panic("Not implemented!") + } + + if err != nil { + fmt.Println(err.Error()) + return JSValueMakeNull(ctx) + } + return jsv +} + func addFunctionToView(view ULView, name string, callback JSObjectCallAsFunctionCallback) { ctx := UlViewGetJSContext(view) gobj := JSContextGetGlobalObject(ctx) @@ -155,8 +262,8 @@ func addFunctionToView(view ULView, name string, callback JSObjectCallAsFunction JSObjectSetProperty(ctx, gobj, fn, val, KJSPropertyAttributeNone, []JSValueRef{}) } -func resizeCallback(user_data unsafe.Pointer, width uint32, height uint32) { - overlay := *(*ULOverlay)(user_data) +func resizeCallback(userData unsafe.Pointer, width uint32, height uint32) { + overlay := *(*ULOverlay)(userData) UlOverlayResize(overlay, width, height) } diff --git a/muon_test.go b/muon_test.go new file mode 100644 index 0000000..57aed87 --- /dev/null +++ b/muon_test.go @@ -0,0 +1,97 @@ +package muon + +import ( + "net/http" + "reflect" + "testing" +) + +type testObject struct { + S1 string + F1 float64 + B1 bool +} + +func TestComplexType(t *testing.T) { + cfg := &Config{ + Height: 1, + Width: 1, + } + + m := New(cfg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + + m.Bind("test", func(to *testObject) *testObject { + return &testObject{ + S1: to.S1 + " World!", + F1: to.F1 + 1, + B1: !to.B1, + } + }) + + go func() { + m.Start() + }() + + res, err := m.Eval(`test({S1: "Hello,", F1: 9000, B1: false})`, reflect.TypeOf(&testObject{})) + + if err != nil { + t.Error(err) + } + + to := res.(*testObject) + + if to.S1 != "Hello, World!" { + t.Errorf("to.S1 was not correct, got %s", to.S1) + } + + if to.F1 != 9001 { + t.Errorf("to.F1 was under 9000, got %f", to.F1) + } + + if !to.B1 { + t.Errorf("to.B1 was not True, got false") + } +} + +func TestArrayType(t *testing.T) { + cfg := &Config{ + Height: 1, + Width: 1, + } + + m := New(cfg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + + m.Bind("test", func(strings []string) []float64 { + if strings[0] != "Hello" { + t.Errorf("strings[0] was not Hello, got %s", strings[0]) + } + if strings[1] != "World!" { + t.Errorf("strings[1] was not World!, got %s", strings[1]) + } + return []float64{1, 2, 3} + }) + + go func() { + m.Start() + }() + + res, err := m.Eval(`test(["Hello","World!"])`, reflect.TypeOf([]float64{})) + + if err != nil { + t.Error(err) + } + + nums := res.([]float64) + + if nums[0] != 1 { + t.Errorf("nums[0] was not 1, got %f", nums[0]) + } + + if nums[1] != 2 { + t.Errorf("nums[1] was not 2, got %f", nums[1]) + } + + if nums[2] != 3 { + t.Errorf("nums[2] was not 3, got %f", nums[2]) + } +}