goのtext/template内で使える関数を一覧したいというテーマでgo:linknameと戯れてみた

公開:2020/10/19
更新:2020/10/19
9 min読了の目安(約8500字TECH技術記事

はじめに

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.Templateが作られるわけですね。そしてこれの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
}

ふむふむ、どうやら2つのルートで探していそうです。

  • 自分自身の持つ、execFuncsというmap
  • globalに定義されていそうな、builtinFuncs()の戻り値のmap

名前からして、組み込みの関数は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.Onceで覆って初回利用時に一度だけ初期化をしていそうです。init()で呼ぶとimport時に処理が走ってしまうので処理を遅延させているんでしょうか?。まぁそれは置いておいて、このbultinFuncs()を呼べば、一覧を取得できそうですね。

unexportedな関数を無理やり実行してmapを得る

さて、bultinFuncs()を実行してmapが得られれば埋め込まれた関数の一覧が手に入りそうだというところまでわかりました。ところで、text/templateパッケージではこの関数がunexportedなんですよね。。unexportedな関数も無理やり呼べたらいろいろと捗るんですが。.

例えば、LLに脳をやられたLL脳で考えると、こういうことがしたい。

reflect.ValueOf(template).FindMethodByName("builtinFuncs")

まぁ、無理な話なんですが。。

ここが書きたかったことの1つ目です。絶対に本番でのコードでは使ってほしくないですが、evilな方法を使えばunexportedな関数を別のパッケージから呼び出せたりします。

実は、cmd/CompileのドキュメントのCompiler 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も使ってみましょう。同様の手順でいけそうです。今度はreflect.Valueとして格納された関数の呼び出しになるので、reflectパッケージの知識が必要になるかもしれません。

テキトーに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.Valueのsliceなのがちょっと面倒ですね。

1 %3Fxxx%3D111%26yyy%3D222

実際に呼ばれていた関数は

実際のところ、どういう関数が使われていたのでしょう。ちょっと気になりますね。ソースコードを辿っていけばわかりますが、少し違ったアプローチでみてみましょう。関数のreflect.Valueが手に入るので、そこからpointerが手に入ります。このpointerをsymtableから取り出してみると、関数の情報が取り出せます。

こんな感じのコードを追加してみましょう。

	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.Valueが関数で返したpointerが本物の関数のpointerかわからないですし。何らかの制限が入った値を返してもおかしくはないです。この辺は後々の課題になりそうです。直接呼べるようにできたらまた面白いですけどね。。

追記

コードと戯れてからようやく気づいたんですけど。ドキュメントに載っていましたね。。

参考