'goo의 text/template에서 사용할 수 있는 함수를 보고 싶다'는 주제로 go:linkname와 함께 장난치다

25472 단어 Gotech

개시하다


text/template에서 사용할 수 있는 함수를 열거하고 싶습니다.개인적으로 text/template는 일반인들이reflect를 잘 느끼지 못하는 것과 비슷한 것을 가지고 있다.뭐, 잘 못한다는 의식만으로도 어쩔 수 없는 일이고, 친해지고 싶지는 않지만 계속 걸어가고 싶어요.
따라서 나는 어떤 주제를 바탕으로text/template를 접촉하여text/template에 대한 거리를 좁히고 싶다.이번 주제는 '삽입식 함수 일람표 얻기' 다.

hello world


어쨌든 먼저 text/template로 Hello World를 만들어 봅시다.다음 코드를 써 보세요.각양각색의 일을 고려하기 전에 간단한 코드로 실제로 사용해 보세요.
package main

import (
	"html/template"
	"os"
)

func main() {
	tmpl := template.Must(template.New("ROOT").Parse(`Hello {{.Thing}}`))
	data := struct{ Thing string }{"World"}
	tmpl.Execute(os.Stdout, data)
}
실행 결과.
Hello World
안전 이용.Hello World 완성.

함수의lookup 부분 탐색


Hello World를 통해 알게 된 것은template입니다.New로 어떤 값을 만들면 Execute()가 실행될 수 있다고 한다.어떤 struct를 사용했는지 한번 봅시다.
$ go doc text/template.New
package template // import "text/template"

func New(name string) *Template
    New allocates a new, undefined template with the given name.
응응,text.템플레이트를 만들 수 있겠죠.그리고 이 Execute () 로 실행하면
$ go doc text/template.Execute
package template // import "text/template"

func (t *Template) Execute(wr io.Writer, data interface{}) error
    Execute applies a parsed template to the specified data object, and writes
    the output to wr. If an error occurs executing the template or writing its
    output, execution stops, but partial results may already have been written
    to the output writer. A template may be executed safely in parallel,
    although if parallel executions share a Writer the output may be
    interleaved.

    If data is a reflect.Value, the template applies to the concrete value that
    the reflect.Value holds, as in fmt.Print.
이 New (),Parse () 또는 Execute () 의 중간에 삽입식으로 제공된 함수의 일람표를 주입합니다.
천천히 전선을 보세요.필요 없는 부분은 요약이 된다.
Template.Execute()
  -> Template.execute()
    -> state.walk()
      -> nodeによって分岐。walkTemplate(),walkRange()だとかnode毎の処理が呼ばれる
      -> state.walkTemplate()
        -> state.evalPipeline()
          -> state.evalCommand()
            -> state.evalFunction()
              -> findFunction()
Template.Execute()는 다양한 node를 찾아본 뒤findFunction()을 부른다고 한다.이름도 그런 느낌이야.이런 함수입니다.
https://github.com/golang/go/blob/1984ee00048b63eacd2155cd6d74a2d13e998272/src/text/template/funcs.go#L139-L151
// findFunction looks for a function in the template, and global map.
func findFunction(name string, tmpl *Template) (reflect.Value, bool) {
	if tmpl != nil && tmpl.common != nil {
		tmpl.muFuncs.RLock()
		defer tmpl.muFuncs.RUnlock()
		if fn := tmpl.execFuncs[name]; fn.IsValid() {
			return fn, true
		}
	}
	if fn := builtinFuncs()[name]; fn.IsValid() {
		return fn, true
	}
	return reflect.Value{}, false
}
응, 두 노선을 찾고 있는 것 같아.
  • 자신이 가지고 있는execFuncs라는 맵
  • builtinFuncs()의 반환값 맵
  • 이름별로 보면 삽입식 함수는 builtinFuncs()를 따라가면 된다.
    https://github.com/golang/go/blob/1984ee00048b63eacd2155cd6d74a2d13e998272/src/text/template/funcs.go#L70
    var builtinFuncsOnce struct {
    	sync.Once
    	v map[string]reflect.Value
    }
    
    // builtinFuncsOnce lazily computes & caches the builtinFuncs map.
    // TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
    func builtinFuncs() map[string]reflect.Value {
    	builtinFuncsOnce.Do(func() {
    		builtinFuncsOnce.v = createValueFuncs(builtins())
    	})
    	return builtinFuncsOnce.v
    }
    
    보아하니sync.One으로 덮어쓰기, 처음 사용할 때 한 번만 초기화된 것 같습니다.init () 로 부르면 import 때 처리가 지나가기 때문에 처리가 늦어지나요?이것은 잠시 내버려두고 이 bultin Funcs라고 부르면 일람표를 얻을 수 있을 것 같습니다.

    unexported 함수를 강제로 실행하여 맵을 얻습니다


    그럼bultinFuncs()를 실행하여 맵을 얻으면 삽입식 함수의 일람을 얻을 수 있다는 것을 알았습니다.참, text/template 패키지에서 이 함수는 unexported입니다.unexported라는 함수를 강제로 부르면 많은 진전이 있을 것이다.
    예를 들어 LL에게 뇌를 물리친 LL뇌로 생각하면 이런 일을 하고 싶다.
    reflect.ValueOf(template).FindMethodByName("builtinFuncs")
    
    야, 그럴 리가 없어.
    이것은 내가 쓰고 싶은 첫 번째 일이다.정식 공연 코드에서 절대 사용하고 싶지 않지만, evil 방법을 사용하면 다른 포장에서 unexported 함수를 호출할 수 있습니다.
    사실 Compuile 문서용 Compuiler Directives에 적힌 바와 같이 go의 컴파일러는 코드에 특수한 주석을 박으면 그 기능을 잘 볼 수 있다.
    이 중 go:linkname 을 사용하면 unexported 함수를 호출할 수 있습니다.
    //go:linkname localname [importpath.name]
    
    This special directive does not apply to the Go code that follows it. Instead, the//go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. If the “importpath.name” argument is omitted, the directive uses the symbol's default object file symbol name and only has the effect of making the symbol accessible to other packages. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".
    unsafe의 import을 잊지 마세요.이런 느낌의 코드예요.
    package main
    
    import (
    	"fmt"
    	"reflect"
    	_ "text/template"
    	_ "unsafe"
    )
    
    //go:linkname template_builtinFuncs text/template.builtinFuncs
    func template_builtinFuncs() map[string]reflect.Value
    
    func main() {
    	bultins := template_builtinFuncs()
    	for name, rv := range bultins {
    		fmt.Printf("%10s: %[2]T %+[2]v\n", name, rv)
    	}
    }
    
    실행해 보세요.
    go run main.go
            eq: reflect.Value 0x10b1a60
           and: reflect.Value 0x10b1740
            ne: reflect.Value 0x10b2da0
        printf: reflect.Value 0x10a40a0
            ge: reflect.Value 0x10b3d20
         slice: reflect.Value 0x10b01a0
           len: reflect.Value 0x10b0920
         index: reflect.Value 0x10afb80
           not: reflect.Value 0x10b1a00
          html: reflect.Value 0x10b41e0
            js: reflect.Value 0x10b4b20
         print: reflect.Value 0x10a4180
       println: reflect.Value 0x10a4240
      urlquery: reflect.Value 0x10b4ba0
            gt: reflect.Value 0x10b3c40
            le: reflect.Value 0x10b3ae0
          call: reflect.Value 0x10b0ae0
            or: reflect.Value 0x10b18a0
            lt: reflect.Value 0x10b2e80
    
    했네요.

    추출한 함수 사용하기


    겸사겸사 findFunction도 사용해 보세요.같은 순서로 진행할 수 있을 것 같습니다.이번엔 리플렉스야.밸류에 저장된 함수로 호출되기 때문에 리플렉스 패키지에 대한 지식이 필요할 수 있습니다.
    텍스트 상자에서 urlquery 일대를 사용해 보십시오.아마 URL 코드 주실 거죠?
    package main
    
    import (
    	"fmt"
    	"reflect"
    	"text/template"
    	_ "unsafe"
    )
    
    //go:linkname template_findFunction text/template.findFunction
    func template_findFunction(name string, tmpl *template.Template) (reflect.Value, bool)
    
    func main() {
    	tmpl := template.New("ROOT")
    	rfn, ok := template_findFunction("urlquery", tmpl)
    	if !ok {
    		panic("not found")
    	}
    
    	s := "?xxx=111&yyy=222"
    	rvs := rfn.Call([]reflect.Value{reflect.ValueOf(s)})
    	fmt.Println(len(rvs), rvs[0].String())
    }
    
    typo를 할 때 조용히 이런 메시지를 보내면 피곤하겠지(template가tepmlate)
    main.main: relocation target text/tepmlate.findFunction not defined
    
    수정 후 실행.기분 좋네.reflect.Value의 Call 매개 변수와 반환 값은 reflect입니다.밸류 슬라이스 좀 귀찮은데.
    1 %3Fxxx%3D111%26yyy%3D222
    

    실제 호출 함수는


    실제로 어떤 함수를 사용했을까.신경 쓰이네.소스 코드를 따라가면 알 수 있으니 조금만 다른 방법을 써 보세요.함수의reflect.밸류(Value)를 얻었기 때문에 그곳에서 포인트를 얻었다.symtable에서 이pointer를 꺼내면 함수의 정보를 검색할 수 있습니다.
    이런 느낌의 코드를 추가해 보세요.
    	rfunc := runtime.FuncForPC(rfn.Pointer())
    	fname, line := rfunc.FileLine(rfunc.Entry())
    	fmt.Println(rfunc.Name(), fname, line)
    
    rfn은 방금 꺼낸'urlquery'의 물건이다.실행 후 아래 출력을 얻을 수 있습니다.
    text/template.URLQueryEscaper /opt/local/lib/go/src/text/template/funcs.go 740
    
    그렇군요, URLQueryEscaper.
    https://github.com/golang/go/blob/1984ee00048b63eacd2155cd6d74a2d13e998272/src/text/template/funcs.go#L740
    듣기에 매우 옳다.
    // URLQueryEscaper returns the escaped value of the textual representation of
    // its arguments in a form suitable for embedding in a URL query.
    func URLQueryEscaper(args ...interface{}) string {
    	return url.QueryEscape(evalArgs(args))
    }
    

    future work


    (이것은 덤이다)
    그런데 이reflect에 둘러싸인 함수를 직접 불러도 되지 않나요?아직 자세히 조사하지 않았습니다. 다만 unsafe입니다.Pointer cast만 이용하면 안 돼요.
    	s := "?xxx=111&yyy=222"
    	fn := *(*func(...interface{}) string)(unsafe.Pointer(rfn.Pointer()))
    	fmt.Println("use unsafe", fn(s))
    
    에 이런 오류가 발생했습니다.
    unexpected fault address 0x30250c8b4865
    fatal error: fault
    [signal SIGSEGV: segmentation violation code=0x1 addr=0x30250c8b4865 pc=0x10b7ad1
    
    reflect.밸류가 함수로 되돌아오는 포인트가 진짜 함수의 포인트인지도 모르겠다.어떤 제한이 있는 값을 돌려주는 것도 이상하지 않다.이 근처가 앞으로 과제가 될 것 같아.직접 호칭할 수 있다면 재미있을 텐데.

    추기


    전선과 놀다가 마침내 알아차렸다.문서에 기록되어 있네요.
  • https://golang.org/pkg/text/template/#hdr-Functions
  • 참고 자료

  • https://golang.org/pkg/text/template/#hdr-Functions
  • https://golang.org/cmd/compile/#hdr-Compiler_Directives
  • http://www.alangpierce.com/blog/2016/03/17/adventures-in-go-accessing-unexported-functions/
  • https://go101.org/article/unsafe.html
  • 좋은 웹페이지 즐겨찾기