TinyGoでWasmをbuildするまでの内部実装

# TinyGoとWasm
TinyGoとは組み込み向けアーキテクチャで動作させるGo言語のコンパイラです。サポートしているマイコンボードの一覧は、GitHubのReadMeに記載されています(https://github.com/tinygo-org/tinygo)。
TinyGoの特徴として、Wasmにコンパイルさせることができます。Wasmは今後発展していきそうな技術トピックの１つだと思います。OS非依存にVMで動く、ブラウザで動作するなど、将来性を感じさせてくれる技術です。

本記事では、tinygoコマンドを実行した際、内部で何が起きているのか、コードを追って調べました。

## TinyGoでWasmを動かす

```golang
// 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を読み込みます。

https://github.com/tinygo-org/tinygo/blob/release/targets/wasm.json
```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
```go
	// 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が実行されます。
```go
	// 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関数が実行されます。
```go
err = link(config.Target.Linker, ldflags...)
```

link関数内で、wasm-ldコマンドが実行されます。
https://github.com/tinygo-org/tinygo/blob/06df31944c6af706d6cb8b30a2e92d19060436d2/builder/tools.go#L30-L50
```go
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が実行されます。
```go
	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
```go
		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公式ドキュメント・レポジトリを読むべきですね。

誤っている点などありましたら、ご指摘いただけると幸いです。