package main import ( "bufio" "flag" "fmt" "log" "os" "regexp" "strings" ) func main() { var experiments string flag.StringVar(&experiments, "experiments", "", "experiments to enable") flag.Parse() loadedExperiments := make(map[string]bool) for _, exp := range strings.Split(experiments, ",") { loadedExperiments[exp] = true } generatedBindingsPrefix := `` generatedBindings := `` experimentRegistry := `` experimentUpdateSettings := `` file, err := os.Open("spec") if err != nil { log.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) // optionally, resize scanner's capacity for lines over 64K, see next example for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "#") || len(line) == 0 { // ignore continue } parts := strings.Split(line, " ") if len(parts) < 2 { fmt.Printf("all spec lines must start with a type prefix and a function name: %v\n", parts) os.Exit(1) } fType := parts[0] fName := parts[1] args := parts[2:] experiment := "" if strings.HasPrefix(fType, "!") { experiment = strings.ReplaceAll(fType, "!", "") if _, gen := loadedExperiments[experiment]; gen { fType = parts[1] fName = parts[2] args = parts[3:] } else { continue // skip experiment } } if strings.HasPrefix(fType, "import") { generatedBindingsPrefix += fName + "\n" continue } if strings.HasPrefix(fType, "global") { generatedBindings += fmt.Sprintf("var %s %s\n", parts[2], parts[3]) experimentRegistry += fmt.Sprintf(` %s = %s.Init(&globalACN, appDir) eventHandler.AddModule(%s) %s.Enable(application, &globalACN) `, parts[2], parts[4], parts[2], parts[2]) experimentUpdateSettings += fmt.Sprintf(` %s.UpdateSettings(application, &globalACN) `, parts[2]) continue } fmt.Printf("generating %v function for %v\n", fType, fName) switch fType { case "app": generatedBindings = generateAppFunction(generatedBindings, fName, args) case "exp": generatedBindings = generateExpFunction(generatedBindings, fName, experiment, args) case "(json)app": generatedBindings = generateJsonAppFunction(generatedBindings, fName, args) case "profile": generatedBindings = generateProfileFunction(generatedBindings, fName, args) case "(json)profile": generatedBindings = generateJsonProfileFunction(generatedBindings, fName, args, false) case "(json-err)profile": generatedBindings = generateJsonProfileFunction(generatedBindings, fName, args, true) case "@profile-experiment": experiment := args[0] generatedBindings = generateExperimentalProfileFunction(generatedBindings, experiment, fName, args[1:]) case "@(json)profile-experiment": experiment := args[0] generatedBindings = generateExperimentalJsonProfileFunction(generatedBindings, experiment, fName, args[1:]) default: fmt.Printf("unknown function type %v\n", parts) os.Exit(1) } } if err := scanner.Err(); err != nil { log.Fatal(err) } fmt.Printf("%v\n", generatedBindings) // NOTE: You can see individual generations by uncommenting the below lines.. // os.WriteFile("templates/bindings.go", []byte(generatedBindings), 0644) // os.WriteFile("templates/imports.go", []byte(generatedBindingsPrefix), 0644) template, _ := os.ReadFile("templates/lib_template.go") templateString := string(template) templateString = strings.ReplaceAll(templateString, "{{BINDINGS}}", generatedBindings) templateString = strings.ReplaceAll(templateString, "{{EXPERIMENT_REGISTER}}", experimentRegistry) templateString = strings.ReplaceAll(templateString, "{{EXPERIMENT_UPDATESETTINGS}}", experimentUpdateSettings) templateString = strings.ReplaceAll(templateString, "{{IMPORTS}}", generatedBindingsPrefix) os.WriteFile("lib.go", []byte(templateString), 0644) } var uniqueVarCounter = 0 func profileHandleArgPrototype() (string, string, string, string) { return `onion_ptr *C.char, onion_len C.int`, `C.GoStringN(onion_ptr, onion_len)`, `profile string`, "profile" } func nameArgPrototype() (string, string, string, string) { return `name_ptr *C.char, name_len C.int`, `C.GoStringN(name_ptr, name_len)`, `name string`, "name" } func passwordArgPrototype() (string, string, string, string) { return `password_ptr *C.char, password_len C.int`, `C.GoStringN(password_ptr, password_len)`, `password string`, "password" } func intArgPrototype(varName string) (string, string, string, string) { return fmt.Sprintf(`%s C.int`, ToSnakeCase(varName)), fmt.Sprintf(`int(%v)`, ToSnakeCase(varName)), fmt.Sprintf(`%v int`, varName), varName } func uintArgPrototype(varName string) (string, string, string, string) { return fmt.Sprintf(`%s C.uint`, ToSnakeCase(varName)), fmt.Sprintf(`int(%v)`, ToSnakeCase(varName)), fmt.Sprintf(`%v int`, varName), varName } func conversationArgPrototype(varName string) (string, string, string, string) { return intArgPrototype(varName) } func channelArgPrototype() (string, string, string, string) { return intArgPrototype("channel_id") } func messageArgPrototype() (string, string, string, string) { return intArgPrototype("message_id") } func boolArgPrototype(name string) (string, string, string, string) { uniqueVarCounter += 1 varName := fmt.Sprintf("%s%d", name, uniqueVarCounter) return fmt.Sprintf(`%s C.char`, ToSnakeCase(varName)), fmt.Sprintf(`%v == 1`, ToSnakeCase(varName)), fmt.Sprintf(`%v bool`, varName), varName } func stringArgPrototype(name string) (string, string, string, string) { uniqueVarCounter += 1 varName := fmt.Sprintf("%s%d", name, uniqueVarCounter) return fmt.Sprintf(`%s_ptr *C.char, %s_len C.int`, varName, varName), fmt.Sprintf(`C.GoStringN(%s_ptr, %s_len)`, varName, varName), fmt.Sprintf(`%v string`, varName), varName } func mapArgs(argsTypes []string) (string, string, string, string) { var cArgs []string var c2GoArgs []string var goSpec []string var gUse []string for _, argSpec := range argsTypes { argTypeParts := strings.Split(argSpec, ":") argType := argTypeParts[0] switch argType { case "application": gUse = append(gUse, "application") case "acn": gUse = append(gUse, "&globalACN") case "profile": c1, c2, c3, c4 := profileHandleArgPrototype() cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "name": c1, c2, c3, c4 := nameArgPrototype() cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "password": c1, c2, c3, c4 := passwordArgPrototype() cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "conversation": name := "conversation" if len(argTypeParts) == 2 { name = argTypeParts[1] } c1, c2, c3, c4 := conversationArgPrototype(name) cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "channel": c1, c2, c3, c4 := channelArgPrototype() cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "message": c1, c2, c3, c4 := messageArgPrototype() cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "bool": if len(argTypeParts) != 2 { fmt.Printf("generic bool arg must have have e.g. bool:\n") os.Exit(1) } c1, c2, c3, c4 := boolArgPrototype(argTypeParts[1]) cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "int": if len(argTypeParts) != 2 { fmt.Printf("generic int arg must have have e.g. int:\n") os.Exit(1) } c1, c2, c3, c4 := intArgPrototype(argTypeParts[1]) cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) case "uint": if len(argTypeParts) != 2 { fmt.Printf("generic uint arg must have have e.g. uint:\n") os.Exit(1) } c1, c2, c3, c4 := uintArgPrototype(argTypeParts[1]) cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) // because of java/kotlin/android/gomobile inability to recognize unsigned integers // we need to pretent this is a signed interface...so do the final cast here... // this will cause bad behavior if a negative number is passed through the java // interface...so...don't do that... gUse = append(gUse, fmt.Sprintf("uint(%s)", c4)) case "string": if len(argTypeParts) != 2 { fmt.Printf("generic string arg must have have e.g. string:\n") os.Exit(1) } c1, c2, c3, c4 := stringArgPrototype(argTypeParts[1]) cArgs = append(cArgs, c1) c2GoArgs = append(c2GoArgs, c2) goSpec = append(goSpec, c3) gUse = append(gUse, c4) default: fmt.Printf("unknown arg type [%v]\n", argType) os.Exit(1) } } return strings.Join(cArgs, ","), strings.Join(c2GoArgs, ","), strings.Join(goSpec, ","), strings.Join(gUse, ",") } func generateAppFunction(bindings string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) { {{FNAME}}({{C2GO_ARGS}}) } func {{FNAME}}({{GO_ARGS_SPEC}}) { application.{{LIBNAME}}({{GO_ARG}}) } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateExpFunction(bindings string, name string, exp string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) { {{FNAME}}({{C2GO_ARGS}}) } func {{FNAME}}({{GO_ARGS_SPEC}}) { {{EXPERIMENT}}.{{LIBNAME}}({{GO_ARG}}) } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", exp) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateJsonAppFunction(bindings string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) *C.char { return C.CString({{FNAME}}({{C2GO_ARGS}})) } func {{FNAME}}({{GO_ARGS_SPEC}}) string { return application.{{LIBNAME}}({{GO_ARG}}) } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateProfileFunction(bindings string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) { {{FNAME}}({{C2GO_ARGS}}) } func {{FNAME}}({{GO_ARGS_SPEC}}) { cwtchProfile := application.GetPeer(profile) if cwtchProfile != nil { cwtchProfile.{{LIBNAME}}({{GO_ARG}}) } } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) // We need to prepend a set of profile handle arguments... pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateJsonProfileFunction(bindings string, name string, argsTypes []string, handleErr bool) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) *C.char { return C.CString({{FNAME}}({{C2GO_ARGS}})) } func {{FNAME}}({{GO_ARGS_SPEC}}) string { cwtchProfile := application.GetPeer(profile) if cwtchProfile != nil { {{HANDLE_FUNC}} } return "" } ` noErrorPrototype := `return cwtchProfile.{{LIBNAME}}({{GO_ARG}})` withErrorPrototype := `res,err := cwtchProfile.{{LIBNAME}}({{GO_ARG}}) if err != nil { log.Errorf("could not {{FNAME}} %v", err) } return res` functionPrototype := noErrorPrototype if handleErr { functionPrototype = withErrorPrototype } cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{HANDLE_FUNC}}", functionPrototype) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) // We need to prepend a set of profile handle arguments... pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateExperimentalProfileFunction(bindings string, experiment string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) { {{FNAME}}({{C2GO_ARGS}}) } func {{FNAME}}({{GO_ARGS_SPEC}}) { cwtchProfile := application.GetPeer(profile) if cwtchProfile != nil { functionality := {{EXPERIMENT}}.FunctionalityGate() if functionality != nil { err := functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}}) if err != nil { log.Errorf("error calling experimental feature {{FNAME}}: %v", err) } } } } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment) // We need to prepend a set of profile handle arguments... pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } func generateExperimentalJsonProfileFunction(bindings string, experiment string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} func c_{{FNAME}}({{C_ARGS}}) *C.char { return C.CString({{FNAME}}({{C2GO_ARGS}})) } func {{FNAME}}({{GO_ARGS_SPEC}}) string { cwtchProfile := application.GetPeer(profile) if cwtchProfile != nil { functionality := {{EXPERIMENT}}.FunctionalityGate() if functionality != nil { return functionality.{{LIBNAME}}(cwtchProfile, {{GO_ARG}}) } } return "" } ` cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) appPrototype = strings.ReplaceAll(appPrototype, "{{EXPERIMENT}}", experiment) // We need to prepend a set of profile handle arguments... pArgs, c2GoPArg, goSpecP, _ := profileHandleArgPrototype() appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", strings.Join(append([]string{pArgs}, cArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", strings.Join(append([]string{c2GoPArg}, c2GoArgs), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", strings.Join(append([]string{goSpecP}, goSpec), ",")) appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) bindings += appPrototype return bindings } var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") func ToSnakeCase(str string) string { snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") return strings.ToLower(snake) }