autobindings/generate/generate_bindings.go

510 lines
17 KiB
Go

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:<name>\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:<name>\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:<name>\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:<name>\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)
}