Commit 7cc63f67 authored by Oscar Campos's avatar Oscar Campos
Browse files

feat: add capabilities for code generation with autoregistration

parent 30e084b9
// Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
// Copyright © 2017 - William Edwards
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
package main
import (
"github.com/alecthomas/kong"
)
type context struct {
Path string
Verbose bool
}
type generateCmd struct {
path string
}
type listCmd struct{}
// cli defines our command line structure using Kong
var cli struct {
Path string `type:"path" default:"." help:"Path where execute the command"`
Verbose bool `help:"Verbose output"`
Generate generateCmd `cmd help:"Generates autotoregistration boilerplate Go code for user defined structures"`
List listCmd `cmd help:"List user defined autoregistrable data structures"`
}
func main() {
ctx := kong.Parse(&cli)
err := ctx.Run(&context{Path: cli.Path, Verbose: cli.Verbose})
ctx.FatalIfErrorf(err)
}
// Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
// Copyright © 2017 - William Edwards
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"gitlab.com/pimpam-games-studio/gdnative-go/gdnative"
)
// RegistryData structure is a container for registrable classes
// that is passed to the gdnative_wrapper.go.tmpl template for filling
type RegistryData struct {
Package string
Classes map[string]gdnative.Registrable
}
// GDNativeInit construct and returns a SetNativeInitScript call
func (rd RegistryData) GDNativeInit() string {
initFunctions := []string{}
for className := range rd.Classes {
initFunctions = append(initFunctions, fmt.Sprintf("nativeScriptInit%s", className))
}
return fmt.Sprintf("gdnative.SetNativeScriptInit(%s)", strings.Join(initFunctions, ", "))
}
func (cmd *generateCmd) Run(ctx *context) error {
fset := token.NewFileSet()
packages, parseErr := parser.ParseDir(fset, ctx.Path, cmd.filter, parser.ParseComments)
if parseErr != nil {
return fmt.Errorf("could not parse Go files at %s: %w", ctx.Path, parseErr)
}
tplPath, pathErr := getTemplatePath("gdnative_wrapper.go")
if pathErr != nil {
return fmt.Errorf("could not get GDNative template: %w", pathErr)
}
for pkg, p := range packages {
data := RegistryData{ Package: pkg, Classes: map[string]gdnative.Registrable{} }
registrable := gdnative.LookupRegistrableTypeDeclarations(p)
if len(registrable) == 0 {
fmt.Printf("not found any registrable sources on %s", ctx.Path)
return nil
}
for className, classData := range registrable {
data.Classes[className] = classData
}
// create a template from the template file
tpl, tplErr := template.ParseFiles(tplPath)
if tplErr != nil {
return tplErr
}
outputFileName := fmt.Sprintf("%s_registrable.gen.go", pkg)
outputFilePath := filepath.Join(ctx.Path, outputFileName)
file, fileErr := os.Create(outputFilePath)
if fileErr != nil {
return fmt.Errorf("can not open output file %s for writing: %w", outputFilePath, fileErr)
}
execErr := tpl.Execute(file, data)
if execErr != nil {
return execErr
}
return format(outputFilePath)
}
return nil
}
// get the template path
func getTemplatePath(templateType string) (string, error) {
currentPath, err := getCurrentPath()
if err != nil {
return "", err
}
return filepath.Join(currentPath, "..", "..", "generate", "templates", templateType+".tmpl"), nil
}
// get the current in execution file path on disk
func getCurrentPath() (string, error) {
_, filename, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("could not get current file execution path")
}
return filename, nil
}
func (cmd *generateCmd) filter(info os.FileInfo) bool {
if info.IsDir() {
return false
}
length := len(info.Name())
if length > 7 && info.Name()[length-7:length-2] == ".gen." {
return false
}
return true
}
// formats the given path with gofmt
func format(filepath string) error {
fmt.Println("gofmt", "-w", filepath)
cmd := exec.Command("gofmt", "-w", filepath)
return cmd.Run()
}
// Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
// Copyright © 2017 - William Edwards
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
"gitlab.com/pimpam-games-studio/gdnative-go/gdnative"
)
func (cmd *listCmd) Run(ctx *context) error {
fset := token.NewFileSet()
packages, parseErr := parser.ParseDir(fset, ctx.Path, cmd.filter, parser.ParseComments)
if parseErr != nil {
return fmt.Errorf("could not parse Go files at %s: %w", ctx.Path, parseErr)
}
for pkg, p := range packages {
fmt.Printf("Analyzing package: %s\n", pkg)
gdregistrable := gdnative.LookupRegistrableTypeDeclarations(p)
for key, data := range gdregistrable {
base := data.GetBase()
if base != "" {
base = fmt.Sprintf("(%s)", base)
}
fmt.Printf("Found Structure: %s%s\n", key, base)
for _, property := range data.GetProperties() {
fmt.Printf("\t\t%s\n", property)
}
fmt.Printf("\tConstructor: %s\n", data.GetConstructor())
fmt.Printf("\tDestructor: %s\n", data.GetDestructor())
for _, method := range data.GetMethods() {
fmt.Printf("\t%s\n", method)
}
fmt.Println()
}
}
return nil
}
func (cmd *listCmd) filter(info os.FileInfo) bool {
if info.IsDir() {
return false
}
length := len(info.Name())
if length > 7 && info.Name()[length-7:length-2] == ".gen." {
return false
}
return true
}
......@@ -11,7 +11,7 @@ import (
)
// View is a structure that holds the api struct, so it can be used inside
// our temaplte.
// our template.
type View struct {
API API
StructType string
......
{{ $data := . -}}
// Copyright © 2019 - 2020 Oscar Campos <oscar.campos@thepimpam.com>
// Copyright © 2017 - William Edwards
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ==================================================================
// This file was autogenerated by PimPam GDNative-Go binding tools
// Please do not modify this file, any change will be lost
// ==================================================================
{{/* create wrapper boilerplate */}}
package {{ $data.Package }}
import (
"fmt"
"gitlab.com/pimpam-games-studio/gdnative-go/gdnative"
)
{{ range $className, $class := $data.Classes -}}
// {{ $className }}Wrapper is a wrapper over {{ $className }} that will register it with in godot
type {{ $className }}Wrapper struct {
class *{{ $className }}
}
// {{ $className }}Instances is an internal registry of instances for our custom types
var {{ $className }}Instances = map[string]*{{ $className }}Wrapper{}
// handle{{ $className }} handles calls from Godot to this instance methods
func handle{{ $className }}(object gdnative.Object, methodData, userData string, numArgs int, args []gdnative.Variant) gdnative.Variant {
// lookup instance on registry, if it does not exists return nil
instance, ok := {{ $className }}Instances[userData]
if !ok {
gdnative.Log.Warning(fmt.Sprintf("could not find instance %s on registry", userData))
return gdnative.NewVariantNil()
}
// find the right method and execute it or return an empty nil value and log it
switch methodData {
{{ range $i, $method := $class.Methods -}}
case "{{ $method.GodotName }}":
{{ range $i, $arg := $method.Arguments -}}
{{ $arg.Name }} := args[{{ $i }}].{{ $arg.ConvertFunction }}
{{ end -}}
{{ if $method.HasReturns -}}
value := instance.class.{{ $method.FunctionCallWithParams }}
return {{ $method.NewVariantType }}
{{ else -}}
instance.class.{{ $method.FunctionCallWithParams }}
return gdnative.NewVariantNil()
{{ end -}}
{{ end -}}
}
// if we are here it means the method being called is unknown to us
gdnative.Log.Warning(fmt.Sprintf("could not find method %s on instance %s", methodData, userData))
return gdnative.NewVariantNil()
}
// nativeScriptInit{{ $className }} will run upon NativeScript initialization and its
// responsible for registering all our classes within Godot
func nativeScriptInit{{ $className }}() {
// define an instance creation function, it will be called by Godot
constructor := gdnative.CreateConstructor("{{ $className }}", func(object gdnative.Object, methodData string) string {
// create a new value of this wrapper type
{{ if $class.HasConstructor -}}
instance := {{ $className }}Wrapper{
class: {{ $class.Constructor }}(),
}
{{ else -}}
instance := {{ $className }}Wrapper{
class: ${{ $className }}{},
}
{{ end -}}
// use the pointer address as instance ID
instanceID := fmt.Sprintf("{{ $className }}Wrapper_%p", &instance)
{{ $className }}Instances[instanceID] = &instance
// return the instance ID to Godot
return instanceID
})
// define an instance destruction function, it will be called by Godot
destructor := gdnative.CreateDestructor("{{ $className }}", func(object gdnative.Object, methodData, userData string) {
{{ if $class.HasDestructor -}}
{{ $class.Destructor }}()
{{ end -}}
delete({{ $className }}Instances, userData)
})
// define methods attached to the instance
methods := []gdnative.Method{
{{ range $methodName, $method := $class.Methods -}}
gdnative.NewGodotMethod("{{ $className }}", "{{ $method.GodotName }}", handle{{ $className }}),
{{ end -}}
}
// define properties attached to the instance
properties := []gdnative.Property{
{{ range $propertyName, $property := $class.GetProperties -}}
gdnative.NewGodotProperty("{{ $className }}", {{ $property.Name }}, $property.Hint, $property.HintString, $property.Usage, $property.RsetType, $property.SetFunc, $property.GetFunc),
{{ end -}}
}
// register a new class within Godot
gdnative.RegisterNewGodotClass(false, "{{ $className }}", "{{ $class.GetBase }}", &constructor, &destructor, methods, properties)
}
{{ end -}}{{/* range $className, $class := $data.Classes */ -}}
// The "init()" function is a special Go function that will be called when this library
// is initialized. Here we can register our Godot classes.
func init() {
{{ $data.GDNativeInit }}
}
......@@ -15,11 +15,11 @@ import (
"gitlab.com/pimpam-games-studio/gdnative-go/cmd/generate/methods"
)
// don't try to use goimpots if its misding (nowadays goreturns is used mainly)
// 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 temaplte.
// our template.
type View struct {
Headers []string
TypeDefinitions []TypeDef
......@@ -171,7 +171,7 @@ func (v View) ToGoArgName(str string) string {
return str
}
// IsBasicType returns true if the given sring is part of our defined basic types
// 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":
......@@ -296,14 +296,14 @@ type Method struct {
// Generate will generate Go wrappers for all Godot base types
func Generate() {
// Get the API Path so we can localte the godot api JSON.
// 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/structs to ignore. Definitions in the given headers
// 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{
......
package gdnative
import (
"fmt"
"go/ast"
"strings"
)
const (
godotRegister string = "godot::register"
godotConstructor string = "godot::constructor"
godotDestructor string = "godot::destructor"
)
func LookupRegistrableTypeDeclarations(pkg *ast.Package) map[string]Registrable {
var classes = make(map[string]Registrable)
// make a first iteration to capture all registrable classes and their properties
for _, file := range pkg.Files {
for _, node := range file.Decls {
gd, ok := node.(*ast.GenDecl)
if !ok {
continue
}
for _, d := range gd.Specs {
tp, ok := d.(*ast.TypeSpec)
if !ok {
continue
}
sp, ok := tp.Type.(*ast.StructType)
if !ok {
continue
}
if gd.Doc != nil {
for _, line := range gd.Doc.List {
docstring := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(line.Text, "/", "")))
if strings.HasPrefix(docstring, godotRegister) {
className := getClassName(tp)
class := registryClass{
base: getBaseClassName(sp),
properties: lookupProperties(sp),
}
classes[className] = &class
}
}
}
}
}
}
// make a second iteration to look for class methods and signals
for className := range classes {
class := classes[className]
for _, file := range pkg.Files {
class.SetConstructor(lookupInstanceCreateFunc(className, file))
class.SetDestructor(lookupInstanceDestroyFunc(className, file))
class.AddMethods(lookupMethods(className, file))
// signals: lookupSignals(className, file))
}
}
return classes
}
// getClassName extracts and build the right class name for the registry
func getClassName(tp *ast.TypeSpec) string {
className := tp.Name.String()
return className
}
// getBaseClassName extracts the base class name for the registry
func getBaseClassName(sp *ast.StructType) string {
// TODO: need to make this way smarter to look for parent types of this type
var baseClassName string
for i := 0; i < sp.Fields.NumFields(); i++ {
expr, ok := sp.Fields.List[i].Type.(*ast.SelectorExpr)
if !ok {
continue
}
ident, ok := expr.X.(*ast.Ident)
if !ok {
continue
}
if ident.Name != "godot" {
continue
}
baseClassName = fmt.Sprintf("godot.%s", expr.Sel.Name)
break
}
return baseClassName
}
// lookupInstanceCreateFunc extract the "constructor" for the given type or create a default one
func lookupInstanceCreateFunc(className string, file *ast.File) *registryConstructor {
for _, node := range file.Decls {
fd, ok := node.(*ast.FuncDecl)
if !ok || fd.Doc == nil {
continue
}
for _, line := range fd.Doc.List {
docstring := strings.TrimSpace(strings.ReplaceAll(line.Text, "/", ""))
if strings.HasPrefix(strings.ToLower(docstring), godotConstructor) {
structName := strings.TrimSpace(docstring[len(godotConstructor):])
// get rid of parenthesis
if strings.HasPrefix(structName, "(") && strings.HasSuffix(structName, ")") {
// make sure this is the only parenthesis structName
if strings.Count(structName, "(") > 1 || strings.Count(structName, ")") > 1 {
// this is a syntax error
panic(fmt.Errorf("could not parse constructor comment %s, many parenthesis", docstring))
}
structName = structName[1 : len(structName)-1]
if structName != className {
// this constructor doesn't match with our class, skip it
continue
}
constructor, err := validateConstructor(structName, fd)
if err != nil {
panic(err)
}
return constructor
}
}
}
}
// if we are here it means the user didn't specified a custom constructor
return nil
}
// validateConstructor returns an error if the given constructor is not valid, it returns nil otherwise
func validateConstructor(structName string, fd *ast.FuncDecl) (*registryConstructor, error) {