Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Callbackgen

 Callbackgen

Callback generator with Go generate.

#golang #gogenerate #go #callback #codegen

Yo-An Lin

March 29, 2022
Tweet

More Decks by Yo-An Lin

Other Decks in Programming

Transcript

  1. Reflection handle dynamic type information in the runtime • dynamic

    type handling, especially interface types. • slower runtime, more CPU instructions. • more code for handling di ff erent types. • could cause runtime panic if types are not handled well.
  2. Code generation handle types in the compile time • static

    code analysis + code generation. • compilation could fail if types are not handled well. • no runtime handling, faster code. • work like generic. • code completion is possible.
  3. The Story started from… hand-written callbacks package main import (

    "github.com/gorilla/websocket" ) type Client struct { connectCallbacks []func(c *websocket.Conn) disconnectCallbacks []func(c *websocket.Conn) } func (c *Client) OnConnect(cb func(c *websocket.Conn)) { c.connectCallbacks = append(c.connectCallbacks, cb) } func (c *Client) EmitConnect(conn *websocket.Conn) { for _, cb := range c.connectCallbacks { cb(conn) } }
  4. package main import ( "github.com/gorilla/websocket" ) type Client struct {

    connectCallbacks []func(c *websocket.Conn) disconnectCallbacks []func(c *websocket.Conn) } func (c *Client) OnConnect(cb func(c *websocket.Conn)) { c.connectCallbacks = append(c.connectCallbacks, cb) } func (c *Client) EmitConnect(conn *websocket.Conn) { for _, cb := range c.connectCallbacks { cb(conn) } } func (c *Client) OnDisconnect(cb func(c *websocket.Conn)) { c.disconnectCallbacks = append(c.disconnectCallbacks, cb) } func (c *Client) EmitDisconnect(conn *websocket.Conn) { for _, cb := range c.disconnectCallbacks { cb(conn) } }
  5. package main import ( "github.com/gorilla/websocket" ) type Client struct {

    connectCallbacks []func(c *websocket.Conn) disconnectCallbacks []func(c *websocket.Conn) } func (c *Client) OnConnect(cb func(c *websocket.Conn)) { c.connectCallbacks = append(c.connectCallbacks, cb) } func (c *Client) EmitConnect(conn *websocket.Conn) { for _, cb := range c.connectCallbacks { cb(conn) } } func (c *Client) OnDisconnect(cb func(c *websocket.Conn)) { c.disconnectCallbacks = append(c.disconnectCallbacks, cb) } func (c *Client) EmitDisconnect(conn *websocket.Conn) { for _, cb := range c.disconnectCallbacks { cb(conn) } } package main import ( "github.com/gorilla/websocket" ) //go:generate callbackgen -type Client type Client struct { connectCallbacks []func(c *websocket.Conn) disconnectCallbacks []func(c *websocket.Conn) }
  6. Step 1 parse package fi les package main func parsePackage(patterns

    []string, tags []string) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo, Tests: false, BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, } pkgs, err := packages.Load(cfg, patterns...) if err != nil { log.Fatal(err) } }
  7. Step 2 traverse the syntax tree for _, pkg :=

    range pkgs { for i, file := range pkg.Syntax { ast.Inspect(file.file, func(node ast.Node) bool { switch decl := node.(type) { case *ast.ImportSpec: case *ast.FuncDecl: // .... } return true } } } USVFJUFSBUFJOUPUIFOPEF CMPDL TUBUFNFOU GVODUJPOʜFUD UIFOPEFUZQFUIBUZPVXBOUUPJOTQFDU
  8. type ( // An ImportSpec node represents a single package

    import. ImportSpec struct { Doc *CommentGroup // associated documentation; or nil Name *Ident // local package name (including "."); or nil Path *BasicLit // import path Comment *CommentGroup // line comments; or nil EndPos token.Pos // end of spec (overrides Path.Pos if nonzero) } // A ValueSpec node represents a constant or variable declaration // (ConstSpec or VarSpec production). // ValueSpec struct { Doc *CommentGroup // associated documentation; or nil Names []*Ident // value names (len(Names) > 0) Type Expr // value type; or nil Values []Expr // initial values; or nil Comment *CommentGroup // line comments; or nil } ) import ( "golang.org/x/tools/go/packages" ) a := 1 + 2
  9. Step 3 Resolve the AST node to Type Info switch

    decl := node.(type) { case *ast.FuncDecl: // get the first receiver node receiver := decl.Recv.List[0] // pass the receiver type token to the type info map receiverTypeAndValue, ok := pkg.TypesInfo.Types[receiver.Type] if !ok { return true } // inspect the REAL type switch v := recvTV.Type.(type) { case *types.Named: // type A int case *types.Pointer: // *int } } 5ZQF"OE7BMVFDPOUBJOTUIFUZQFJOGPSNBUJPO VTFUIFUZQFJOGPSNBUJPOUPDIFDL
  10. Step 4 generate the code func renderCode() { var sliceCallbackTmpl

    = template.New("slice-callbacks").Funcs(funcMap) sliceCallbackTmpl = template.Must(sliceCallbackTmpl.Parse(` func ( {{- .RecvName }} *{{ .Field.StructName -}} ) On{{- .Field.EventName -}} (cb {{ .Field.CallbackTypeName .Qualifier -}} ) {{ .RecvName }}.{{ .Field.FieldName }} = append({{- .RecvName }}.{{ .Field.FieldName }}, cb) } func ( {{- .RecvName }} *{{ .Field.StructName -}} ) Emit{{- .Field.EventName -}} {{ .Field.CallbackParamsTuple | typeString }} for _, cb := range {{ .RecvName }}.{{ .Field.FieldName }} { cb({{ .Field.CallbackParamsVarNames | join ", " }}) } } `) sliceCallbackTmpl.Execute(&buf, TemplateArgs{ Field: field, RecvName: recvName, Qualifier: qf, GenerateRemoveMethod: *generateRemoveMethod, }) }
  11. Step 5 reformat the code import ( "go/format" ) src,

    err := format.Source(buf.Bytes()) if err != nil { // Should never happen, but can arise when developing this code. // The user can compile the output to see the error. log.Printf("warning: internal error: invalid Go generated: %s", err) log.Printf("warning: compile the package to analyze the error") return buf.Bytes() } return src
  12. Your Filter Friends Filtering the AST nodes • FilterDecl(decl Decl,

    f Filter) bool • FilterFile(src *File, f Filter) bool • FilterPackage(pkg *Package, f Filter) bool • Inspect(node Node, f func(Node) bool)