[インデックス 15610] ファイルの概要
このコミットは、Go言語プロジェクトのlib9ライブラリに、一時ディレクトリの作成 (mktempdir)、ファイルやディレクトリの再帰的削除 (removeall)、および外部コマンドの実行 (runcmd、コミットメッセージではrunprogと記載) の機能を追加するものです。これらの機能は、Plan 9、Unix系システム (Darwin, FreeBSD, Linux, NetBSD, OpenBSD)、およびWindowsといった複数のオペレーティングシステムに対応するように実装されています。
コミット
commit 7610a0552fcd5ef3ed75c0e931275d2e7cdb9eaf
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 6 15:48:28 2013 -0500
lib9: add mktempdir, removeall, runprog
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7523043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7610a0552fcd5ef3ed75c0e931275d2e7cdb9eaf
元コミット内容
lib9: add mktempdir, removeall, runprog
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7523043
変更の背景
Go言語のツールチェインやランタイムは、その一部でC言語で書かれたコード(特に初期のGoコンパイラやリンカ、一部のシステムコールラッパーなど)を利用しています。lib9は、Goプロジェクト内で使用されるC言語のユーティリティライブラリであり、Plan 9オペレーティングシステムの影響を強く受けています。
このコミットの背景には、Goのビルドプロセスやテスト、あるいは内部ツールが、一時的なファイルシステム操作(一時ディレクトリの作成と削除)や外部プロセスの実行を、クロスプラットフォームで信頼性高く行う必要があったことが挙げられます。特に、Windows環境ではファイルパスのエンコーディング(UTF-16)やプロセス管理のAPIがUnix系システムと大きく異なるため、専用の実装が必要でした。
これらの基本的なシステム操作をlib9に集約することで、Goのツールチェインが様々なプラットフォームで一貫した動作を保証し、開発者がプラットフォーム固有の差異を意識することなくコードを書けるようにするための基盤を強化しています。
前提知識の解説
- lib9: Go言語プロジェクト内で使用されるC言語のユーティリティライブラリ。Plan 9オペレーティングシステムの設計思想に影響を受けており、Goの初期のツールチェインやランタイムの一部で利用されていました。システムコールや基本的なI/O操作、文字列操作など、低レベルな機能を提供します。
- Plan 9: ベル研究所で開発された分散オペレーティングシステム。Unixの後継として設計され、"Everything is a file"という哲学が特徴です。Go言語の設計にはPlan 9の思想が色濃く反映されています。
- クロスプラットフォーム開発: ソフトウェアを複数の異なるオペレーティングシステム(例: Windows, Linux, macOS)で動作させるための開発手法。OSごとに異なるAPIやファイルシステム、プロセス管理の仕組みを抽象化し、共通のインターフェースを提供することが重要になります。
- 一時ディレクトリ (Temporary Directory): プログラムの実行中に一時的にファイルを保存するために使用されるディレクトリ。通常、プログラムの終了時に内容が削除されるか、システムによって定期的にクリーンアップされます。セキュリティ上の理由から、予測不可能な名前で作成されることが一般的です。
execvp(Unix系): Unix系システムで新しいプロセスを実行するためのC標準ライブラリ関数。指定されたパスの実行可能ファイルを、環境変数PATHを検索して見つけ、現在のプロセスを置き換えます。CreateProcessW(Windows): Windows APIで新しいプロセスを作成するための関数。Wサフィックスは、ワイド文字(UTF-16)文字列を引数にとることを示します。WaitForMultipleObjects(Windows): Windows APIで一つ以上のオブジェクトのシグナル状態が変化するのを待機する関数。ここでは子プロセスの終了を待つために使用されます。GetExitCodeProcess(Windows): Windows APIで終了したプロセスの終了コードを取得する関数。WinRune: このコミットで定義されているunsigned short型のエイリアス。Windows APIが通常UTF-16エンコーディングを使用するため、ワイド文字(2バイト)を扱うために導入されています。Goのrune型(Unicodeコードポイントを表すint32)とは異なりますが、概念的には似ています。- UTF-8とUTF-16: 文字エンコーディング方式。UTF-8は可変長でASCII互換性があり、Unix系システムで広く使われます。UTF-16は通常2バイトまたは4バイトで文字を表し、Windowsの内部で広く使われます。これら二つのエンコーディング間の変換は、クロスプラットフォーム互換性を確保する上で重要です。
技術的詳細
このコミットは、mktempdir、removeall、runcmdという3つの主要な機能を、Plan 9、Unix系、Windowsの各プラットフォーム向けに実装しています。
-
mktempdir(一時ディレクトリの作成):- Plan 9 (
src/lib9/tempdir_plan9.c):/tmp/go-link-XXXXXXのようなパターンで一時ディレクトリ名を生成し、createシステムコールで排他的にディレクトリを作成します。nrand関数でランダムなサフィックスを生成し、衝突を避けます。 - Unix系 (
src/lib9/tempdir_unix.c): 環境変数TMPDIRを優先し、なければ/var/tmpを使用します。mkdtemp関数(POSIX標準)を利用して、安全に一時ディレクトリを作成します。mkdtempはテンプレート文字列のXXXXXX部分をユニークな文字列に置き換え、ディレクトリを作成します。 - Windows (
src/lib9/tempdir_windows.c):GetTempPathWで一時ディレクトリのパスを取得し、GetTempFileNameWでユニークなファイル名を生成します。その後、そのファイル名を削除し、CreateDirectoryWでディレクトリを作成します。Windows APIはワイド文字(UTF-16)を使用するため、WinRune型とtoutf/torune関数によるUTF-8とUTF-16間の変換が不可欠です。
- Plan 9 (
-
removeall(再帰的削除):- Plan 9 (
src/lib9/tempdir_plan9.c):removeシステムコールで直接削除を試み、失敗した場合はディレクトリと判断して再帰的に内容を削除します。dirstatでファイル情報を取得し、dirreadallでディレクトリの内容を読み取ります。 - Unix系 (
src/lib9/tempdir_unix.c):statでファイル情報を取得し、ディレクトリでなければunlinkでファイルを削除します。ディレクトリの場合はopendir、readdirで内容を列挙し、再帰的にremoveallを呼び出して子要素を削除した後、rmdirで空になったディレクトリを削除します。 - Windows (
src/lib9/tempdir_windows.c):GetFileAttributesWでファイル属性を取得し、ディレクトリでなければDeleteFileWでファイルを削除します。ディレクトリの場合はFindFirstFileWとFindNextFileWで内容を列挙し、再帰的にremoveallを呼び出して子要素を削除した後、RemoveDirectoryWで空になったディレクトリを削除します。ここでもWinRuneとエンコーディング変換が重要な役割を果たします。
- Plan 9 (
-
runcmd(外部コマンドの実行):- Plan 9 (
src/lib9/run_plan9.c):forkで子プロセスを作成し、子プロセスでexecvpを呼び出してコマンドを実行します。親プロセスはwaitで子プロセスの終了を待ち、終了ステータスを確認します。 - Unix系 (
src/lib9/run_unix.c): Plan 9と同様にforkとexecvpを使用します。親プロセスはwaitまたはwaitpidで子プロセスの終了を待ち、WIFEXITEDとWEXITSTATUSマクロで終了ステータスを確認します。EINTR(システムコールがシグナルによって中断された場合)のハンドリングも含まれます。 - Windows (
src/lib9/run_windows.c):CreateProcessWを使用して新しいプロセスを作成します。コマンドライン引数は、スペースや特殊文字を含む場合に適切に引用符で囲むための複雑なロジック(fmtstrinitとfmtprintを使った文字列構築)を介して構築されます。子プロセスの終了はWaitForMultipleObjectsで待ち、GetExitCodeProcessで終了コードを取得します。ここでも、コマンドライン文字列のUTF-8からUTF-16への変換(torune)と、結果の解放(free)が適切に行われます。
- Plan 9 (
このコミットは、Goのビルドシステムやテストハーネスが、異なるOS環境下でも一貫して動作するための重要な基盤を提供しています。特にWindowsにおけるファイルパスとプロセス管理の複雑さに対処するための詳細な実装が含まれている点が注目されます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが新規作成または変更されています。
-
include/libc.h:mktempdir、removeall、runcmdの関数プロトタイプが追加されました。これにより、これらの関数がlib9の外部から利用可能になります。
-
src/cmd/dist/windows.c:genrun関数内に、src/lib9/run_windows.cにも同様のロジックがあることを示すコメントが追加されました。これは、コードの重複と、一方のバグがもう一方にも影響する可能性を示唆しています。
-
src/lib9/run_plan9.c(新規):- Plan 9環境向けの
runcmdの実装。forkとexecvpを使用し、waitで子プロセスの終了を待ちます。
- Plan 9環境向けの
-
src/lib9/run_unix.c(新規):- Unix系環境向けの
runcmdの実装。+build darwin freebsd linux netbsd openbsdビルドタグにより、これらのOSでのみコンパイルされます。forkとexecvpを使用し、waitで子プロセスの終了を待ちます。
- Unix系環境向けの
-
src/lib9/run_windows.c(新規):- Windows環境向けの
runcmdの実装。CreateProcessWを使用し、コマンドライン引数のエスケープ処理、WaitForMultipleObjectsとGetExitCodeProcessによるプロセス終了の待機と終了コードの取得を行います。
- Windows環境向けの
-
src/lib9/tempdir_plan9.c(新規):- Plan 9環境向けの
mktempdirとremoveallの実装。create、dirstat、dirreadall、removeといったPlan 9固有のシステムコールを使用します。
- Plan 9環境向けの
-
src/lib9/tempdir_unix.c(新規):- Unix系環境向けの
mktempdirとremoveallの実装。+build darwin freebsd linux netbsd openbsdビルドタグにより、これらのOSでのみコンパイルされます。mkdtemp、stat、unlink、opendir、readdir、rmdirといったPOSIX標準の関数を使用します。
- Unix系環境向けの
-
src/lib9/tempdir_windows.c(新規):- Windows環境向けの
mktempdirとremoveallの実装。GetTempPathW、GetTempFileNameW、CreateDirectoryW、GetFileAttributesW、DeleteFileW、FindFirstFileW、FindNextFileW、RemoveDirectoryWといったWindows APIを使用します。また、UTF-8とUTF-16間の変換を行うtoutfとtorune関数も含まれます。
- Windows環境向けの
-
src/lib9/win.h(新規):- Windows固有の型定義と関数プロトタイプ。
WinRune型(unsigned shortのエイリアス)と、torune、toutf関数のプロトタイプが定義されています。
- Windows固有の型定義と関数プロトタイプ。
コアとなるコードの解説
ここでは、特にWindows環境での実装に焦点を当てて解説します。WindowsはUnix系システムとAPIが大きく異なるため、その実装はより複雑です。
src/lib9/tempdir_windows.c
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <u.h>
#include <windows.h>
#include <libc.h>
#include "win.h"
// UTF-16 (WinRune) から UTF-8 (char*) への変換
char*
toutf(WinRune *r)
{
Rune *r1;
int i, n;
char *p;
n = 0;
while(r[n] != '\0')
n++;
n++; // null終端のため
r1 = malloc(n*sizeof r1[0]); // Goのrune型(int32)の配列を確保
for(i=0; i<n; i++)
r1[i] = r[i]; // WinRuneからRuneへコピー(暗黙の型変換)
p = smprint("%S", r1); // Rune配列をUTF-8文字列に変換
free(r1);
return p;
}
// UTF-8 (char*) から UTF-16 (WinRune*) への変換
WinRune*
torune(char *p)
{
int i, n;
Rune *r1;
WinRune *r;
r1 = runesmprint("%s", p); // UTF-8文字列をGoのrune配列に変換
n = 0;
while(r1[n] != '\0')
n++;
n++; // null終端のため
r = malloc(n*sizeof r[0]); // WinRune配列を確保
for(i=0; i<n; i++)
r[i] = r1[i]; // RuneからWinRuneへコピー(暗黙の型変換)
free(r1);
return r;
}
// 一時ディレクトリの作成
char*
mktempdir(void)
{
WinRune buf[1024]; // 一時パスを格納するバッファ (UTF-16)
WinRune tmp[MAX_PATH]; // 生成された一時ファイル名を格納するバッファ (UTF-16)
WinRune golink[] = {'g', 'o', 'l', 'i', 'n', 'k', '\0'}; // プレフィックス (UTF-16)
int n;
n = GetTempPathW(nelem(buf), buf); // システムの一時パスを取得
if(n <= 0)
return nil;
buf[n] = '\0'; // null終端
if(GetTempFileNameW(buf, golink, 0, tmp) == 0) // 一時ファイル名を生成
return nil;
DeleteFileW(tmp); // 生成された一時ファイルを削除(ディレクトリ作成のため)
if(!CreateDirectoryW(tmp, nil)) // 一時ディレクトリを作成
return nil;
return toutf(tmp); // UTF-8に変換して返す
}
// ファイル/ディレクトリの再帰的削除
void
removeall(char *p)
{
WinRune *r, *r1;
DWORD attr;
char *q, *elem;
HANDLE h;
WIN32_FIND_DATAW data; // ファイル検索結果を格納する構造体 (UTF-16)
r = torune(p); // 入力パスをUTF-16に変換
attr = GetFileAttributesW(r); // ファイル属性を取得
if(attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
// ディレクトリでない場合、ファイルを削除
DeleteFileW(r);
free(r);
return;
}
// ディレクトリの場合、内容を列挙して再帰的に削除
q = smprint("%s\\\\*", p); // 検索パターンを構築 (例: "C:\\temp\\*")
r1 = torune(q); // 検索パターンをUTF-16に変換
free(q);
h = FindFirstFileW(r1, &data); // 最初のファイル/ディレクトリを検索
if(h == INVALID_HANDLE_VALUE)
goto done; // 検索失敗または空のディレクトリ
do{
q = toutf(data.cFileName); // 見つかったファイル/ディレクトリ名をUTF-8に変換
elem = strrchr(q, '\\'); // パスからファイル名部分を抽出
if(elem != nil) {
elem++;
if(strcmp(elem, ".") == 0 || strcmp(elem, "..") == 0) {
// "." と ".." はスキップ
free(q);
continue;
}
}
removeall(q); // 再帰的に削除
free(q);
}while(FindNextFileW(h, &data)); // 次のファイル/ディレクトリを検索
FindClose(h); // 検索ハンドルを閉じる
done:
free(r1);
RemoveDirectoryW(r); // 空になったディレクトリを削除
free(r);
}
src/lib9/run_windows.c
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include <u.h>
#include <windows.h>
#define NOPLAN9DEFINES // Plan 9の定義とWindowsの定義の衝突を避ける
#include <libc.h>
#include "win.h"
// 外部コマンドの実行
int
runcmd(char **argv)
{
// ほとんど ../cmd/dist/windows.c からコピーされたロジック。
// ここにバグがあれば、そちらのロジックも修正すること。
int i, j, nslash;
Fmt fmt; // 文字列フォーマットのための構造体
char *q;
WinRune *r; // コマンドライン文字列 (UTF-16)
STARTUPINFOW si; // プロセス起動情報 (UTF-16対応)
PROCESS_INFORMATION pi; // プロセス情報
DWORD code; // 終了コード
fmtstrinit(&fmt); // フォーマッタを初期化
for(i=0; argv[i]; i++) { // 引数を結合してコマンドライン文字列を構築
if(i > 0)
fmtprint(&fmt, " "); // スペースで区切る
q = argv[i];
// 引数にスペース、タブ、引用符、バックスラッシュが含まれる場合、適切に引用符で囲む
if(strstr(q, " ") || strstr(q, "\t") || strstr(q, "\"") || strstr(q, "\\\\") || (strlen(q) > 0 && q[strlen(q)-1] == '\\')) {
fmtprint(&fmt, "\""); // 開始引用符
nslash = 0;
for(; *q; q++) {
if(*q == '\\') {
nslash++;
continue;
}
if(*q == '"') {
// 引用符の前にバックスラッシュをエスケープ
for(j=0; j<2*nslash+1; j++)
fmtprint(&fmt, "\\\\");
nslash = 0;
}
// バックスラッシュをエスケープ
for(j=0; j<nslash; j++)
fmtprint(&fmt, "\\\\");
nslash = 0;
fmtprint(&fmt, "%c", *q); // 文字を追加
}
// 末尾のバックスラッシュをエスケープ
for(j=0; j<2*nslash; j++)
fmtprint(&fmt, "\\\\");
fmtprint(&fmt, "\""); // 終了引用符
} else {
fmtprint(&fmt, "%s", q); // そのまま追加
}
}
q = fmtstrflush(&fmt); // 構築されたコマンドライン文字列を取得 (UTF-8)
r = torune(q); // UTF-8からUTF-16に変換
free(q);
memset(&si, 0, sizeof si);
si.cb = sizeof si;
si.dwFlags = STARTF_USESTDHANDLES; // 標準ハンドルを使用
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 標準出力ハンドルを設定
si.hStdError = GetStdHandle(STD_ERROR_HANDLE); // 標準エラーハンドルを設定
// プロセスを作成
if(!CreateProcessW(nil, r, nil, nil, TRUE, 0, nil, nil, &si, &pi)) {
free(r);
return -1; // 失敗
}
free(r);
// プロセスが終了するまで待機
if(WaitForMultipleObjects(1, &pi.hProcess, FALSE, INFINITE) != 0)
return -1;
// 終了コードを取得
i = GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess); // プロセスハンドルを閉じる
CloseHandle(pi.hThread); // スレッドハンドルを閉じる
if(!i)
return -1;
if(code != 0) {
werrstr("unsuccessful exit status: %d", (int)code); // 終了コードが0以外の場合はエラー
return -1;
}
return 0; // 成功
}
これらのコードは、GoのツールチェインがWindows環境でファイルシステム操作や外部コマンド実行を正確に行うための基盤を提供しています。特に、Windows APIがUTF-16ベースであるため、Goの内部で使われるUTF-8文字列との間で適切なエンコーディング変換を行うtoutfとtorune関数が重要な役割を担っています。また、runcmdにおけるコマンドライン引数のエスケープ処理は、Windowsのコマンドプロンプトの複雑なルールに対応するためのもので、堅牢なプロセス起動を実現しています。
関連リンク
- Go言語公式ウェブサイト: https://golang.org/
- Go言語のソースコードリポジトリ (GitHub): https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://go.googlesource.com/go/+/refs/heads/master
- Plan 9 from Bell Labs: https://9p.io/plan9/
参考にした情報源リンク
- Go言語のソースコード (特に
src/lib9ディレクトリ) - Windows APIドキュメント (MSDN)
- POSIX標準ドキュメント
- Plan 9のドキュメント
- Go言語のコミット履歴とコードレビュー (Gerrit)
mkdtempman pageCreateProcessfunction (Windows)WaitForMultipleObjectsfunction (Windows)GetExitCodeProcessfunction (Windows)GetTempPathfunction (Windows)GetTempFileNamefunction (Windows)CreateDirectoryfunction (Windows)DeleteFilefunction (Windows)FindFirstFileandFindNextFilefunctions (Windows)RemoveDirectoryfunction (Windows)execvpman pagewaitman pagestatman pageopendir,readdir,rmdirman pagesu.h,libc.h(Plan 9 C library headers)fmtpackage (Go's internal formatting library, similar concepts apply to CFmtstruct)runetype in Go: https://go.dev/blog/strings- UTF-8 and UTF-16 encodings.
- Command line string parsing rules on Windows.