TinyGoとWasm
TinyGoとは組み込み向けアーキテクチャで動作させるGo言語のコンパイラです。サポートしているマイコンボードの一覧は、GitHubのReadMeに記載されています(https://github.com/tinygo-org/tinygo)。
TinyGoの特徴として、Wasmにコンパイルさせることができます。Wasmは今後発展していきそうな技術トピックの1つだと思います。OS非依存にVMで動く、ブラウザで動作するなど、将来性を感じさせてくれる技術です。
本記事では、tinygoコマンドを実行した際、内部で何が起きているのか、コードを追って調べました。
TinyGoでWasmを動かす
// main.go
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
main.goを作成したら、以下のコマンドを実行すればwasm.wasmが出力されます。
tinygo build -o wasm.wasm -target wasm ./main.go
tinygoコマンドでwasmがbuildされるまで
tinygoコマンドを実行した際、内部で何が起きているのか、コードを追って調べました。
1.wasm.jsonの読み込み
各targetのbuild方法が定義されたjsonファイルが存在します。wasm.jsonを読み込みます。
{
"llvm-target": "wasm32-unknown-wasi",
"cpu": "generic",
"features": "+bulk-memory,+nontrapping-fptoint,+sign-ext",
"build-tags": ["tinygo.wasm"],
"goos": "js",
"goarch": "wasm",
"linker": "wasm-ld",
"libc": "wasi-libc",
"scheduler": "asyncify",
"default-stack-size": 16384,
"cflags": [
"-mbulk-memory",
"-mnontrapping-fptoint",
"-msign-ext"
],
"ldflags": [
"--allow-undefined",
"--stack-first",
"--no-demangle"
],
"emulator": "node {root}/targets/wasm_exec.js {}",
"wasm-abi": "js"
}
2.SSA作成
buildが実行されます。SSA形式の中間言語が作られます。
https://github.com/tinygo-org/tinygo/blob/release/builder/build.go#L97
// Load the target machine, which is the LLVM object that contains all
// details of a target (alignment restrictions, pointer size, default
// address spaces, etc).
machine, err := compiler.NewTargetMachine(compilerConfig)
if err != nil {
return err
}
defer machine.Dispose()
// Load entire program AST into memory.
lprogram, err := loader.Load(config, pkgName, config.ClangHeaders, types.Config{
Sizes: compiler.Sizes(machine),
})
if err != nil {
return err
}
err = lprogram.Parse()
if err != nil {
return err
}
// Create the *ssa.Program. This does not yet build the entire SSA of the
// program so it's pretty fast and doesn't need to be parallelized.
program := lprogram.LoadSSA()
3.compileとlinkの実行
compileとlinkが実行されます。
// Run all jobs to compile and link the program.
// Do this now (instead of after elf-to-hex and similar conversions) as it
// is simpler and cannot be parallelized.
err = runJobs(linkJob, config.Options.Semaphore)
if err != nil {
return err
}
linkJob内でlink関数が実行されます。
err = link(config.Target.Linker, ldflags...)
link関数内で、wasm-ldコマンドが実行されます。
https://github.com/tinygo-org/tinygo/blob/06df31944c6af706d6cb8b30a2e92d19060436d2/builder/tools.go#L30-L50
func link(linker string, flags ...string) error {
if hasBuiltinTools && (linker == "ld.lld" || linker == "wasm-ld") {
// Run command with internal linker.
cmd := exec.Command(os.Args[0], append([]string{linker}, flags...)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// Fall back to external command.
if _, ok := commands[linker]; ok {
return execCommand(linker, flags...)
}
cmd := exec.Command(linker, flags...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = goenv.Get("TINYGOROOT")
return cmd.Run()
}
4.wasmファイルの出力
BuildResultを引数に渡して、actionが実行されます。
return action(BuildResult{
Executable: executable,
Binary: tmppath,
MainDir: lprogram.MainPkg().Dir,
ModuleRoot: moduleroot,
ImportPath: lprogram.MainPkg().ImportPath,
})
action関数は、builder.Buildを実行する際に渡している以下の関数です。
https://github.com/tinygo-org/tinygo/blob/d984b55311a2acd6c1b14ef6e87ee280e737a925/main.go#L157-L192
if outpath == "" {
if strings.HasSuffix(pkgName, ".go") {
// A Go file was specified directly on the command line.
// Base the binary name off of it.
outpath = filepath.Base(pkgName[:len(pkgName)-3]) + config.DefaultBinaryExtension()
} else {
// Pick a default output path based on the main directory.
outpath = filepath.Base(result.MainDir) + config.DefaultBinaryExtension()
}
}
if err := os.Rename(result.Binary, outpath); err != nil {
// Moving failed. Do a file copy.
inf, err := os.Open(result.Binary)
if err != nil {
return err
}
defer inf.Close()
outf, err := os.OpenFile(outpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777)
if err != nil {
return err
}
// Copy data to output file.
_, err = io.Copy(outf, inf)
if err != nil {
return err
}
// Check whether file writing was successful.
return outf.Close()
} else {
// Move was successful.
return nil
}
})
まとめ
TinyGoでWasmを作成すると、SSA作成を行なった上で、wasm-ldコマンドが実行されます。wasm-ldコマンドの実行結果を最終パスにmoveすることで、Wasmファイルが作成されます。
つまり、TinyGo内部でWasm作成が完結しているわけでは無いようです。
TinyGoのコードを読むこと自体は非常に勉強になりましたが、Wasmに対する理解は深まりませんでした。Wasmを詳しく知りたい場合はWebAssembly公式ドキュメント・レポジトリを読むべきですね。
誤っている点などありましたら、ご指摘いただけると幸いです。