3
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-17

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公式ドキュメント・レポジトリを読むべきですね。

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

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0