[インデックス 1569] ファイルの概要
このコミットは、Go言語にdefer
ステートメントを導入するものです。defer
は、関数がリターンする直前に実行される関数呼び出しをスケジュールするためのキーワードであり、リソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングにおいて非常に重要な役割を果たします。この変更により、Go言語のコードのクリーンアップ処理が大幅に簡素化され、堅牢性が向上しました。
コミット
defer
R=r
OCL=23592
CL=23592
---
src/cmd/6g/gen.c | 71 ++++++++++++++++++++++++++++++++++++++-----------\n src/cmd/6g/gg.h | 4 ++-\n src/cmd/gc/go.h | 3 ++-\n src/cmd/gc/go.y | 7 ++++-\n src/cmd/gc/lex.c | 4 +--\n src/cmd/gc/subr.c | 7 ++---\n src/cmd/gc/walk.c | 3 +++\n src/runtime/proc.c | 37 +++++++++++++++++++++++++-\n src/runtime/rt0_amd64.s | 12 ++++++++-\n src/runtime/runtime.h | 23 +++++++++++++---\n 10 files changed, 141 insertions(+), 30 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1e1cc4eb570aa6fec645ff4faf13431847b99db8
元コミット内容
diff --git a/src/cmd/6g/gen.c b/src/cmd/6g/gen.c
index 769a72b8f2..f01f1d8b54 100644
--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -26,6 +26,22 @@ if(newproc == N) {\n newproc->ullman = 1;\n }\n \n+if(deferproc == N) {\n+\tdeferproc = nod(ONAME, N, N);\n+\tdeferproc->sym = pkglookup(\"deferproc\", \"sys\");\n+\tdeferproc->class = PEXTERN;\n+\tdeferproc->addable = 1;\n+\tdeferproc->ullman = 1;\n+}\n+\n+if(deferreturn == N) {\n+\tdeferreturn = nod(ONAME, N, N);\n+\tdeferreturn->sym = pkglookup(\"deferreturn\", \"sys\");\n+\tdeferreturn->class = PEXTERN;\n+\tdeferreturn->addable = 1;\n+\tdeferreturn->ullman = 1;\n+}\n+\n if(throwindex == N) {\n throwindex = nod(ONAME, N, N);\n throwindex->sym = pkglookup(\"throwindex\", \"sys\");
@@ -63,6 +79,7 @@ if(throwreturn == N) {\n }\n }\n \n+\thasdefer = 0;\n \twalk(curfn);\n \tif(nerrors != 0)\n \t\tgoto ret;\n@@ -90,6 +107,8 @@ if(throwreturn == N) {\n \t\tgins(ACALL, N, throwreturn);\n \t}\n \n+\tif(hasdefer)\n+\t\tgins(ACALL, N, deferreturn);\n \tpc->as = ARET;\t// overwrite AEND\n \tpc->lineno = lineno;\n \n@@ -343,7 +362,11 @@ loop:\n \t\tbreak;\n \n \tcase OPROC:\n-\t\tcgen_proc(n);\n+\t\tcgen_proc(n, 1);\n+\t\tbreak;\n+\n+\tcase ODEFER:\n+\t\tcgen_proc(n, 2);\n \t\tbreak;\n \n \tcase ORETURN:\
@@ -683,19 +706,26 @@ argsize(Type *t)\n /*\n * generate:\n *\tcall f\n- * if proc, generate:\n- *\tpush f\n- *\tpush argsize\n- *\tcall newproc\n- *\tpop\n- *\tpop\n+ *\tproc=0\tnormal call\n+ *\tproc=1\tgoroutine run in new proc\n+ *\tproc=2\tdefer call save away stack\n */\n void\n ginscall(Node *f, int proc)\n {\n \tNode reg, con;\n \n-\tif(proc) {\n+\tswitch(proc) {\n+\tdefault:\n+\t\tfatal(\"ginscall: bad proc %d\", proc);\n+\t\tbreak;\n+\n+\tcase 0:\t// normal call\n+\t\tgins(ACALL, N, f);\n+\t\tbreak;\n+\n+\tcase 1:\t// call in new proc (go)\n+\tcase 2:\t// defered call (defer)\n \t\tnodreg(®, types[TINT64], D_AX);\n \t\tif(f->op != OREGISTER) {\n \t\t\tgins(ALEAQ, f, ®);\
@@ -704,12 +734,14 @@ ginscall(Node *f, int proc)\n \t\t\tgins(APUSHQ, f, N);\n \t\tnodconst(&con, types[TINT32], argsize(f->type));\n \t\tgins(APUSHQ, &con, N);\n-\t\tgins(ACALL, N, newproc);\n+\t\tif(proc == 1)\n+\t\t\tgins(ACALL, N, newproc);\n+\t\telse\n+\t\t\tgins(ACALL, N, deferproc);\n \t\tgins(APOPQ, N, ®);\n \t\tgins(APOPQ, N, ®);\n-\t\treturn;\n+\t\tbreak;\n \t}\n-\tgins(ACALL, N, f);\n }\n \n /*\n@@ -767,6 +799,9 @@ cgen_callinter(Node *n, Node *res, int proc)\n \n /*\n * generate call to non-interface method\n+ *\tproc=0\tnormal call\n+ *\tproc=1\tgoroutine run in new proc\n+ *\tproc=2\tdefer call save away stack\n */\n void\n cgen_callmeth(Node *n, int proc)\n@@ -791,7 +826,9 @@ cgen_callmeth(Node *n, int proc)\n \n /*\n * generate function call;\n- * if proc, run call in new proc.\n+ *\tproc=0\tnormal call\n+ *\tproc=1\tgoroutine run in new proc\n+ *\tproc=2\tdefer call save away stack\n */\n void\n cgen_call(Node *n, int proc)\n@@ -851,22 +888,22 @@ ret:\n * generate code to start new proc running call n.\n */\n void\n-cgen_proc(Node *n)\n+cgen_proc(Node *n, int proc)\n {\n \tswitch(n->left->op) {\n \tdefault:\n \t\tfatal(\"cgen_proc: unknown call %O\", n->left->op);\n \n \tcase OCALLMETH:\n-\t\tcgen_callmeth(n->left, 1);\n+\t\tcgen_callmeth(n->left, proc);\n \t\tbreak;\n \n \tcase OCALLINTER:\n-\t\tcgen_callinter(n->left, N, 1);\n+\t\tcgen_callinter(n->left, N, proc);\n \t\tbreak;\n \n \tcase OCALL:\n-\t\tcgen_call(n->left, 1);\n+\t\tcgen_call(n->left, proc);\n \t\tbreak;\n \t}\n \n@@ -947,6 +984,8 @@ void\n cgen_ret(Node *n)\n {\n \tgen(n->left, L);\t// copy out args\n+\tif(hasdefer)\n+\t\tgins(ACALL, N, deferreturn);\n \tgins(ARET, N, N);\n }\n \ndiff --git a/src/cmd/6g/gg.h b/src/cmd/6g/gg.h\nindex a01e5b6e55..881a230737 100644\n--- a/src/cmd/6g/gg.h\n+++ b/src/cmd/6g/gg.h\n@@ -116,6 +116,8 @@ EXTERN\tLabel*\tlabellist;\n EXTERN\tLabel*\tfindlab(Sym*);\n EXTERN\tNode*\tcurfn;\n EXTERN\tNode*\tnewproc;\n+EXTERN\tNode*\tdeferproc;\n+EXTERN\tNode*\tdeferreturn;\n EXTERN\tNode*\tthrowindex;\n EXTERN\tNode*\tthrowreturn;\n \n@@ -151,7 +153,7 @@ void\tcgen_ret(Node*);\n void\tcgen_call(Node*, int);\n void\tcgen_callmeth(Node*, int);\n void\tcgen_callinter(Node*, Node*, int);\n-void\tcgen_proc(Node*);\n+void\tcgen_proc(Node*, int);\n void\tcgen_callret(Node*, Node*);\n void\tcgen_div(int, Node*, Node*, Node*);\n void\tcgen_bmul(int, Node*, Node*, Node*);\ndiff --git a/src/cmd/gc/go.h b/src/cmd/gc/go.h\nindex abb08ebe2d..a5d518b8d1 100644\n--- a/src/cmd/gc/go.h\n+++ b/src/cmd/gc/go.h\n@@ -291,7 +291,7 @@ enum\n \tODOT, ODOTPTR, ODOTMETH, ODOTINTER,\n \tODCLFUNC, ODCLFIELD, ODCLARG,\n \tOLIST, OCMP, OPTR, OARRAY, ORANGE,\n-\tORETURN, OFOR, OIF, OSWITCH,\n+\tORETURN, OFOR, OIF, OSWITCH, ODEFER,\n \tOAS, OASOP, OCASE, OXCASE, OFALL, OXFALL,\n \tOGOTO, OPROC, OMAKE, ONEW, OEMPTY, OSELECT,\n \tOLEN, OCAP, OPANIC, OPANICN, OPRINT, OPRINTN, OTYPEOF,\n@@ -498,6 +498,7 @@ EXTERN\tint32\tstksize;\t\t// stack size for current frame\n EXTERN\tint32\tinitstksize;\t\t// stack size for init function\n EXTERN\tushort\tblockgen;\t\t// max block number\n EXTERN\tushort\tblock;\t\t\t// current block number\n+EXTERN\tint\thasdefer;\t\t// flag that curfn has defer statetment\n \n EXTERN\tNode*\tretnil;\n EXTERN\tNode*\tfskel;\ndiff --git a/src/cmd/gc/go.y b/src/cmd/gc/go.y\nindex ac764b94d8..4aafd0b0c1 100644\n--- a/src/cmd/gc/go.y\n+++ b/src/cmd/gc/go.y\n@@ -15,7 +15,7 @@\n %token\t<val>\t\tLLITERAL\n %token\t<lint>\t\tLASOP\n %token\t<sym>\t\tLNAME LBASETYPE LATYPE LPACK LACONST\n-%token\t<sym>\t\tLPACKAGE LIMPORT LEXPORT\n+%token\t<sym>\t\tLPACKAGE LIMPORT LDEFER\n %token\t<sym>\t\tLMAP LCHAN LINTERFACE LFUNC LSTRUCT\n %token\t<sym>\t\tLCOLAS LFALL LRETURN LDDD\n %token\t<sym>\t\tLLEN LCAP LTYPEOF LPANIC LPANICN LPRINT LPRINTN\n@@ -504,6 +504,11 @@ semi_stmt:\n \t\t$$ = nod(OCALL, $2, $4);\n \t\t$$ = nod(OPROC, $$, N);\n \t}\n+|\tLDEFER pexpr \'(\' oexpr_list \')\'\n+\t{\n+\t\t$$ = nod(OCALL, $2, $4);\n+\t\t$$ = nod(ODEFER, $$, N);\n+\t}\n |\tLGOTO new_name\n \t{\n \t\t$$ = nod(OGOTO, $2, N);\ndiff --git a/src/cmd/gc/lex.c b/src/cmd/gc/lex.c\nindex 83fc1f8d39..b8514549f8 100644\n--- a/src/cmd/gc/lex.c\n+++ b/src/cmd/gc/lex.c\n@@ -1056,7 +1056,7 @@ static\tstruct\n \t\"continue\",\tLCONTINUE,\tTxxx,\n \t\"default\",\tLDEFAULT,\tTxxx,\n \t\"else\",\t\tLELSE,\t\tTxxx,\n-\t\"export\",\tLEXPORT,\tTxxx,\n+\t\"defer\",\tLDEFER,\t\tTxxx,\n \t\"fallthrough\",\tLFALL,\t\tTxxx,\n \t\"false\",\tLFALSE,\t\tTxxx,\n \t\"for\",\t\tLFOR,\t\tTxxx,\n@@ -1275,7 +1275,7 @@ struct\n \tLPRINT,\t\t\"PRINT\",\n \tLPACKAGE,\t\"PACKAGE\",\n \tLIMPORT,\t\"IMPORT\",\n-\tLEXPORT,\t\"EXPORT\",\n+\tLDEFER,\t\t\"DEFER\",\n \tLPANIC,\t\t\"PANIC\",\n };\n \ndiff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c\nindex bfcdd08f5b..98e99ab3b3 100644\n--- a/src/cmd/gc/subr.c\n+++ b/src/cmd/gc/subr.c\n@@ -641,11 +641,12 @@ opnames[] =\n \t[ODCLARG]\t= \"DCLARG\",\n \t[ODCLFIELD]\t= \"DCLFIELD\",\n \t[ODCLFUNC]\t= \"DCLFUNC\",\n+\t[ODEFER]\t= \"DEFER\",\n \t[ODIV]\t\t= \"DIV\",\n-\t[ODOT]\t\t= \"DOT\",\n-\t[ODOTPTR]\t= \"DOTPTR\",\n-\t[ODOTMETH]\t= \"DOTMETH\",\n \t[ODOTINTER]\t= \"DOTINTER\",\n+\t[ODOTMETH]\t= \"DOTMETH\",\n+\t[ODOTPTR]\t= \"DOTPTR\",\n+\t[ODOT]\t\t= \"DOT\",\n \t[OEMPTY]\t= \"EMPTY\",\n \t[OEND]\t\t= \"END\",\n \t[OEQ]\t\t= \"EQ\",\ndiff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c\nindex 99fe055ea2..5004a86f02 100644\n--- a/src/cmd/gc/walk.c\n+++ b/src/cmd/gc/walk.c\n@@ -145,6 +145,7 @@ loop:\n \tcase OXFALL:\n \tcase ORETURN:\n \tcase OPROC:\n+\tcase ODEFER:\n \t\twalktype(n, Etop);\n \t\tbreak;\n \t}\n@@ -342,6 +343,8 @@ loop:\n \t\twalkstate(n->nelse);\n \t\tgoto ret;\n \n+\tcase ODEFER:\n+\t\thasdefer = 1;\n \tcase OPROC:\n \t\tif(top != Etop)\n \t\t\tgoto nottop;\ndiff --git a/src/runtime/proc.c b/src/runtime/proc.c\nindex 7435830ff6..3fe08df94d 100644\n--- a/src/runtime/proc.c\n+++ b/src/runtime/proc.c\n@@ -171,7 +171,7 @@ sys·newproc(int32 siz, byte* fn, byte* arg0)\n \n \tif((newg = gfget()) != nil){\n \t\tnewg->status = Gwaiting;\n-\t}else{\n+\t} else {\n \t\tnewg = malg(4096);\n \t\tnewg->status = Gwaiting;\n \t\tnewg->alllink = allg;\n@@ -204,6 +204,41 @@ sys·newproc(int32 siz, byte* fn, byte* arg0)\n //printf(\" goid=%d\\n\", newg->goid);\n }\n \n+void\n+sys·deferproc(int32 siz, byte* fn, byte* arg0)\n+{\n+\tDefer *d;\n+\n+\td = mal(sizeof(*d) + siz - sizeof(d->args));\n+\td->fn = fn;\n+\td->sp = (byte*)&arg0;\n+\td->siz = siz;\n+\tmcpy(d->args, d->sp, d->siz);\n+\n+\td->link = g->defer;\n+\tg->defer = d;\n+}\n+\n+void\n+sys·deferreturn(int32 arg0)\n+{\n+\t// warning: jmpdefer knows the frame size\n+\t// of this routine. dont change anything\n+\t// that might change the frame size\n+\tDefer *d;\n+\tbyte *sp;\n+\n+\td = g->defer;\n+\tif(d == nil)\n+\t\treturn;\n+\tsp = (byte*)&arg0;\n+\tif(d->sp != sp)\n+\t\treturn;\n+\tmcpy(d->sp, d->args, d->siz);\n+\tg->defer = d->link;\n+\tjmpdefer(d->fn);\n+}\n+\n void\n tracebackothers(G *me)\n {\ndiff --git a/src/runtime/rt0_amd64.s b/src/runtime/rt0_amd64.s\nindex 8588d61a4e..f8d4a381b3 100644\n--- a/src/runtime/rt0_amd64.s\n+++ b/src/runtime/rt0_amd64.s\n@@ -120,7 +120,7 @@ TEXT setspgoto(SB), 7, $0\n //\tif(*val == old){\n //\t\t*val = new;\n //\t\treturn 1;\n-//\t}else\n+//\t} else\n //\t\treturn 0;\n TEXT cas(SB), 7, $0\n \tMOVQ\t8(SP), BX\n@@ -133,3 +133,13 @@ TEXT cas(SB), 7, $0\n \tRET\n \tMOVL\t$1, AX\n \tRET\n+\n+// void jmpdefer(byte*);\n+// 1. pop the caller\n+// 2. sub 5 bytes from the callers return\n+// 3. jmp to the argument\n+TEXT jmpdefer(SB), 7, $0\n+\tMOVQ\t8(SP), AX\t// function\n+\tADDQ\t$(8+56), SP\t// pop saved PC and callers frame\n+\tSUBQ\t$5, (SP)\t// reposition his return address\n+\tJMP\tAX\t\t// and goto function\ndiff --git a/src/runtime/runtime.h b/src/runtime/runtime.h\nindex 5552c9e94d..78e2affc62 100644\n--- a/src/runtime/runtime.h\n+++ b/src/runtime/runtime.h\n@@ -52,6 +52,7 @@ typedef\tstruct\tSigTab\t\tSigTab;\n typedef\tstruct\tMCache\t\tMCache;\n typedef\tstruct\tIface\t\tIface;\n typedef\tstruct\tItype\t\tItype;\n+typedef\tstruct\tDefer\t\tDefer;\n \n /*\n * per cpu declaration\n@@ -128,6 +129,7 @@ struct\tG\n {\n \tbyte*\tstackguard;\t// must not move\n \tbyte*\tstackbase;\t// must not move\n+\tDefer*\tdefer;\t\t// must not move\n \tbyte*\tstack0;\t\t// first stack segment\n \tGobuf\tsched;\n \tG*\talllink;\t// on allg\n@@ -136,8 +138,8 @@ struct\tG\n \tint32\tgoid;\n \tint32\tselgen;\t\t// valid sudog pointer\n \tG*\tschedlink;\n-\tbool\t\treadyonstop;\n-\tM*\tm;\t// for debuggers\n+\tbool\treadyonstop;\n+\tM*\tm;\t\t// for debuggers\n };\n struct\tMem\n {\n@@ -151,8 +153,8 @@ struct\tM\n \tG*\tg0;\t\t// g0 w interrupt stack - must not move\n \tuint64\tmorearg;\t// arg to morestack - must not move\n \tuint64\tcret;\t\t// return value from C - must not move\n-\tuint64\tprocid;\t// for debuggers - must not move\n-\tG*\tgsignal;\t\t// signal-handling G - must not move\n+\tuint64\tprocid;\t\t// for debuggers - must not move\n+\tG*\tgsignal;\t// signal-handling G - must not move\n \tG*\tcurg;\t\t// current running goroutine\n \tG*\tlastg;\t\t// last running goroutine - to emulate fifo\n \tGobuf\tsched;\n@@ -235,6 +237,18 @@ enum\n \tAmax\n };\n \n+/*\n+ * defered subroutine calls\n+ */\n+struct Defer\n+{\n+\tint32\tsiz;\n+\tbyte*\tsp;\n+\tbyte*\tfn;\n+\tDefer*\tlink;\n+\tbyte\targs[8];\t// padded to actual size\n+};\n+\n /*\n * external data\n */\n@@ -286,6 +300,7 @@ int32\twrite(int32, void*, int32);\n void\tclose(int32);\n int32\tfstat(int32, void*);\n bool\tcas(uint32*, uint32, uint32);\n+void\tjmpdefer(byte*);\n void\texit1(int32);\n void\tready(G*);\n byte*\tgetenv(int8*);\n```
## 変更の背景
Go言語における`defer`ステートメントの導入は、プログラミングにおける重要な課題、特にリソース管理とエラーハンドリングをより簡潔かつ安全に行うためのものです。従来のプログラミング言語では、ファイルを開いた後に必ず閉じる、ロックを取得した後に必ず解放する、といったクリーンアップ処理は、関数の複数の終了点(正常終了、エラーによる早期リターンなど)で重複して記述されることが多く、コードの可読性や保守性を損ねる原因となっていました。また、エラーパスでのクリーンアップ忘れは、リソースリークやデッドロックといった深刻なバグにつながる可能性がありました。
`defer`ステートメントは、関数が終了する直前に実行されるコードブロックを登録するメカニズムを提供することで、これらの問題を解決します。これにより、リソースの確保と解放のコードを論理的に近い場所に配置でき、コードの意図が明確になり、エラーパスでのクリーンアップ忘れを防ぐことができます。例えば、ファイルを開いた直後に`defer file.Close()`と記述することで、関数がどのように終了してもファイルが確実に閉じられるようになります。これは、Go言語が提唱する「エラーを明示的に扱う」という設計思想とも合致し、堅牢なアプリケーション開発を支援します。
## 前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の内部構造とプログラミング概念に関する知識が必要です。
### Go言語の`defer`キーワード
`defer`キーワードは、Go言語の関数内で使用され、その関数がリターンする直前に実行される関数呼び出しをスケジュールします。複数の`defer`ステートメントがある場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。つまり、最後に登録された`defer`が最初に実行されます。これは、スタックのように動作するため、「遅延スタック」とも呼ばれます。
### Goコンパイラの構造
Goコンパイラは、ソースコードを機械語に変換するプロセスを担います。このコミットで変更されている主要なコンポーネントは以下の通りです。
* **`src/cmd/gc`**: Goコンパイラの共通部分であり、字句解析(lexing)、構文解析(parsing)、抽象構文木(AST)の構築、型チェック、中間表現(IR)の生成などを行います。`go.y`はYacc/Bisonの文法定義ファイルで、言語の構文規則を定義します。`lex.c`は字句解析器で、ソースコードをトークンに分割します。`go.h`は共通のヘッダファイルで、ASTノードの型定義などが含まれます。`walk.c`はASTを走査し、意味解析や最適化、コード生成の前処理を行います。
* **`src/cmd/6g`**: AMD64アーキテクチャ向けのGoコンパイラのバックエンドです。`gc`によって生成された中間表現を受け取り、特定のアーキテクチャ(この場合はAMD64)の機械語コードを生成します。`gen.c`はコード生成の主要な部分を担い、ASTノードを具体的なアセンブリ命令に変換します。`gg.h`は`6g`固有のヘッダファイルです。
### Goランタイムの役割
Goランタイム(`src/runtime`)は、Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、ゴルーチン(goroutine)のスケジューリング、チャネル通信、メモリ管理、そして`defer`ステートメントの実行など、Goプログラムの実行に必要な多くの機能を提供します。`proc.c`はプロセス(ゴルーチン)管理に関連するコードを含み、`runtime.h`はランタイムのデータ構造やAPIの定義を含みます。`rt0_amd64.s`はAMD64アーキテクチャ向けのランタイムの初期化コードや低レベルのアセンブリ関数を含みます。
### スタックフレームと関数呼び出し規約
関数が呼び出されると、その関数に必要な情報(引数、ローカル変数、リターンアドレスなど)を格納するための「スタックフレーム」がスタック上に作成されます。関数呼び出し規約は、引数がどのように渡され、戻り値がどのように返され、レジスタがどのように保存・復元されるかといった、関数呼び出しに関する取り決めです。`defer`ステートメントは、このスタックフレームと密接に関連して動作し、関数の終了時に特定のコードを実行するために、スタック上の情報を利用します。
## 技術的詳細
このコミットは、Go言語に`defer`ステートメントを導入するために、コンパイラとランタイムの両方に広範な変更を加えています。
1. **字句解析と構文解析 (`src/cmd/gc/lex.c`, `src/cmd/gc/go.y`, `src/cmd/gc/go.h`)**:
* `lex.c`に新しいキーワード`"defer"`が追加され、`LDEFER`トークンとして認識されるようになりました。
* `go.y`の文法定義に`LDEFER`トークンとそれに対応する`ODEFER`ノードタイプが追加されました。これにより、パーサーは`defer`キーワードに続く関数呼び出しを認識し、AST(抽象構文木)内に`ODEFER`ノードとして表現できるようになります。
* `go.h`には、ASTノードの列挙型に`ODEFER`が追加され、`hasdefer`という新しいグローバル変数が導入されました。この`hasdefer`フラグは、現在の関数が`defer`ステートメントを含んでいるかどうかを示すために使用されます。
2. **ASTの走査と意味解析 (`src/cmd/gc/walk.c`)**:
* `walk.c`の`walk`関数(ASTを走査する主要な関数)に`ODEFER`ケースが追加されました。
* `ODEFER`ノードが検出されると、`hasdefer`フラグが`1`に設定されます。これは、その関数が`defer`ステートメントを持つことをコンパイラに通知するために重要です。
3. **コード生成 (`src/cmd/6g/gen.c`, `src/cmd/6g/gg.h`)**:
* `gen.c`には、`deferproc`と`deferreturn`という2つの新しい外部関数(ランタイムで定義される)への参照が追加されました。これらは、`defer`呼び出しの登録と実行をそれぞれ担当します。
* `ginscall`、`cgen_callmeth`、`cgen_callinter`、`cgen_proc`といったコード生成関数に、新しい`proc`引数が追加されました。この引数は、呼び出しの種類(通常呼び出し: `0`、ゴルーチン起動: `1`、`defer`呼び出し: `2`)を区別するために使用されます。
* `ODEFER`ノードが検出されると、`cgen_proc`関数が`proc=2`で呼び出され、`defer`呼び出しとして処理されるようになります。
* 関数がリターンする際(`cgen_ret`関数内)、もし`hasdefer`フラグがセットされていれば、`deferreturn`関数が呼び出されます。これにより、関数終了時に登録された`defer`関数が実行されるメカニズムが提供されます。
* `ginscall`関数内で、`proc`が`2`の場合(`defer`呼び出し)、`newproc`の代わりに`deferproc`が呼び出されるように変更されました。これは、`defer`呼び出しがゴルーチン起動とは異なる方法で処理されることを意味します。
4. **ランタイムサポート (`src/runtime/proc.c`, `src/runtime/runtime.h`, `src/runtime/rt0_amd64.s`)**:
* `runtime.h`に`Defer`構造体が新しく定義されました。この構造体は、遅延実行される関数の情報(関数ポインタ`fn`、スタックポインタ`sp`、引数のサイズ`siz`、引数データ`args`、次の`Defer`構造体へのリンク`link`)を保持します。
* `G`(ゴルーチン)構造体に`Defer* defer`フィールドが追加され、各ゴルーチンが自身の遅延スタックの先頭を指すようになりました。
* `proc.c`に`sys·deferproc`と`sys·deferreturn`という2つのC関数が追加されました。
* `sys·deferproc(int32 siz, byte* fn, byte* arg0)`: `defer`ステートメントが実行されるたびに呼び出されます。この関数は、`Defer`構造体を割り当て、遅延実行される関数の情報(関数ポインタ、引数、スタックポインタ)を保存し、現在のゴルーチンの`defer`リストの先頭に追加します。
* `sys·deferreturn(int32 arg0)`: 関数がリターンする直前に呼び出されます。この関数は、現在のゴルーチンの`defer`リストから最も新しい`Defer`構造体を取り出し、保存された引数をスタックにコピーし、`jmpdefer`を呼び出して遅延関数にジャンプします。
* `rt0_amd64.s`に`jmpdefer`というアセンブリ関数が追加されました。この関数は、`sys·deferreturn`から呼び出され、遅延実行される関数に直接ジャンプするための低レベルのスタック操作を行います。具体的には、呼び出し元のリターンアドレスを調整し、遅延関数へのジャンプを実行します。これは、通常の関数呼び出しとは異なり、スタックフレームを破棄せずに遅延関数に制御を移すために必要です。
これらの変更により、Goコンパイラは`defer`ステートメントを認識し、適切なランタイム関数を呼び出すコードを生成するようになり、Goランタイムはこれらの呼び出しをインターセプトして、関数の終了時に登録された遅延関数を正しい順序で実行するメカニズムを提供します。
## コアとなるコードの変更箇所
### 1. `src/cmd/gc/go.y` (構文解析器の定義)
```diff
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -504,6 +504,11 @@ semi_stmt:
$$ = nod(OCALL, $2, $4);
$$ = nod(OPROC, $$, N);
}
+|\tLDEFER pexpr '(' oexpr_list ')'
+\t{
+\t\t$$ = nod(OCALL, $2, $4);
+\t\t$$ = nod(ODEFER, $$, N);
+\t}
|\tLGOTO new_name
{
$$ = nod(OGOTO, $2, N);
2. src/cmd/6g/gen.c
(コード生成)
--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -343,7 +362,11 @@ loop:
break;
case OPROC:
-\t\tcgen_proc(n);\n+\t\tcgen_proc(n, 1);\n+\t\tbreak;\n+\n+\tcase ODEFER:\n+\t\tcgen_proc(n, 2);\n break;
case ORETURN:
@@ -683,19 +706,26 @@ argsize(Type *t)
* generate:
* call f
- * if proc, generate:
- *\tpush f
- *\tpush argsize
- *\tcall newproc
- *\tpop
- *\tpop
+ * proc=0 normal call
+ * proc=1 goroutine run in new proc
+ * proc=2 defer call save away stack
*/
void
ginscall(Node *f, int proc)
{
Node reg, con;
-\tif(proc) {\n+\tswitch(proc) {\n+\tdefault:\n+\t\tfatal(\"ginscall: bad proc %d\", proc);\n+\t\tbreak;\n+\n+\tcase 0: // normal call\n+\t\tgins(ACALL, N, f);\n+\t\tbreak;\n+\n+\tcase 1: // call in new proc (go)\n+\tcase 2: // defered call (defer)\n nodreg(®, types[TINT64], D_AX);\
@@ -704,12 +744,14 @@ ginscall(Node *f, int proc)
gins(APUSHQ, f, N);
nodconst(&con, types[TINT32], argsize(f->type));
gins(APUSHQ, &con, N);
-\t\tgins(ACALL, N, newproc);\n+\t\tif(proc == 1)\n+\t\t\tgins(ACALL, N, newproc);\n+\t\telse\n+\t\t\tgins(ACALL, N, deferproc);\n gins(APOPQ, N, ®);
gins(APOPQ, N, ®);
-\t\treturn;\n+\t\tbreak;\n }
-\tgins(ACALL, N, f);\n }\n \n /*
@@ -947,6 +989,8 @@ void
cgen_ret(Node *n)
{
gen(n->left, L); // copy out args
+\tif(hasdefer)\n+\t\tgins(ACALL, N, deferreturn);\n gins(ARET, N, N);
}
3. src/runtime/proc.c
(ランタイムのdefer
処理)
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -204,6 +204,41 @@ sys·newproc(int32 siz, byte* fn, byte* arg0)
//printf(" goid=%d\n", newg->goid);
}
+void
+sys·deferproc(int32 siz, byte* fn, byte* arg0)
+{
+ Defer *d;
+
+ d = mal(sizeof(*d) + siz - sizeof(d->args));
+ d->fn = fn;
+ d->sp = (byte*)&arg0;
+ d->siz = siz;
+ mcpy(d->args, d->sp, d->siz);
+
+ d->link = g->defer;
+ g->defer = d;
+}
+
+void
+sys·deferreturn(int32 arg0)
+{
+ // warning: jmpdefer knows the frame size
+ // of this routine. dont change anything
+ // that might change the frame size
+ Defer *d;
+ byte *sp;
+
+ d = g->defer;
+ if(d == nil)
+ return;
+ sp = (byte*)&arg0;
+ if(d->sp != sp)
+ return;
+ mcpy(d->sp, d->args, d->siz);
+ g->defer = d->link;
+ jmpdefer(d->fn);
+}
+
void
tracebackothers(G *me)
{
4. src/runtime/runtime.h
(データ構造の定義)
--- a/src/runtime/runtime.h
+++ b/src/runtime/runtime.h
@@ -128,6 +129,7 @@ struct G
{
byte* stackguard; // must not move
byte* stackbase; // must not move
+\tDefer*\tdefer;\t\t// must not move
byte* stack0; // first stack segment
Gobuf sched;
G* alllink; // on allg
@@ -235,6 +237,18 @@ enum
Amax
};
+/*
+ * defered subroutine calls
+ */
+struct Defer
+{
+\tint32 siz;
+\tbyte* sp;
+\tbyte* fn;
+\tDefer* link;
+\tbyte args[8]; // padded to actual size
+};
+
/*
* external data
*/
@@ -286,6 +300,7 @@ int32 write(int32, void*, int32);
void close(int32);
int32 fstat(int32, void*);
bool cas(uint32*, uint32, uint32);
+void jmpdefer(byte*);
void exit1(int32);
void ready(G*);
byte* getenv(int8*);
5. src/runtime/rt0_amd64.s
(アセンブリコード)
--- a/src/runtime/rt0_amd64.s
+++ b/src/runtime/rt0_amd64.s
@@ -133,3 +133,13 @@ TEXT cas(SB), 7, $0
RET
MOVL $1, AX
RET
+\
+// void jmpdefer(byte*);
+// 1. pop the caller
+// 2. sub 5 bytes from the callers return
+// 3. jmp to the argument
+TEXT jmpdefer(SB), 7, $0
+\tMOVQ 8(SP), AX // function
+\tADDQ $(8+56), SP // pop saved PC and callers frame
+\tSUBQ $5, (SP) // reposition his return address
+\tJMP AX // and goto function
コアとなるコードの解説
1. src/cmd/gc/go.y
の変更
この変更は、Go言語の構文解析器にdefer
キーワードを認識させるためのものです。
LDEFER pexpr '(' oexpr_list ')'
という新しいルールが追加されています。これは、defer
キーワードの後に式(pexpr
、通常は関数名)と括弧で囲まれた引数リスト(oexpr_list
)が続く構文を定義しています。
この構文が解析されると、$$ = nod(OCALL, $2, $4);
によって通常の関数呼び出しノード(OCALL
)が作成され、その後に$$ = nod(ODEFER, $$, N);
によってそのOCALL
ノードを子に持つODEFER
ノードが作成されます。これにより、コンパイラの後の段階で、このノードが通常の関数呼び出しではなく、遅延実行されるべき関数呼び出しであることが識別されます。
2. src/cmd/6g/gen.c
の変更
case ODEFER:
の追加:OPROC
(ゴルーチン起動)と同様に、ODEFER
ノードが検出された場合にcgen_proc(n, 2)
が呼び出されるようになりました。ここで2
という引数は、この呼び出しがdefer
呼び出しであることを示します。これにより、コンパイラはdefer
ステートメントに対して特別なコード生成パスを使用するようになります。ginscall
関数の変更:ginscall
は、関数呼び出しのアセンブリコードを生成する汎用関数です。以前はproc
引数が真偽値(ゴルーチン起動かどうか)でしたが、この変更でint proc
となり、0
(通常呼び出し)、1
(ゴルーチン起動)、2
(defer
呼び出し)の3つのケースを扱うようになりました。proc
が1
の場合(ゴルーチン起動)は引き続きnewproc
ランタイム関数を呼び出しますが、proc
が2
の場合(defer
呼び出し)は新しく導入されたdeferproc
ランタイム関数を呼び出すように分岐が追加されました。これは、defer
呼び出しがゴルーチンとは異なる方法でランタイムに登録されることを意味します。cgen_ret
関数の変更:cgen_ret
は、関数のリターンコードを生成する関数です。この関数にif(hasdefer) gins(ACALL, N, deferreturn);
という行が追加されました。これは、現在の関数内にdefer
ステートメントが存在する場合(hasdefer
フラグが1
の場合)、関数がリターンする直前にdeferreturn
ランタイム関数を呼び出すことを意味します。これにより、関数終了時に遅延スタックに積まれた関数が実行されるトリガーとなります。
3. src/runtime/proc.c
の変更
sys·deferproc
関数の追加: この関数は、defer
ステートメントが実行されるたびにコンパイラによって生成されたコードから呼び出されます。Defer
構造体を動的に割り当てます。この構造体は、遅延実行される関数の情報(関数ポインタfn
、引数のサイズsiz
、引数データargs
、呼び出し時のスタックポインタsp
)を保持します。- 呼び出される関数の引数を、現在のスタックから
Defer
構造体のargs
フィールドにコピーします。 - 現在のゴルーチン(
g
)のdefer
リストの先頭に、新しく作成したDefer
構造体をリンクします。これにより、defer
呼び出しはLIFO順で管理されます。
sys·deferreturn
関数の追加: この関数は、defer
ステートメントを持つ関数がリターンする直前にコンパイラによって生成されたコードから呼び出されます。- 現在のゴルーチンの
defer
リストの先頭(最も新しいdefer
)を取得します。リストが空であれば何もしません。 defer
構造体に保存されているスタックポインタd->sp
と現在のスタックポインタsp
を比較し、一致しない場合は何もしません。これは、defer
が別のスタックフレームで実行されることを防ぐための安全チェックです。Defer
構造体に保存されている引数データd->args
を、現在のスタックポインタd->sp
の位置にコピーし直します。これにより、遅延関数が正しい引数で呼び出される準備が整います。g->defer = d->link;
によって、現在のDefer
構造体をリストから削除し、次の遅延関数をリストの先頭に設定します。jmpdefer(d->fn);
を呼び出して、遅延実行される関数にジャンプします。
- 現在のゴルーチンの
4. src/runtime/runtime.h
の変更
Defer
構造体の定義:Defer
構造体は、遅延実行される関数呼び出しのコンテキストを保持するために導入されました。int32 siz
: 遅延関数の引数の合計サイズ。byte* sp
:defer
が登録された時点のスタックポインタ。byte* fn
: 遅延実行される関数のポインタ。Defer* link
: 次のDefer
構造体へのポインタ。これにより、遅延関数がリンクリストとして管理されます。byte args[8]
: 遅延関数の引数を格納するためのバッファ。実際のサイズは動的に確保されます。
G
構造体へのDefer* defer
フィールドの追加: 各ゴルーチン(G
構造体)が自身の遅延スタックの先頭を指すように、Defer* defer
フィールドが追加されました。これにより、ゴルーチンごとに独立したdefer
リストが管理されます。jmpdefer
関数の宣言: アセンブリで実装されるjmpdefer
関数のプロトタイプが宣言されました。
5. src/runtime/rt0_amd64.s
の変更
jmpdefer
アセンブリ関数の追加: このアセンブリ関数は、sys·deferreturn
から呼び出され、遅延実行される関数に制御を移すための低レベルのジャンプを実行します。MOVQ 8(SP), AX
: スタックから遅延実行される関数のポインタをAX
レジスタにロードします。ADDQ $(8+56), SP
: 呼び出し元のスタックフレームをポップし、スタックポインタを調整します。8
はリターンアドレス、56
はsys·deferreturn
のローカル変数と保存されたレジスタのサイズに相当します。SUBQ $5, (SP)
: 呼び出し元のリターンアドレスを5バイト減らします。これは、CALL
命令が5バイトであるため、jmpdefer
から戻った際に、deferreturn
を呼び出したCALL
命令の直後ではなく、その前の命令(CALL
命令の開始点)に戻るように調整するためと考えられます。これにより、defer
呼び出しが完了した後に、元の関数の実行が適切に再開されます。JMP AX
:AX
レジスタに格納されている遅延関数のアドレスに直接ジャンプします。これにより、遅延関数が実行されます。
これらの変更は、Go言語のdefer
ステートメントが、コンパイル時に特別なノードとして扱われ、ランタイムにおいてゴルーチンごとに管理される遅延スタックに登録され、関数の終了時に低レベルのアセンブリジャンプによって実行されるという、複雑かつ効率的なメカニズムで実現されていることを示しています。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメント: - Go言語のコンパイラとランタイムの内部構造に関する一般的な情報:
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc
,src/cmd/6g
,src/runtime
ディレクトリ) - Go言語の
defer
に関する一般的な解説記事 - Go言語のコンパイラとランタイムの内部動作に関する技術ブログやドキュメント
- Yacc/Bisonの文法定義に関する情報