From 7d632c50217800c1aa456f784ac4a7f8ab0829f7 Mon Sep 17 00:00:00 2001 From: Star <> Date: Sat, 17 Feb 2024 12:55:08 +0800 Subject: [PATCH] first --- .gitignore | 25 +-- bridge.go | 422 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 16 ++ gojs.go | 99 ++++++++++++ gojs_test.go | 149 ++++++++++++++++++ 5 files changed, 688 insertions(+), 23 deletions(-) create mode 100644 bridge.go create mode 100644 go.mod create mode 100644 gojs.go create mode 100644 gojs_test.go diff --git a/.gitignore b/.gitignore index adf8f72..9ff6ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,2 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - +.* +/go.sum diff --git a/bridge.go b/bridge.go new file mode 100644 index 0000000..60b4ac9 --- /dev/null +++ b/bridge.go @@ -0,0 +1,422 @@ +package gojs + +import ( + "apigo.cloud/git/apigo/plugin" + "errors" + "fmt" + "apigo.cloud/git/apigo/qjs" + "github.com/ssgo/log" + "github.com/ssgo/u" + "reflect" + "strings" +) + +func MakeJsValue(ctx *plugin.Context, in interface{}, inArray bool) quickjs.Value { + return _makeJsValue(ctx, in, 0, "", "", inArray) +} + +func MakeJsValueForPlugin(ctx *plugin.Context, in interface{}, pluginName string, inArray bool) quickjs.Value { + return _makeJsValue(ctx, in, 0, "", pluginName, inArray) +} + +func makeLowerCaseStartWord(str string) string { + if len(str) > 0 && str[0] >= 'A' && str[0] <= 'Z' { + return string(str[0]+32) + str[1:] + } + return str +} + +func _makeJsValue(ctx *plugin.Context, in interface{}, n int, key string, pluginName string, inArray bool) quickjs.Value { + if n > 100 { + return quickjs.Value{} + } + jsCtx, jsCtxOk := ctx.GetInject("*quickjs.Context").(*quickjs.Context) + if !jsCtxOk { + return quickjs.Value{} + } + + if err, isErr := in.(error); isErr { + return jsCtx.ThrowError(err) + } + + var v reflect.Value + //var ov reflect.To + if inV, ok := in.(reflect.Value); ok { + v = inV + } else { + //ov = reflect.ValueOf(in) + v = reflect.ValueOf(in) + } + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Bool: + return jsCtx.Bool(v.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + return jsCtx.Int32(int32(v.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: + return jsCtx.Uint32(uint32(v.Uint())) + case reflect.Int64: + return jsCtx.Int64(v.Int()) + case reflect.Uint64: + return jsCtx.Int64(int64(v.Uint())) + case reflect.String: + return jsCtx.String(v.String()) + case reflect.Float32, reflect.Float64: + //return jsCtx.String(u.String(v.Float())) + return jsCtx.Float64(v.Float()) + case reflect.Slice: + if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 { + originBuf := v.Bytes() + if len(originBuf) == 0 { + // 空的ArrayBuffer用空的Array代替 + arr := jsCtx.Array() + arrValue := arr.ToValue() + if inArray { + if freeJsValues, ok := ctx.GetData("_freeJsValues").(*[]quickjs.Value); ok { + *freeJsValues = append(*freeJsValues, arrValue) + } + } + return arrValue + } + //if freeJsValues, ok := ctx.GetData("_freeJsValues").(*[]quickjs.Value); ok { + // *freeJsValues = append(*freeJsValues, bufValue) + //} + return jsCtx.ArrayBuffer(originBuf) + //arr := jsCtx.Array() + //for _, b := range v.Bytes() { + // o := jsCtx.Uint32(uint32(b)) + // arr.Push(o) + //} + //if inArray { + // serverLogger.Error("==>2", n, key, "arr", "==") + // freeJsValues := ctx.GetData("_freeJsValues").(*[]quickjs.Value) + // *freeJsValues = append(*freeJsValues, arr.ToValue()) + //} + //return arr.ToValue() + } else { + arr := jsCtx.Array() + for i := 0; i < v.Len(); i++ { + o := _makeJsValue(ctx, v.Index(i), n+1, "", pluginName, true) + if o.IsObject() || o.IsArray() || o.IsMap() { + if freeJsValues, ok := ctx.GetData("_freeJsValues").(*[]quickjs.Value); ok { + *freeJsValues = append(*freeJsValues, o) + } + } + arr.Push(o) + } + arrValue := arr.ToValue() + if inArray { + if freeJsValues, ok := ctx.GetData("_freeJsValues").(*[]quickjs.Value); ok { + *freeJsValues = append(*freeJsValues, arrValue) + } + } + return arrValue + } + case reflect.Struct: + o := jsCtx.Object() + var structInfo *u.StructInfo + if v.CanAddr() { + structInfo = u.FlatStruct(v.Addr()) + } else { + structInfo = u.FlatStruct(v) + } + for k2, v2 := range structInfo.Values { + o.Set(makeLowerCaseStartWord(k2), _makeJsValue(ctx, v2, n+1, k2, pluginName, false)) + } + for k2, v2 := range structInfo.MethodValues { + o.Set(makeLowerCaseStartWord(k2), _makeJsValue(ctx, v2, n+1, k2, pluginName, false)) + } + return o + case reflect.Map: + o := jsCtx.Object() + for _, k2 := range v.MapKeys() { + k2s := u.String(u.FinalValue(k2).Interface()) + if len(k2s) > 0 && k2s[0] != '_' { + o.Set(k2s, _makeJsValue(ctx, v.MapIndex(k2), n+1, k2s, pluginName, false)) + } + } + return o + case reflect.Func: + t := v.Type() + return jsCtx.Function(func(js *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + defer func() { + if err := recover(); err != nil { + ctx.GetInject("*log.Logger").(*log.Logger).Error(u.String(err), "func", key) + } + }() + needArgs := make([]reflect.Type, 0) + realArgs := make([]reflect.Value, t.NumIn()) + needArgsIndex := map[int]int{} + for i := 0; i < t.NumIn(); i++ { + inTypeString := t.In(i).String() + if inTypeString == "*plugin.Context" { + realArgs[i] = reflect.ValueOf(ctx) + continue + //} else if inTypeString == "plugin.Config" { + // pluginConf := GetPluginConfig(pluginName) + // realArgs[i] = reflect.ValueOf(pluginConf) + // continue + } else if injectObject := ctx.GetInject(inTypeString); injectObject != nil { + realArgs[i] = reflect.ValueOf(injectObject) + continue + } + + if !realArgs[i].IsValid() { + needArgs = append(needArgs, t.In(i)) + needArgsIndex[len(needArgsIndex)] = i + } + } + + //if len(args) < len(needArgs) { + // return js.ThrowError(errors.New(fmt.Sprintf("call %s no enough args, need %d, given %d", key, len(needArgs), len(args)))) + //} + for i, needArgType := range needArgs { + var argValue reflect.Value + isLastVariadicArg := false + if v.Type().IsVariadic() && needArgsIndex[i] == len(realArgs)-1 { + // 可变参数函数的最后一个使用成员类型 + isLastVariadicArg = true + argValue = reflect.New(needArgType.Elem()) + } else { + argValue = reflect.New(needArgType) + } + if i > len(args)-1 { + if !isLastVariadicArg && needArgType.Kind() != reflect.Interface && needArgType.Kind() != reflect.Ptr { + return js.ThrowError(errors.New(fmt.Sprintf("call %s no enough args, need %d, given %d", key, len(needArgs), len(args)))) + } + realArgs[needArgsIndex[i]] = reflect.ValueOf(argValue.Interface()).Elem() + } else if needArgType.Kind() == reflect.Func { + jsFunc := args[i] + funcType := needArgType + argValue = reflect.MakeFunc(funcType, func(goArgs []reflect.Value) []reflect.Value { + ins := make([]quickjs.Value, 0) + for _, goArg := range goArgs { + ins = append(ins, MakeJsValue(ctx, goArg.Interface(), false)) + } + outs := make([]reflect.Value, 0) + for j := 0; j < funcType.NumOut(); j++ { + outs = append(outs, reflect.New(funcType.Out(j)).Elem()) + } + jsResult := jsCtx.Invoke(jsFunc, jsCtx.Null(), ins...) + if !jsResult.IsUndefined() && len(outs) > 0 { + out0P := outs[0].Interface() + u.Convert(MakeFromJsValue(jsResult), out0P) + outs[0] = reflect.ValueOf(out0P).Elem() + } + return outs + }) + realArgs[needArgsIndex[i]] = argValue + } else { + //fmt.Println(222, len(args), len(needArgs), reflect.TypeOf(MakeFromJsValue(args[i])).String(), reflect.TypeOf(argValue).String(), MakeFromJsValue(args[i]), argValue) + argValueP := argValue.Interface() + ff1 := MakeFromJsValue(args[i]) + u.Convert(ff1, argValueP) + argValue = reflect.ValueOf(argValueP).Elem() + realArgs[needArgsIndex[i]] = argValue + } + } + + // 处理可变参数 + if len(args) > len(needArgs) { + lastArgType := needArgs[len(needArgs)-1] + if lastArgType.Kind() == reflect.Slice && lastArgType.Elem().Kind() != reflect.Uint8 { + //lastRealArgIndex := needArgsIndex[len(needArgs)-1] + //fmt.Println(222221, realArgs[lastRealArgIndex].Type().String()) + for i := len(needArgs); i < len(args); i++ { + argValue := reflect.New(lastArgType.Elem()).Interface() + //fmt.Println(22222, len(args), len(needArgs), reflect.TypeOf(MakeFromJsValue(args[i])).String(), reflect.TypeOf(argValue).String(), MakeFromJsValue(args[i]), argValue) + u.Convert(MakeFromJsValue(args[i]), argValue) + realArgs = append(realArgs, reflect.ValueOf(argValue).Elem()) + //realArgs[lastRealArgIndex] = reflect.Append(realArgs[lastRealArgIndex], reflect.ValueOf(argValue).Elem()) + } + } + } + + outValues := v.Call(realArgs) + outs := make([]reflect.Value, 0) + for _, outValue := range outValues { + if outValue.Type().String() == "error" { + if !outValue.IsNil() { + // 抛出异常 + return _makeJsValue(ctx, outValue.Interface(), n+1, "", pluginName, false) + } + // 忽略error参数 + continue + } + outs = append(outs, outValue) + } + if len(outs) == 1 { + //fmt.Println("**out, ", u.JsonP(outs[0].Interface()), "**") + out := _makeJsValue(ctx, outs[0].Interface(), n+1, "", pluginName, false) + return out + } else if len(outs) > 1 { + arr := jsCtx.Array() + for _, outValue := range outs { + //r.Set(int64(i), _makeJsValue(ctx, outValue.Interface(), n+1, "", pluginName, inReturn)) + o := _makeJsValue(ctx, outValue.Interface(), n+1, "", pluginName, true) + if o.IsObject() || o.IsArray() || o.IsMap() { + if freeJsValues, ok := ctx.GetData("_freeJsValues").(*[]quickjs.Value); ok { + *freeJsValues = append(*freeJsValues, o) + } + } + arr.Push(o) + } + return arr.ToValue() + } else { + return jsCtx.Null() + } + }) + case reflect.Invalid: + return jsCtx.Null() + default: + return jsCtx.String(u.String(in)) + } +} + +func MakeFromJsValue(in quickjs.Value) interface{} { + return _makeFromJsValue(in, 0) +} + +func _makeFromJsValue(in quickjs.Value, n int) interface{} { + if n > 100 { + return quickjs.Value{} + } + if in.IsBool() { + return in.Bool() + } else if in.IsBigInt() { + return in.Int64() + } else if in.IsBigFloat() || in.IsBigDecimal() { + return in.Float64() + } else if in.IsNumber() { + if strings.ContainsRune(in.String(), '.') { + return in.Float64() + } else { + return in.Int64() + } + } else if in.IsString() { + return in.String() + } else if in.IsByteArray() { + buf, err := in.ToByteArray(uint(in.ByteLen())) + if err != nil { + return []byte{} + } + return buf + } else if in.IsArray() { + a := make([]interface{}, 0) + //isBytes := true + //isChars := true + arr := in.ToArray() + for i := int64(0); i < arr.Len(); i++ { + if v, err := arr.Get(i); err == nil { + value := _makeFromJsValue(v, n+1) + a = append(a, value) + //value.Free() + v.Free() + //_freeJsValues = append(_freeJsValues, v) + } + + //v := in.GetIdx(int64(i)) + //value := _makeFromJsValue(v, n+1) + ////if isChars { + //// // 判断是否 Uint16Array 或 Uint8Array + //// if !v.IsNumber() { + //// isBytes = false + //// isChars = false + //// } + ////} + ////if isBytes { + //// // 判断是否 Uint8Array + //// if v.Int64() > 255 { + //// isBytes = false + //// } + ////} + //a = append(a, value) + //v.Free() + } + //arr.Free() + //_freeJsValues = append(_freeJsValues, in) + + //if isBytes { + // buf := make([]byte, len(a)) + // for i, arrV := range a { + // buf[i] = byte(u.Uint(arrV)) + // } + // return buf + //} + //if isChars { + // buf := make([]rune, len(a)) + // for i, arrV := range a { + // buf[i] = rune(u.Uint(arrV)) + // } + // return buf + //} + return a + } else if in.IsFunction() { + //reflect.Func + return nil + } else if in.IsObject() { + o := map[string]interface{}{} + keys, _ := in.PropertyNames() + isBytes := true + isChars := true + isArray := true + arr := make([]interface{}, 0) + for i, k := range keys { + if k == "prototype" { + continue + } + v := in.Get(k) + value := _makeFromJsValue(v, n+1) + // 判断是否数组类对象 + if (i == 0 && k != "0") || (i > 0 && u.Int(k) != i) { + isBytes = false + isChars = false + isArray = false + } + if isArray { + if isChars { + // 判断是否 Uint16Array 或 Uint8Array + if !v.IsNumber() { + isBytes = false + isChars = false + } + } + if isBytes { + // 判断是否 Uint8Array + if v.Int64() > 255 { + isBytes = false + } + } + arr = append(arr, value) + } + o[k] = value + v.Free() + } + + if isBytes { + buf := make([]byte, len(arr)) + for i, arrV := range arr { + buf[i] = byte(u.Uint(arrV)) + } + return buf + } + if isChars { + buf := make([]rune, len(arr)) + for i, arrV := range arr { + buf[i] = rune(u.Uint(arrV)) + } + return buf + } + if isArray { + return arr + } + return o + } else if in.IsNull() || in.IsUndefined() || in.IsUninitialized() { + return nil + } else { + return in.String() + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c14dfd3 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module apigo.cloud/git/apigo/gojs + +go 1.17 + +require ( + apigo.cloud/git/apigo/plugin v1.0.1 + apigo.cloud/git/apigo/qjs v0.0.1 + github.com/ssgo/log v0.6.11 + github.com/ssgo/u v0.6.11 +) + +require ( + github.com/ssgo/config v0.6.11 // indirect + github.com/ssgo/standard v0.6.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/gojs.go b/gojs.go new file mode 100644 index 0000000..c5ba58f --- /dev/null +++ b/gojs.go @@ -0,0 +1,99 @@ +package gojs + +import ( + "apigo.cloud/git/apigo/plugin" + "apigo.cloud/git/apigo/qjs" + "errors" + "github.com/ssgo/log" + "github.com/ssgo/u" + "regexp" + "strings" +) + +var pluginNameMatcher = regexp.MustCompile(`(\w+?)\.`) + +func Run(code string, globals map[string]interface{}, logger *log.Logger) (out interface{}) { + // 初始化JS虚拟机 + freeJsValues := make([]quickjs.Value, 0) + rt := quickjs.NewRuntime() + jsCtx := rt.NewContext() + defer func() { + if err := recover(); err != nil { + logger.Error(u.String(err)) + } + for _, v := range freeJsValues { + v.Free() + } + freeJsValues = make([]quickjs.Value, 0) + jsCtx.Close() + rt.Close() + }() + + tryPlugins := map[string]bool{} + for _, m := range pluginNameMatcher.FindAllStringSubmatch(code, 1024) { + tryPlugins[m[1]] = true + } + + goCtx := plugin.NewContext(map[string]interface{}{ + "*log.Logger": logger, + "*quickjs.Context": jsCtx, + }) + goCtx.SetData("_freeJsValues", &freeJsValues) + + for _, plg := range plugin.List() { + if tryPlugins[plg.Id] { + jsCtx.Globals().Set(plg.Id, MakeJsValueForPlugin(goCtx, plg.Objects, plg.Id, false)) + if plg.JsCode != "" { + if result, err := jsCtx.Eval(plg.JsCode); err != nil { + stack := GetJSError(err, plg.JsCode) + logger.Error(err.Error(), "stack", stack) + } else { + result.Free() + } + } + } + } + + // 全局变量 + if globals != nil { + for k, obj := range globals { + jsCtx.Globals().Set(k, MakeJsValue(goCtx, obj, false)) + } + } + + // 运行API + if r, err := jsCtx.Eval("(function(){" + code + "})()"); err == nil { + result := MakeFromJsValue(r) + r.Free() + return result + } else { + // 检查错误 + stack := GetJSError(err, code) + logger.Error(err.Error(), "stack", stack) + return nil + } +} + +var jsErrorCodeMatcher = regexp.MustCompile(`code:(\d+)`) + +func GetJSError(err error, code string) string { + if err != nil { + var jsErr *quickjs.Error + if errors.As(err, &jsErr) { + // 在错误信息中加入代码 + codeLines := strings.Split(code, "\n") + return jsErrorCodeMatcher.ReplaceAllStringFunc(jsErr.Stack, func(s2 string) string { + errorLineNumber := u.Int(jsErrorCodeMatcher.FindStringSubmatch(s2)[1]) + errorLine := "" + if len(codeLines) >= errorLineNumber { + errorLine = codeLines[errorLineNumber-1] + } + return s2 + " ```" + errorLine + "```" + }) + } else { + return err.Error() + } + } else { + return "" + } +} diff --git a/gojs_test.go b/gojs_test.go new file mode 100644 index 0000000..9b1c827 --- /dev/null +++ b/gojs_test.go @@ -0,0 +1,149 @@ +package gojs_test + +import ( + "apigo.cloud/git/apigo/gojs" + "apigo.cloud/git/apigo/plugin" + "fmt" + "github.com/ssgo/log" + "github.com/ssgo/u" + "runtime" + "testing" + "time" +) + + +type Object struct { + id string +} + +func (obj *Object) GetId() string { + return obj.id +} + +func init() { + defaultObject := Object{id: "o-00"} + plugin.Register(plugin.Plugin{ + Id: "obj", + Name: "test obj plugin", + Objects: map[string]interface{}{ + "getId": defaultObject.GetId, + "new": func(id string) interface{} { + return &Object{id: id} + }, + "echo": func(text string, echoFunc func(text string) string) interface{} { + return echoFunc(text) + }, + "echoTimes": func(echoFunc func(text string)) { + for i := 0; i < 5; i++ { + //time.Sleep(100 * time.Millisecond) + echoFunc(fmt.Sprint(i)) + } + }, + }, + }) +} + +func test(t *testing.T, name string, check bool, extArgs ...interface{}) { + if check { + fmt.Println(u.Green(name), u.BGreen("[OK]")) + } else { + fmt.Println(u.Red(name), u.BRed("[Failed]"), fmt.Sprintln(extArgs...)) + t.Error(name) + } +} + +func TestGlobal(t *testing.T) { + code := ` +log('test', 'name', 'log') +return plus(number,2) +` + globals := map[string]interface{}{ + "number": 9, + "log": log.DefaultLogger.Info, + "plus": func(i, j int) int { return i + j }, + } + + r := gojs.Run(code, globals, log.DefaultLogger) + test(t, "call", u.Int(r) == 11, r) +} + +func TestPlugin(t *testing.T) { + r := gojs.Run("return obj.getId()", nil, log.DefaultLogger) + test(t, "obj.getId()", u.String(r) == "o-00", r) + + r = gojs.Run(` +o = obj.new('o-01') +return o.getId() +`, nil, log.DefaultLogger) + test(t, "new obj.getId()", u.String(r) == "o-01", r) + + t1 := time.Now() + r = gojs.Run(` +out = '' +obj.echo('123', function(text){ + out = text +}) +return out +`, nil, log.DefaultLogger) + t2 := time.Now() + fmt.Println("time:", t2.UnixMicro() - t1.UnixMicro()) + test(t, "callback", u.String(r) == "123", r) + + t1 = time.Now() + r = gojs.Run(` +out = '' +obj.echoTimes(function(text){ + out += text +}) +return out +`, nil, log.DefaultLogger) + t2 = time.Now() + fmt.Println("time:", t2.UnixMicro() - t1.UnixMicro()) + test(t, "callbacks", u.String(r) == "01234", r) +} + + +func BenchmarkEcho(tb *testing.B) { + tb.StopTimer() + ms1 := runtime.MemStats{} + runtime.ReadMemStats(&ms1) + tb.StartTimer() + for i := 0; i < tb.N; i++ { + gojs.Run(`return 1`, nil, log.DefaultLogger) + } + tb.StopTimer() + + ms2 := runtime.MemStats{} + runtime.ReadMemStats(&ms2) + + runtime.GC() + ms3 := runtime.MemStats{} + runtime.ReadMemStats(&ms3) + fmt.Println(">>", ms1.HeapInuse, ms2.HeapInuse, ms3.HeapInuse) +} + + +func BenchmarkCallback(tb *testing.B) { + tb.StopTimer() + ms1 := runtime.MemStats{} + runtime.ReadMemStats(&ms1) + tb.StartTimer() + for i := 0; i < tb.N; i++ { + gojs.Run(` +out = '' +obj.echoTimes(function(text){ + out += text +}) +return out +`, nil, log.DefaultLogger) + } + tb.StopTimer() + + ms2 := runtime.MemStats{} + runtime.ReadMemStats(&ms2) + + runtime.GC() + ms3 := runtime.MemStats{} + runtime.ReadMemStats(&ms3) + fmt.Println(">>", ms1.HeapInuse, ms2.HeapInuse, ms3.HeapInuse) +}