// Package types is responsible for parsing the Godot headers for type definitions // and generating Go wrappers around that structure. package types import ( "bytes" "log" "os" "os/exec" "path/filepath" "sort" "strings" "text/template" "git.alfi.li/gamelang/gdnative-go/cmd/generate/methods" "github.com/pinzolo/casee" ) // don't try to use goimports if its missing (nowadays goreturns is used mainly) var noGoImport bool // View is a structure that holds the api struct, so it can be used inside // our template. type View struct { Headers []string TypeDefinitions []TypeDef MethodDefinitions []Method IgnoreMethods []string } // Debug will allow you to log inside the running template. func (v View) Debug(itm string) string { log.Println("Template Log:", itm) return "" } // IsValidProperty will determine if we should be generating the given property // in our Go structure. func (v View) IsValidProperty(prop TypeDef) bool { return !strings.Contains(prop.Name, "_touch_that") } // IsGodotBaseType will check to see if the given simple type definition is defining // a built-in C type or a Godot type. func (v View) IsGodotBaseType(typeDef TypeDef) bool { return strings.Contains(typeDef.Base, "godot_") } // ToGoBaseType will convert a base type name to the correct Go base type. func (v View) ToGoBaseType(base string) string { switch base { case "float": return "float64" case "wchar_t": return "string" } return base } // ToGoName will convert a prefixed string to the correct Go name func (v View) ToGoName(str string) string { str = strings.Replace(str, "godot_", "", 1) str = strings.Replace(str, "GODOT_", "", 1) return casee.ToPascalCase(str) } // ToGoReturnType will remove void return types func (v View) ToGoReturnType(str string) string { str = v.ToGoArgType(str, true) if strings.Contains(str, "Void") { return "" } return str } // HasReturn returns true if the given string is void func (v View) HasReturn(str string) bool { if str == "void" || str == "Void" || strings.Contains(str, "void") { return false } return true } // HasPointerReturn returns true if the given string contains an indirection operator func (v View) HasPointerReturn(str string) bool { return strings.Contains(str, "*") } // IsVoidPointerType returns true if the given string matches godot object void types func (v View) IsVoidPointerType(str string) bool { switch str { case "godot_object *", "const godot_object *": return true } return false } // IsWcharT returns true if the given strig contains wchar_t type func (v View) IsWcharT(str string) bool { return strings.Contains(str, "wchar_t") } // IsDoublePointer returns true if the given string contains two indirection // operators one beside another func (v View) IsDoublePointer(str string) bool { return strings.Contains(str, "**") } // ToGoArgType converts arguments types to Go valid types func (v View) ToGoArgType(str string, parseArray bool) string { str = strings.Replace(str, "const ", "", -1) str = v.ToGoName(str) str = strings.Replace(str, "*", "", 1) str = strings.TrimSpace(str) // If the string still contains a *, it is a list. if strings.Contains(str, "*") { str = strings.Replace(str, "*", "", 1) if parseArray { str = "[]" + str } } return str } // ToGoArgName converts argument names to idiomatic Go ones removing any prefixes func (v View) ToGoArgName(str string) string { if strings.HasPrefix(str, "p_") { str = strings.Replace(str, "p_", "", 1) } if strings.HasPrefix(str, "r_") { str = strings.Replace(str, "r_", "", 1) } str = casee.ToCamelCase(str) // Check for any reserved names switch str { case "type": return "aType" case "default": return "aDefault" case "var": return "variable" case "func": return "function" case "return": return "returns" case "interface": return "intrfce" case "string": return "str" } return str } // IsBasicType returns true if the given string is part of our defined basic types func (v View) IsBasicType(str string) bool { switch str { case "Uint", "WcharT", "Bool", "Double", "Error", "Int", "Int64T", "Uint64T", "Uint8T", "Uint32T", "Real", "MethodRpcMode", "PropertyHint", "SignedChar", "UnsignedChar", "Vector3Axis": return true } return false } // OutputCArg will determine if we need to reference, dereference, etc. an argument // before passing it to a C function. func (v View) OutputCArg(arg []string) string { argType := arg[0] // For basic types, we usually don't pass by pointer. if v.IsBasicType(v.ToGoArgType(argType, true)) { if v.HasPointerReturn(argType) { return "&" } if argType == "wchar_t" && !v.HasPointerReturn(argType) { return "*" } return "" } // Non-basic types are returned as pointers. If the C function doesn't want // a pointer, we need to dereference the argument. if !v.HasPointerReturn(argType) { return "*" } return "" } // MethodsList returns all of the methods that match this typedef. func (v View) MethodsList(typeDef TypeDef) []Method { methods := []Method{} // Look for all methods that match this typedef name. for _, method := range v.MethodDefinitions { ignoreMethod := false for _, ignMethod := range v.IgnoreMethods { if method.Name == ignMethod { ignoreMethod = true } } if ignoreMethod { continue } for _, arg := range method.Arguments { argName := arg[1] argType := strings.Replace(arg[0], "const", "", 1) argType = strings.Replace(argType, "*", "", 1) argType = strings.TrimSpace(argType) if argType == typeDef.Name && argName == "p_self" { methods = append(methods, method) break } else if strings.Contains(method.Name, typeDef.Name) && v.MethodIsConstructor(method) { methods = append(methods, method) break } } } return methods } // MethodIsConstructor returns true if the given method contains the `_new` sub string func (v View) MethodIsConstructor(method Method) bool { return strings.Contains(method.Name, "_new") } // NotSelfArg return false if the given string contains any reference to self or p_self func (v View) NotSelfArg(str string) bool { if str == "self" || str == "p_self" { return false } return true } // StripPointer strips the indirection operator from a given string func (v View) StripPointer(str string) string { str = strings.Replace(str, "*", "", 1) str = strings.TrimSpace(str) return str } // ToGoMethodName cleans names from typed definitions and adapt to Go func (v View) ToGoMethodName(typeDef TypeDef, method Method) string { methodName := method.Name // Replace the typedef in the method name methodName = strings.Replace(methodName, typeDef.Name, "", 1) // Swap some things around if this is a constructor if v.MethodIsConstructor(method) { methodName = strings.Replace(methodName, "_new", "", 1) methodName = "new_" + typeDef.GoName + "_" + methodName } if strings.HasPrefix(methodName, "2") { methodName = "T" + methodName } return casee.ToPascalCase(methodName) } // Method defines a regular method components type Method struct { Name string ReturnType string Arguments [][]string } // Generate will generate Go wrappers for all Godot base types func Generate() { // Get the API Path so we can localize the godot api JSON. apiPath := os.Getenv("API_PATH") if apiPath == "" { panic("$API_PATH is not defined.") } packagePath := apiPath // Set up headers/structures to ignore. Definitions in the given headers // with the given name will not be added to the returned list of type definitions. // We'll need to manually create these structures. ignoreHeaders := []string{ filepath.Join("pluginscript", "godot_pluginscript.h"), filepath.Join("net/godot_net.h"), filepath.Join("net/godot_webrtc.h"), } ignoreStructs := []string{ "godot_char_type", "godot_gdnative_api_struct", "godot_gdnative_core_api_struct", "godot_gdnative_ext_arvr_api_struct", "godot_gdnative_ext_nativescript_1_1_api_struct", "godot_gdnative_ext_nativescript_api_struct", "godot_gdnative_ext_pluginscript_api_struct", "godot_gdnative_init_options", "godot_gdnative_ext_net_3_2_api_struct", "godot_instance_binding_functions", "godot_instance_create_func", "godot_instance_destroy_func", "godot_instance_method", "godot_method_attributes", "godot_property_get_func", "godot_property_set_func", "godot_property_usage_flags", } ignoreMethods := []string{ "godot_string_new_with_wide_string", "godot_string_new", "godot_string_new_copy", "godot_string_name_new", "godot_string_name_new_data", "godot_transform2d_new", "godot_transform2d_new_axis_origin", "godot_transform2d_new_identity", } // Parse all available methods gdnativeAPI := methods.Parse() // Convert the API definitions into a method struct allMethodDefinitions := []Method{} for _, api := range gdnativeAPI.Core.API { method := Method{ Name: api.Name, ReturnType: api.ReturnType, Arguments: api.Arguments, } allMethodDefinitions = append(allMethodDefinitions, method) } // Parse the Godot header files for type definitions allTypeDefinitions := Parse(ignoreHeaders, ignoreStructs) // Create a map of the type definitions by header name defMap := map[string][]TypeDef{} // Organize the type definitions by header name for _, typeDef := range allTypeDefinitions { _, ok := defMap[typeDef.HeaderName] if ok { defMap[typeDef.HeaderName] = append(defMap[typeDef.HeaderName], typeDef) } else { defMap[typeDef.HeaderName] = []TypeDef{typeDef} } } // pretty.Println(defMap) // Loop through each header name and generate the Go code in a file based // on the header name. log.Println("Generating Go wrappers for Godot base types...") for headerName, typeDefs := range defMap { // Convert the header name into the Go filename _, headerFile := filepath.Split(headerName) outFileName := strings.Replace(headerFile, ".h", ".gen.go", 1) // outFileName = strings.Replace(outFileName, "godot_", "", 1) log.Printf(" Generating Go code for: \x1b[32m%s\x1b[0m...\n", outFileName) // Create a structure for our template view. This will contain all of // the data we need to construct our Go wrappers. var view View // Add the type definitions for this file to our view. view.MethodDefinitions = allMethodDefinitions view.TypeDefinitions = typeDefs view.Headers = []string{} view.IgnoreMethods = ignoreMethods // Collect all of the headers we need to use in our template. headers := map[string]bool{} for _, typeDef := range view.TypeDefinitions { headers[typeDef.HeaderName] = true } for header := range headers { view.Headers = append(view.Headers, header) } sort.Strings(view.Headers) // Write the file using our template. WriteTemplate( filepath.Join(packagePath, "cmd", "generate", "templates", "types.go.tmpl"), filepath.Join(packagePath, "gdnative", outFileName), view, ) // Run gofmt on the generated Go file. log.Println(" Running gofmt on output:", outFileName+"...") if !noGoImport { GoFmt(packagePath + "/gdnative/" + outFileName) } log.Println(" Running goimports on output:", outFileName+"...") err := GoImports(packagePath + "/gdnative/" + outFileName) if err != nil { log.Println(" Trying to run goreturns on output:", outFileName+"...") GoReturns(packagePath + "/gdnative/" + outFileName) noGoImport = true } } // pretty.Println(allMethodDefinitions) } // WriteTemplate writes the result from our template file func WriteTemplate(templatePath, outputPath string, view View) { // Create a template from our template file. t, err := template.ParseFiles(templatePath) if err != nil { log.Fatal("Error parsing template:", err) } // Open the output file for writing f, err := os.Create(outputPath) if err != nil { panic(err) } defer f.Close() // Write the template with the given view. err = t.Execute(f, view) if err != nil { panic(err) } } // GoFmt runs gofmt on the given filepath func GoFmt(filePath string) { cmd := exec.Command("gofmt", "-w", filePath) var stdErr bytes.Buffer cmd.Stderr = &stdErr err := cmd.Run() if err != nil { log.Println("Error running gofmt:", err) panic(stdErr.String()) } } // GoImports runs goimports in the given filepath func GoImports(filePath string) error { cmd := exec.Command("goimports", "-w", filePath) var stdErr bytes.Buffer cmd.Stderr = &stdErr err := cmd.Run() return err } // GoReturns runs goreturns in the given filepath func GoReturns(filepath string) { cmd := exec.Command("goreturns", "-w", filepath) var stdErr bytes.Buffer cmd.Stderr = &stdErr err := cmd.Run() if err != nil { log.Println("Error running goreturns and goimports is not installed", err) panic(stdErr.String()) } }