PR

Foreign.C.Errorモジュールでエラー・コードを取得する

 では,FFIで他の言語のエラーを扱う方法を説明しましょう。

 CのライブラリやOSのAPIでは,処理が失敗した場合,関数が失敗を示す値を直接返すか,またはerrno.hのerrnoなどのグローバル変数にエラー・コード(エラー値)を書き込みます。したがって,Cで発生したI/OエラーをHaskellで扱うには,これらの情報を取得する必要があります。

 関数の戻り値はFFIでそのまま取得できるとして,グローバル変数の内容を取得するにはどうすればよいでしょうか?

 これには二つの方法があります。一つは変数に格納された値を返すための補助関数をC側で定義することです。もう一つは,&で名前を修飾することで直接アドレスをポインタとして取得する方法です(参考リンク1参考リンク2)。関数ポインタの呼び出しにも&を使います。ちなみに,GHCでは&を付けずに関数ポインタを呼び出せていたため,第22回ではこのあたりの事情を特に説明しませんでした。しかし,GHC6.10.1以降では,&を付けずに関数ポインタを呼び出すと警告されるようになりました。今後は関数ポインタを呼び出す際には必ず&を付けるようにしてください(参考リンク)。

 これらのいずれかの方法で,Foreign.C.Errorモジュールではerrno.hのerrnoからエラー・コードを取得するための関数getErrnoが定義されています。

getErrno :: IO Errno

-- We must call a C function to get the value of errno in general.  On
-- threaded systems, errno is hidden behind a C macro so that each OS
-- thread gets its own copy.
#ifdef __NHC__
getErrno = do e <- peek _errno; return (Errno e)
foreign import ccall unsafe "errno.h &errno" _errno :: Ptr CInt
#else
getErrno = do e <- get_errno; return (Errno e)
foreign import ccall unsafe "HsBase.h __hscore_get_errno" get_errno :: IO CInt
#endif

 ただし,errnoをただのグローバル変数として扱ってしまうと,マルチスレッド環境でgetErrnoを使用した場合,他のスレッドが未処理のエラーに対するエラー・コードを書き換えてしまう危険性があります。そこで,そうした現象を回避するために,スレッド安全なCのライブラリではerrnoなどをスレッド・ローカルな状態を取得するための関数またはマクロとして定義します(参考リンク1参考リンク2)。

 このように,関数やマクロ呼び出しとして実装されたerrnoは,もはやグローバル変数そのものではありません。アドレスの参照は関数実行後やマクロの展開後の値を見るわけではないので,グローバル変数のつもりでerrnoのアドレスの参照を行うと未定義の動作を引き起こす可能性があります。そこで問題を防ぐために,NHC以外の最近のHaskell処理系では,補助関数として用意した__hscore_get_errno越しにerrnoを呼び出して値を取得するようにしています。

 getErrnoが返すErrno型は,newtype宣言を使ったCIntの別名です。

newtype Errno = Errno CInt

 getErrnoで取得した後のエラー・コードは,resetErrnoを使うことで再び初期化できます。

Prelude Foreign.C.Error> :t resetErrno
resetErrno :: IO ()

 では,ErrnoとI/Oエラーを対応させて処理を行う方法を見ていきましょう。Foreign.C.Errorでは様々なエラー・コードをErrno型として定義しています。

eOK, e2BIG, eACCES, eADDRINUSE, eADDRNOTAVAIL, eADV, eAFNOSUPPORT, eAGAIN, 
  eALREADY, eBADF, eBADMSG, eBADRPC, eBUSY, eCHILD, eCOMM, eCONNABORTED, 
  eCONNREFUSED, eCONNRESET, eDEADLK, eDESTADDRREQ, eDIRTY, eDOM, eDQUOT, 
  eEXIST, eFAULT, eFBIG, eFTYPE, eHOSTDOWN, eHOSTUNREACH, eIDRM, eILSEQ, 
  eINPROGRESS, eINTR, eINVAL, eIO, eISCONN, eISDIR, eLOOP, eMFILE, eMLINK, 
  eMSGSIZE, eMULTIHOP, eNAMETOOLONG, eNETDOWN, eNETRESET, eNETUNREACH, 
  eNFILE, eNOBUFS, eNODATA, eNODEV, eNOENT, eNOEXEC, eNOLCK, eNOLINK, 
  eNOMEM, eNOMSG, eNONET, eNOPROTOOPT, eNOSPC, eNOSR, eNOSTR, eNOSYS, 
  eNOTBLK, eNOTCONN, eNOTDIR, eNOTEMPTY, eNOTSOCK, eNOTTY, eNXIO, 
  eOPNOTSUPP, ePERM, ePFNOSUPPORT, ePIPE, ePROCLIM, ePROCUNAVAIL, 
  ePROGMISMATCH, ePROGUNAVAIL, ePROTO, ePROTONOSUPPORT, ePROTOTYPE, 
  eRANGE, eREMCHG, eREMOTE, eROFS, eRPCMISMATCH, eRREMOTE, eSHUTDOWN, 
  eSOCKTNOSUPPORT, eSPIPE, eSRCH, eSRMNT, eSTALE, eTIME, eTIMEDOUT, 
  eTOOMANYREFS, eTXTBSY, eUSERS, eWOULDBLOCK, eXDEV                    :: Errno

 ただし,すべてのOSや環境がここに挙げたすべてのエラー・コードに対応しているわけではありません。使用している環境がエラー・コードに対応している場合には,e*に意味のある値として格納されます。一方,使用している環境がそのエラー・コードに対応していない場合には-1が格納されます。

Prelude Foreign.C.Error> case e2BIG of Errno x -> x
7
Prelude Foreign.C.Error> case eDEADLK of Errno x -> x
36
Prelude Foreign.C.Error> case eTIMEDOUT of Errno x -> x
-1

 データ構成子から値を取り出す方法以外に,isValidErrno関数を使うことでもe*に有意義な値が格納されているかどうかを調べられます。

Prelude Foreign.C.Error> :t isValidErrno
isValidErrno :: Errno -> Bool
Prelude Foreign.C.Error> isValidErrno e2BIG
True
Prelude Foreign.C.Error> isValidErrno eDEADLK
True
Prelude Foreign.C.Error> isValidErrno eTIME
False

 また,これらのエラー・コードを適切な種類のI/Oエラーに変換する関数としてerrnoToIOErrorが用意されています。GHCでのerrnoToIOErrorの定義は以下の通りです。

errnoToIOError  :: String       -- ^ the location where the error occurred
                -> Errno        -- ^ the error number
                -> Maybe Handle -- ^ optional handle associated with the error
                -> Maybe String -- ^ optional filename associated with the error
                -> IOError
errnoToIOError loc errno maybeHdl maybeName = unsafePerformIO $ do
    str <- strerror errno >>= peekCString
#if __GLASGOW_HASKELL__
    return (IOError maybeHdl errType loc str maybeName)
    where
    errType
        | errno == eOK             = OtherError
        | errno == e2BIG           = ResourceExhausted
        | errno == eACCES          = PermissionDenied
        | errno == eADDRINUSE      = ResourceBusy
        ~ 略 ~
        | errno == eTIME           = TimeExpired
        | errno == eTIMEDOUT       = TimeExpired
        | errno == eTOOMANYREFS    = ResourceExhausted
        | errno == eTXTBSY         = ResourceBusy
        | errno == eUSERS          = ResourceExhausted
        | errno == eWOULDBLOCK     = OtherError
        | errno == eXDEV           = UnsupportedOperation
        | otherwise                = OtherError
#else
    return (userError (loc ++ ": " ++ str ++ maybe "" (": "++) maybeName))
#endif

foreign import ccall unsafe "string.h" strerror :: Errno -> IO (Ptr CChar)

 これでは環境が対応していないエラー・コードをうまく扱えないように見えるかもしれませんが,心配は要りません。「==」演算子を定義しているEqクラスでは,内部でisValidErrnoを使用して結果がFalseになるようにしているからです。

instance Eq Errno where
  errno1@(Errno no1) == errno2@(Errno no2) 
    | isValidErrno errno1 && isValidErrno errno2 = no1 == no2
    | otherwise                                  = False

 対応していないエラー・コードはすべてOtherErrorに割り振られるため,誤って他の種類のI/Oエラーを作成することはありません。

 また,errnoToIOErrorではIOErrorTypeに加え,string.hのstrerrorを利用して,エラー・コードを説明する利用者向けのメッセージを設定しています(参考リンク1参考リンク2)。

 getErrnoとerrnoToIOErrorを組み合わせて行う処理は,多くの場合,定型的なものになります。しかし,同じようなコードがソースコードに何回も現れるのはあまり良いスタイルではありません。そこでForeign.C.Errorではthrow*という名前の様々な補助関数を提供しています。

 様々なthrow*関数の基本になるのが,throwErrno関数です。

throwErrno     :: String        -- ^ textual description of the error location
               -> IO a
throwErrno loc  =
  do
    errno <- getErrno
    ioError (errnoToIOError loc errno Nothing Nothing)

 この関数は,場所の情報を示す文字列とエラー・コードを基にI/Oエラーを作成し,そのエラーを発生させます。

 ファイルのパス名を渡せるようにしたthrowErrnoPath関数もあります。

throwErrnoPath :: String -> FilePath -> IO a
throwErrnoPath loc path =
  do
    errno <- getErrno
    ioError (errnoToIOError loc errno Nothing (Just path))

~ 略 ~

 I/O処理の結果を見てからI/Oエラーを発生させるかどうかを判断したい場合もあるでしょう。このために用意されているのが,throwErrnoIf関数とthrowErrnoIf_関数です。

throwErrnoIf    :: (a -> Bool)  -- ^ predicate to apply to the result value
                                -- of the 'IO' operation
                -> String       -- ^ textual description of the location
                -> IO a         -- ^ the 'IO' operation to be executed
                -> IO a
throwErrnoIf pred loc f  = 
  do
    res <- f
    if pred res then throwErrno loc else return res

-- | as 'throwErrnoIf', but discards the result of the 'IO' action after
-- error handling.
--
throwErrnoIf_   :: (a -> Bool) -> String -> IO a -> IO ()
throwErrnoIf_ pred loc f  = void $ throwErrnoIf pred loc f

 実装からわかるように,I/O処理の実行結果が条件式で真になった場合にI/Oエラーを発生させます。throwErrnoIf_関数は,Foreign.Marshal.Errorモジュールのvoid関数を使うことで最終的な結果を破棄します。

Prelude Foreign.Marshal.Error> :t void
void :: IO a -> IO ()

 ただ,失敗時にエラー・コードを書き換えるようなI/O処理では,処理が失敗したときには-1または空のポインタ(NULL)を返すのが普通です(参考リンク)。そこで,throwErrnoIfにはthrowErrnoIfMinus1やthrowErrnoIfNullといった特別なバージョンの関数も用意されています。

throwErrnoIfMinus1 :: Num a => String -> IO a -> IO a
throwErrnoIfMinus1  = throwErrnoIf (== -1)

~ 略 ~
throwErrnoIfNull :: String -> IO (Ptr a) -> IO (Ptr a)
throwErrnoIfNull  = throwErrnoIf (== nullPtr)

 Cの一般的な慣習に従って,偽である0を失敗時に返す場合もあるかもしれません。残念ながら,2008年12月現在のForeign.C.ErrorモジュールにはthrowErrnoIfZeroという関数は用意されていません。throwErrnoIf関数の条件式にForeign.Marshal.UtilsモジュールのtoBool関数などを渡してやる必要があります。

Prelude Foreign.Marshal.Utils> toBool 0
False
Prelude Foreign.Marshal.Utils> toBool 1
True
Prelude Foreign.Marshal.Utils> toBool (-1)
True

 Foreign.Marshal.Utilsモジュールには,HaskellのBool型をCの関数に渡す数値に変換するfromBoolという関数もあります。

fromBool       :: Num a => Bool -> a
fromBool False  = 0
fromBool True   = 1

 もちろん,I/Oエラーを発生させるだけが選択肢ではありません。I/O処理の失敗の原因が割り込みや処理のブロックである場合,I/Oエラーを発生させるよりも,処理を再度実行するほうがよいでしょう。このために用意されているのが,throwErrnoIfRetryとthrowErrnoIfRetryMayBlockです。

throwErrnoIfRetry            :: (a -> Bool) -> String -> IO a -> IO a
throwErrnoIfRetry pred loc f  = 
  do
    res <- f
    if pred res
      then do
        err <- getErrno
        if err == eINTR
          then throwErrnoIfRetry pred loc f
          else throwErrno loc
      else return res

-- | as 'throwErrnoIfRetry', but checks for operations that would block and
-- executes an alternative action before retrying in that case.
--
throwErrnoIfRetryMayBlock
                :: (a -> Bool)  -- ^ predicate to apply to the result value
                                -- of the 'IO' operation
                -> String       -- ^ textual description of the location
                -> IO a         -- ^ the 'IO' operation to be executed
                -> IO b         -- ^ action to execute before retrying if
                                -- an immediate retry would block
                -> IO a
throwErrnoIfRetryMayBlock pred loc f on_block  = 
  do
    res <- f
    if pred res
      then do
        err <- getErrno
        if err == eINTR
          then throwErrnoIfRetryMayBlock pred loc f on_block
          else if err == eWOULDBLOCK || err == eAGAIN
                 then do on_block; throwErrnoIfRetryMayBlock pred loc f on_block
                 else throwErrno loc
      else return res

 throwErrnoIfRetryMayBlockでは,ブロック可能な処理を記述するために,ブロックされた場合に行うべき処理を与えられるようになっています。

 ほかにも,ここまで説明した関数の機能や性質を組み合わせた様々な亜種の関数が存在します。throwErrnoIfMinus1Retry_やthrowErrnoPathIfNullのように素直に機能を組み合わせた名前になっているので,いちいち説明する必要はないでしょう。これらの関数について興味のある方は,Foreign.C.Errorモジュールのドキュメントを参照してください。

 では,実際に試してみましょう。以下のようなプログラムを用意します。

foreignerr.h:

#include <fcntl.h>
#include <sys/types.h>

int copen (const char*);

foreignerr.c:

#include "foreignerr.h"

int copen (const char *filename) {
  return open (filename, O_RDONLY);
}

ForeignError.hs

{-# INCLUDE "foreignerr" #-}
{-# LANGUAGE ForeignFunctionInterface #-}
module ForeignError where
import Foreign.C
import IOError

open str = do
    withCString str c_open
    throwErrno $ "can't open " ++ str

open' str
 = throwErrnoIfMinus1_
       ("can't open " ++ str)
       $ withCString str c_open

foreign import ccall "copen" c_open :: CString -> IO CInt

 Foreign.C.Error,Foreign.C.String,Foreign.C.Typesの三つのモジュールは,すべてForeign.Cモジュールからインポートしています。withCStringは,Haskellの文字列をCの文字列に変換して処理を行うための関数です。Foreign.C.Stringで定義されています。

Prelude Foreign.C.String> :t withCString
withCString :: String -> (CString -> IO a) -> IO a
Prelude Foreign.C.String> :i CString
type CString = GHC.Ptr.Ptr Foreign.C.Types.CChar
        -- Defined in Foreign.C.String

 結果は以下のようになります。

$ gcc -c foreignerr.c

$ ghci ForeignError.hs foreignerr.o
~ 略 ~
Ok, modules loaded: ForeignError.
*ForeignError> open "nofile"
*** Exception: can't open nofile: does not exist (No such file or directory)
*ForeignError> open' "nofile"
*** Exception: can't open nofile: does not exist (No such file or directory)
*ForeignError> catchIOError $ open "nofile"
"error caught."
*ForeignError> catchIOError $ open' "nofile"
"error caught."

 存在しないnofileを開こうとしたopen*から,そのことを示すエラー・メッセージが出力されているのがわかりますね。IOErrorのcatchIOErrorで捕捉できているので,このエラーがI/Oエラーであることを確認できています。

 なお,errnoToIOErrorはEOKからもI/Oエラーを作成するので,openでは処理が成功した場合でもI/Oエラーが発生してしまいます。

*ForeignError> open' "ForeignError.hs"
*ForeignError> open "ForeignError.hs"
*** Exception: can't open ForeignError.hs: failed (No error)