Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. c9s
    Callbackgen
    Generate callback methods without pain

    View Slide

  2. Golang Taipei Gathering #2
    2013

    View Slide

  3. View Slide

  4. View Slide

  5. CallbackGen

    View Slide

  6. CallbackGen


    = Callback Generator

    View Slide

  7. Reflection


    vs


    Static Code Generation

    View Slide

  8. 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.

    View Slide

  9. 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.

    View Slide

  10. 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)
    }
    }

    View Slide

  11. ⼿寫⼀次

    View Slide

  12. ⼿寫兩次

    View Slide

  13. ⼿寫三次

    View Slide

  14. View Slide

  15. ⼿寫 N 次

    View Slide

  16. View Slide

  17. View Slide

  18. View Slide

  19. 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)
    }
    }

    View Slide

  20. 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)
    }

    View Slide

  21. View Slide

  22. So, How Does It Work?

    View Slide

  23. Go 1.5
    transit to Go compiler from C compiler

    View Slide

  24. go/types
    Go’s type checker

    View Slide

  25. Parsing


    go/{parser, ast, scanner, token}
    Type Checker


    go/types
    vet lint eg gorename stringer mockgen mockery

    View Slide

  26. a.go
    c.go
    b.go
    fmt
    Type Checker


    go/types
    compilation errors
    expressions
    type-checked packages
    imported


    packages

    View Slide

  27. 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)
    }
    }

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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,
    })
    }

    View Slide

  32. 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

    View Slide

  33. Step 6
    compile and test your code!

    View Slide

  34. Step 7
    copy n paste my code, write your own codegen and have fun

    View Slide

  35. 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)

    View Slide

  36. View Slide

  37. Codegen For Fun

    View Slide

  38. https://github.com/MaiAmis/Careers

    View Slide