Previous Up Next

 1  基礎

1.1  Sys モジュールと Unix モジュール

OCamlからシステムにアクセスするときに使われる関数は SysUnix の二つのモジュールにまとめられています。一つ目の Sys モジュールは OCamlが実行される Unix およびその他のオペレーティングシステムで一般的な関数を含みます。二つ目の Unix モジュールは Unix に特有なものを全て含みます。

これ以降 SysUnix モジュールにある識別子はどちらのモジュールのものかを示すことなく使うことにします。つまり open Sys および open Unix を実行した状態であるということです。完全な例を示すときには、open Sysopen Unix を明示的に書くことにします。

Sys および Unix モジュールは Pervasives モジュールに定義されている変数を上書きし、元の定義を隠してしまうことがあるので注意してください。例えば、 Pervasives.stdinUnix.stdin は別物です。隠された定義にはプリフィックスをつけることでアクセスできます。

Unix ライブラリを使う OCamlのプログラムをコンパイルするには、次のようにします:

ocamlc -o prog unix.cma mod1.ml mod2.ml mod3.ml

ここで prog というプログラムは mod1, mod2 そして mod3 という三つのモジュールから成ります。モジュールは別々にコンパイルすることもできます:

ocamlc -c mod1.ml ocamlc -c mod2.ml ocamlc -c mod3.ml

この場合、次のようにしてリンクします:

ocamlc -o prog unix.cma mod1.cmo mod2.cmo mod3.cmo

両方の例において、引数 unix.cma は OCamlで書かれた Unix ライブラリを表します。バイトコードコンパイラではなくネイティブコードコンパイラを使うには、 ocamlcocamlopt に、unix.cmaunix.cmxa に置き換えてください。

コンパイルツール ocamlbuild を使っている場合、次の内容を _tags ファイルに追加してください:

<prog.{native,byte}> : use_unix

“toplevel” を言われる対話環境から Unix システムにアクセスすることもできます。実行している環境が C ライブラリの動的リンクに対応している場合、OCamlトップレベルを起動して次のディレクティブを入力します:

#load "unix.cma";;

動的リンクに対応していない場合、システム関数がプリロードされた対話環境を作る必要があります:

ocamlmktop -o ocamlunix unix.cma

このトップレベルは次のコマンドで起動できます:

./ocamlunix

1.2  プログラムを呼ぶためのインターフェース

シェル (コマンドインタープリタ) からプログラムを実行する場合、シェルは 引数環境 を実行するプログラムに渡します。引数とはコマンドライン上でプログラムの名前の後ろに続く語です。環境とは variable=value の形をした文字列の集まりであり、環境変数のバインディングを表します。このバインディングは csh では setenv var=val で、 sh では var=val; export varでセットされます。

プログラムに渡された引数は文字列の配列 Sys.argv に格納されます:

val argv : string array

プログラムの環境は Unix.environment 関数で取得できます:

val environment : unit -> string array

Sys.getenv 関数を使えば環境をより簡単に検索できます。

val getenv : string -> string

Sys.getenv vv という環境変数に結び付けられた値を返します。環境変数が見つからなかった場合は Not_found 例外を出します。

最初の例として、引数を出力する echo プログラムを示します。これは同じ名前の Unix コマンドと同じ動作です。

let echo () = let len = Array.length Sys.argv in if len > 1 then begin print_string Sys.argv.(1); for i = 2 to len - 1 do print_char ' '; print_string Sys.argv.(i); done; print_newline (); end;; echo ();;
* * *

プログラムは exit を呼ぶことで任意の場所で終了させることができます。

val exit : int -> 'a

引数は呼び出し元のプログラムに送られる返り値です。問題のない場合には 0 を、エラーが起こった場合には0でない値を返すという慣習があります。プログラムの実行結果が条件として使われた場合、sh シェルは返り値 0 をブール値 “true” に、0 でない全ての返り値を “false” として解釈します。

プログラムが全ての式を実行し終わって終了する場合、そのプログラムは暗黙的に exit 0 を呼びます。プログラムが補足されない例外によって途中で実行を終了する場合、そのプログラムは暗黙的に exit 2 を呼びます。

exit 関数は呼ばれたときに書き込み用にオープンされている全てのチャンネルのバッファをフラッシュします。at_exit 関数を使うと、プログラムが終了するときにこれ以外の動作をさせることができます。

val at_exit : (unit -> unit) -> unit

最後に登録された関数が最初に実行されます。at_exit 関数を使って登録された関数は登録を解除することができませんが、これが本質的な制限になることはありません。グローバル変数を使って実行を変えることができるからです。

1.3  エラー処理

他に明示されていない限り、Unix モジュールの全ての関数はエラーが起きたときに Unix_error 例外を出します。

exception Unix_error of error * string * string

Unix_error 例外の第二引数はエラーが起こったシステムコールの名前です。第三引数はエラーが起こったオブジェクトの名前を(可能な場合には)表します。例えば、ファイルの名前を引数として取るシステムコールの場合には、このファイルの名前が Unix_error の第三引数となります。最後に、第一引数はエラーの種類を表すエラーコードを表します。エラーコードは error というヴァリアント型に属しています。

type error = E2BIG | EACCES | EAGAIN | ... | EUNKNOWNERR of int

この型のコンストラクタには posix で定義されるエラーが同じ名前と意味ですべて含まれ、加えて unix98, bsd のエラーの一部が含まれます。その他の全てのエラーは EUNKOWNERR というコンストラクタになります。

例外が発生したとき、 try によって補足されないエラーはプログラムの一番上まで上がっていき、プログラムを実行の途中で終了させます。小さいアプリケーションでは予見できないエラーを致命的なものとみなすことは良い習慣です。しかしその場合、エラーを分かりやすく表示することが望ましいです。エラーを分かりやすく表示するために、 Unix モジュールには handle_unix_error 関数があります:

val handle_unix_error : ('a -> 'b) -> 'a -> 'b

handle_unix_error f x はまず引数 x を関数 f に適用します。この適用が Unix_error を出した場合、エラーを説明するメッセージが表示され、exit 2 によってプログラムは終了します。次のプログラムは典型的な使用例です:

handle_unix_error prog ();;

ここで関数 prog : unit -> unit がプログラム本体を実行します。参考のために、 handle_unix_error の実装を以下に示します。

1 open Unix;; 2 let handle_unix_error f arg = 3 try 4 f arg 5 with Unix_error(err, fun_name, arg) -> 6 prerr_string Sys.argv.(0); 7 prerr_string ": \""; 8 prerr_string fun_name; 9 prerr_string "\" failed"; 10 if String.length arg > 0 then begin 11 prerr_string " on \""; 12 prerr_string arg; 13 prerr_string "\"" 14 end; 15 prerr_string ": "; 16 prerr_endline (error_message err); 17 exit 2;;

prerr_xxx の形をした関数は基本的には print_xxx 関数と同じ動作をしますが、書き込み先は stdout ではなく stderr となります。

error -> string 型をもつ error_message は引数の番号が表すエラーを説明するメッセージを返します (第 16 行)。プログラムに渡される第 0 引数 Sys.argv.(0) にはプログラムを起動するのに使われたコマンドが格納されます (第 6 行)。

handle_unix_error 関数がプログラムの実行を終了させるような致命的なエラーを処理します。OCamlを使うことの利点は全てのエラーを明示的に処理することが求められ、エラーが発生するとプログラムの実行が終了することになるトップレベルでさえこれが求められることです。実際システムコールによるどんなエラーも例外を発生させるので、プログラムの実行は中断され、例外は明示的に補足・処理されるまで上に登る事になります。これによってプログラムが不整合状態で実行が続くことを防ぐことができます。

Unix_error 型のエラーにはもちろんパターンマッチを使うことができます。次の関数はこれからよく目にすることになります:

let rec restart_on_EINTR f x = try f x with Unix_error (EINTR, _, _) -> restart_on_EINTR f x

このコードは関数を実行してもし中断された場合にはもう一度繰り返すという処理を行います(4.5 節を参照)。

1.4  ライブラリ関数

これから例を通して見ていくことですが、システムプログラミングでは同じパターンの処理が繰り返し出てきます。アプリケーションのコードが本質的な部分だけを含むように、共通する処理をまとめたライブラリ関数を定義しておくことが望ましいです。

自分で書いて自分で実行するプログラムではどんなエラーが出て、そのうちどれが実行を終了させるような致命的なエラーかが分かるものですが、ライブラリ関数の場合には実行されるコンテキストが分からないのでどれが致命的なエラーなのかは通常分かりません。かといって全てのエラーが致命的であると仮定することもできません。そのためプログラムを止めるのか、それとも無視するのか呼び出し元に判断させるために、エラーを呼び出し元に伝えることが必要になります。

しかし、ライブラリ関数を普通に実装すると発生したエラーをそのまま呼び出し元に伝えることができません。システムを整合状態に置くことが求められるためです。例えばファイルを開いてそのファイルディスクリプタを使って操作を行うライブラリ関数は、ファイルへの操作でエラーが生じた場合を含めた全ての場合においてファイルディスクリプタを閉じる処理を行う必要があります。ファイルディスクリプタがリークしてファイルディスクリプタを使いきってしまうことを防ぐためです。

ファイルに対する操作を引数として受け取る場合もあります。この場合、いつどのように操作が失敗するかを知ることは (呼び出した側でなければ) できません。そのため操作の本体は “最終処理” コードで守ることが必要になります。このコードは関数が例外を出したかどうかにかかわらず、関数が帰る直前に実行されます。

tryfinalize 構文は OCamlにビルトインでは用意されていませんが、簡単に定義することができます 1

let try_finalize f x finally y = let res = try f x with exn -> finally y; raise exn in finally y; res

この関数はメインの処理 f と最終処理 finally およびそれらの引数 xy を受け取ります。最初にプログラムの本体 f x が実行され、その結果は最終処理 finally y が実行されてから返されます。プログラムの実行で例外 exn が起こった場合、最終処理が実行されてからもう一度 exn を出します。メインの処理と最終処理の両方が失敗した場合、最終処理の例外が出されます(メインの処理の例外が出されるようにすることもできます)。

ノート

これからこのコースでは例でよく使う try_finalize などの関数をまとめた補助ライブラリ Misc を使います。必要に応じてライブラリの関数を紹介するほか、インターフェースは付録にあります。このコースに出てくる例をコンパイルするには、 Misc モジュールの定義をまとめておく必要があります。

Misc モジュールにはこのコースでは直接使用しない可視化のための関数も含まれています。これらの関数は Unix ライブラリを強化するためのもので、いくつかの関数の振る舞いを上書きします。そのため Misc ライブラリを使う場合は Unix の後に読み込まれる必要があります。

このコースにはたくさんの例が含まれています。これらはバージョン 4.05.0の OCamlでコンパイルされることを確認しています。古いバージョンではプログラムを若干改変する必要があります。

例には二つの種類があります: とても一般的で再利用が可能な “ライブラリ関数” と小さなアプリケーションです。これら二つを区別することは重要です。ライブラリ関数の場合には実行時のコンテキストをできるだけ一般的なものと仮定して、インターフェースを熟慮し、全ての特殊ケースを扱うようにします。一方小さなアプリケーションの場合には、多くのエラーは致命的なものであり、プログラムの実行を停止させます。そのためエラーが起きた時にはその原因を伝えるだけで十分であり、システムを整合状態へと戻す処理は必要ありません。


1
構文がビルトインで用意されれば関数を定義して使うよりも便利でしょう。

Previous Up Next