Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Plan 9 ANSI/POSIX環境での環境変数

Plan 9ネイティブのCライブラリはPOSIXに準拠しておらず、独自の習慣がある。例えばfopenfcloseなどは使わずopencloseシステムコールを使う。バッファリングが必要であればBiobufを使いなさいという態度を取る。Goの標準パッケージに名残が残っているので、知っていれば雰囲気は伝わると思う。*1

Plan 9環境変数/env以下にファイルとして提供されていて、プロセス毎に異なる内容が扱える*2。これらの変数は配列もサポートしていて、配列の場合は\0で区切られたバイト列として表現される。例えばhome='/usr/lufia'の場合、/env/homeには"/usr/lufia"と保存されていて、path=(. /bin)('.'と'/bin'の配列)の場合/env/pathの内容は".\0/bin"となる。

ANSI/POSIX環境

Plan 9にはネイティブ環境の他に、Unixツールを移植するためだけにANSI/POSIX環境(APE)も用意されている。このライブラリはネイティブとは分けて、ヘッダは/sys/include/ape/に、アーカイブ/386/lib/ape//amd64/lib/ape/などアーキテクチャ毎に置かれている。POSIXでは、getenvputenvで設定した環境変数environ配列からも同じ内容が参照できなければならないという制約があり、environ配列は"home=/usr/lufia"のような"name=value"形式の文字列配列と定義されているため、ANSI/POSIX環境では/envを使わずメモリ上の配列を操作する方針をとる。だからgetenvenviron配列を検索するだけの関数で、putenvenviron配列を更新するだけの関数として用意されている。*3

他のプロセスへ環境変数を引き渡す時は、単純にexecveの引数にenviron配列を渡すだけで良い。execveは渡されたenviron配列を使って/envを再構築する。APEライブラリを使ったプログラムは、mainを実行する前に/envenviron配列へ読み込む。こうするとPlan 9ネイティブなプログラムは/envを参照すればよいし、APEライブラリを使って書かれたプログラムでは引き続きenviron配列として参照できるようになる。これらの処理は以下のソースコードに書かれている。

  • /sys/src/ape/lib/ap/plan9/_envsetup.c
  • /sys/src/ape/lib/ap/plan9/execve.c

ただし、Cの文字列は\0を文字列の終わりと扱ってしまうため、ネイティブ環境で配列となっている環境変数をそのまま扱うと(1つ目の\0で終わってしまって)2つ目以降の要素を扱うことができない。ネイティブ環境の場合は/env以下にファイルがあるため、stat等でファイルサイズを取れば長さが分かるけれども、environ配列はCの文字列になってしまうため、本来の長さを知る手段がない。なので_envsetup\00x1に置き換えていて、execve/envへ書き戻す前に0x1\0へ戻す実装となっていた。

そういった理由から、Plan 9ANSI/POSIX環境で、getenvを使って得た文字列の途中に0x1が見つかった場合、その文字列は配列として扱ってあげる必要がある。

バグ

余談だけどANSI/POSIX環境のgetenvenviron配列の要素を指すポインタを返す。putenvenviron配列を更新するが、getenvが返したポインタがまだ参照されている可能性があるため、以前の値を変更してはいけない。なので何度もputenvを呼び出ししていると、以前の値で使っていたメモリは消すことも再利用することもできずそのままリークする。setenvも最終的にはputenvと同じなので、同じようにリークする。

Plan 9ネイティブのgetenvmallocした値を返すので、使い終わったらユーザ側で適切にfreeしなければならない。

*1:他にもdialtokenizeなど割と多く残っている

*2:正確には、rforkのオプションにRFENVGRFCENVGがあって、これらを使って環境変数を子プロセスと共有したり分離したりといった操作をすることになる

*3:明らかに動いてなかったので修正した