24

WindowsのHaskellでコンソールI/OをUnicode文字で動作させるのはかなり難しいようです。これが悲惨な話です:

  1. (暫定版)WindowsのコンソールでUnicode I / Oを実行することを検討する前に、必要な文字をレンダリングできるコンソールフォントを使用していることを確認する必要があります。ラスターフォント(デフォルト)のカバレッジは無限に低く(表現できない文字のコピー貼り付けは許可されません)、MSが提供するTrueTypeオプション(consolas、lucidaコンソール)のカバレッジはそれほど大きくありません(ただし、これらは可能になります)表現できない文字のコピー/貼り付け)。DejaVu Sans Monoのインストールを検討することもできます(ここの下部にある手順に従ってください。動作する前に再起動する必要がある場合があります)。これがソートされるまで、どのアプリも多くのUnicode I/Oを実行できません。Haskellだけではありません。
  2. これを行うと、一部のアプリがWindowsでコンソールI/Oを実行できるようになります。しかし、それを機能させることは非常に複雑なままです。ウィンドウの下でコンソールに書き込むには、基本的に2つの方法があります。(以下は、Haskellだけでなく、すべての言語に当てはまります。心配しないでください。Haskellは少し後で画像に入ります!)...
  3. オプションAは、通常のc-libraryスタイルのバイトベースのI/O関数を使用することです。希望は、OSがこれらのバイトを、必要なすべての奇妙で素晴らしい文字をエンコードできるエンコードに従って解釈することです。たとえば、標準のシステムエンコーディングが通常UTF8であるMac OS Xで同等の手法を使用すると、これはうまく機能します。utf8出力を送信すると、きれいなシンボルが表示されます。
  4. Windowsでは、うまく機能しません。Windowsが期待するデフォルトのエンコーディングは、通常、すべてのUnicodeシンボルをカバーするエンコーディングではありません。したがって、この方法できれいなシンボルを表示したい場合は、エンコーディングを変更する必要があります。1つの可能性は、プログラムがSetConsoleCPwin32コマンドを使用することです。(したがって、Win32ライブラリにバインドする必要があります。)または、それを行わない場合は、プログラムのユーザーがコードページを変更することを期待できます(その後、ユーザーはchcpコマンドを実行する前にコマンドを呼び出す必要があります。プログラム)。
  5. オプションBは、のようなUnicode対応のwin32コンソールAPIコマンドを使用することWriteConsoleWです。ここでは、UTF16をWindowsに直接送信します。これにより、UTF16が適切にレンダリングされます。Windowsは常にこれらの関数を備えたUTF16を想定しているため、エンコードの不一致の危険はありません。

残念ながら、これらのオプションはどちらもHaskellではうまく機能しません。まず、オプションBを使用していることを私が知っているライブラリはないので、それは非常に簡単ではありません。これにより、オプションAが残ります。HaskellのI / Oライブラリ(putStrLnなど)を使用する場合、これはライブラリが行うことです。Haskellの最新バージョンでは、現在のコードページが何であるかをウィンドウに注意深く尋ね、適切なエンコーディングで文字列を出力します。このアプローチには2つの問題があります。

  • 1つはショートッパーではありませんが、迷惑です。上記のように、デフォルトのエンコーディングでは、必要な文字がエンコードされることはほとんどありません。ユーザーは、エンコードに変更する必要があります。したがって、ユーザーはchcp cp65001プログラムを実行する前に実行する必要があります(ユーザーにこれを強制するのは不快な場合があります)。SetConsoleCPまたは、プログラム内で同等の機能をバインドして実行する必要があります(次にhSetEncoding、Haskellライブラリが新しいエンコーディングを使用して出力を送信するように使用します)。つまり、win32ライブラリの関連部分をラップして、Haskellを表示できるようにする必要があります。 。
  • さらに深刻なことに、Windowsにバグがあり(解決策:修正されません)、Haskellのバグにつながります。つまり、Unicodeのすべてをカバーできるcp65001のようなコードページを選択した場合、HaskellのI/Oルーチンは誤動作して失敗します。したがって、基本的に、あなた(またはあなたのユーザー)がすべての素晴らしいUnicode文字をカバーするエンコーディングに適切にエンコーディングを設定し、Haskellにそのエンコーディングを使用して出力するように指示する際に「すべてを正しく行う」としても、あなたはまだ負けます。

上記のバグはまだ解決されておらず、優先度が低いと記載されています。基本的な結論は、オプションA(上記の私の分類では)は実行不可能であり、信頼できる結果を得るにはオプションBに切り替える必要があるということです。かなりの作業のように見えるため、これが解決されるまでの時間枠は明確ではありません。

問題は、それまでの間、WindowsのHaskellでUnicodeコンソールI/Oを使用できるようにするための回避策を誰かが提案できるかどうかです。

このPythonバグトラッカーデータベースエントリも参照してください。Python3で同じ問題に取り組んでいます(修正は提案されていますが、コードベースにはまだ受け入れられていません)。このスタックオーバーフローの回答は、Pythonでのこの問題の回避策を示しています(「オプションB」に基づく)私の分類では)。

4

1 に答える 1

20

私は自分の質問に答えて、考えられる答えの1つとして、現在実際に行っていることを次のように挙げようと思いました。もっとうまくやれる可能性は十分にあるので、私は質問をしています!しかし、私は以下を人々が利用できるようにすることは理にかなっていると思いました。これは基本的に、同じ問題に対するこのpython回避策のPythonからHaskellへの翻訳です。質問で言及されている「オプションB」を使用します。

基本的な考え方は、次のコンテンツを含むモジュールIOUtil.hsを作成importし、コードに組み込むことです。

{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE NoImplicitPrelude #-}
module IOUtil (
  IOUtil.interact,
  IOUtil.putChar, IOUtil.putStr, IOUtil.putStrLn, IOUtil.print,
  IOUtil.getChar, IOUtil.getLine, IOUtil.getContents, IOUtil.readIO,
  IOUtil.readLn,
  ePutChar, ePutStr, ePutStrLn, ePrint,
  trace, traceIO
  ) where

#ifdef mingw32_HOST_OS

import System.Win32.Types (BOOL, HANDLE, DWORD, LPDWORD, LPWSTR, LPCWSTR, LPVOID)
import Foreign.C.Types (CWchar)
import Foreign
import Prelude hiding (getContents, putStr, putStrLn) --(IO, Read, Show, String)
--import qualified System.IO
import qualified System.IO (getContents)
import System.IO hiding (getContents, putStr, putStrLn)
import Data.Char (ord)

 {- <http://msdn.microsoft.com/en-us/library/ms683231(VS.85).aspx>
    HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
    returns INVALID_HANDLE_VALUE, NULL, or a valid handle -}

foreign import stdcall unsafe "GetStdHandle" win32GetStdHandle :: DWORD -> IO (HANDLE)

std_OUTPUT_HANDLE = -11 :: DWORD  -- all DWORD arithmetic is performed modulo 2^n
std_ERROR_HANDLE  = -12 :: DWORD

 {- <http://msdn.microsoft.com/en-us/library/aa364960(VS.85).aspx>
    DWORD WINAPI GetFileType(HANDLE hFile); -}

foreign import stdcall unsafe "GetFileType" win32GetFileType :: HANDLE -> IO (DWORD)
_FILE_TYPE_CHAR   = 0x0002 :: DWORD
_FILE_TYPE_REMOTE = 0x8000 :: DWORD

 {- <http://msdn.microsoft.com/en-us/library/ms683167(VS.85).aspx>
    BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); -}

foreign import stdcall unsafe "GetConsoleMode" win32GetConsoleMode :: HANDLE -> LPDWORD -> IO (BOOL)
_INVALID_HANDLE_VALUE = (intPtrToPtr $ -1) :: HANDLE

is_a_console :: HANDLE -> IO (Bool)
is_a_console handle
  = if (handle == _INVALID_HANDLE_VALUE) then return False
      else do ft <- win32GetFileType handle
              if ((ft .&. complement _FILE_TYPE_REMOTE) /= _FILE_TYPE_CHAR) then return False
                else do ptr <- malloc
                        cm  <- win32GetConsoleMode handle ptr
                        free ptr
                        return cm

real_stdout :: IO (Bool)
real_stdout = is_a_console =<< win32GetStdHandle std_OUTPUT_HANDLE

real_stderr :: IO (Bool)
real_stderr = is_a_console =<< win32GetStdHandle std_ERROR_HANDLE

 {- BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars,
                              LPDWORD lpCharsWritten, LPVOID lpReserved); -}

foreign import stdcall unsafe "WriteConsoleW" win32WriteConsoleW
  :: HANDLE -> LPWSTR -> DWORD -> LPDWORD -> LPVOID -> IO (BOOL)

data ConsoleInfo = ConsoleInfo Int (Ptr CWchar) (Ptr DWORD) HANDLE

writeConsole :: ConsoleInfo -> [Char] -> IO ()
writeConsole (ConsoleInfo bufsize buf written handle) string
  = let fillbuf :: Int -> [Char] -> IO ()
        fillbuf i [] = emptybuf buf i []
        fillbuf i remain@(first:rest)
          | i + 1 < bufsize && ordf <= 0xffff = do pokeElemOff buf i asWord
                                                   fillbuf (i+1) rest
          | i + 1 < bufsize && ordf >  0xffff = do pokeElemOff buf i word1
                                                   pokeElemOff buf (i+1) word2
                                                   fillbuf (i+2) rest
          | otherwise                         = emptybuf buf i remain
          where ordf   = ord first
                asWord = fromInteger (toInteger ordf) :: CWchar
                sub    = ordf - 0x10000
                word1' = ((shiftR sub 10) .&. 0x3ff) + 0xD800
                word2' = (sub .&. 0x3FF)             + 0xDC00
                word1  = fromInteger . toInteger $ word1'
                word2  = fromInteger . toInteger $ word2'


        emptybuf :: (Ptr CWchar) -> Int -> [Char] -> IO ()
        emptybuf _ 0 []     = return ()
        emptybuf _ 0 remain = fillbuf 0 remain
        emptybuf ptr nLeft remain
          = do let nLeft'    = fromInteger . toInteger $ nLeft
               ret          <- win32WriteConsoleW handle ptr nLeft' written nullPtr
               nWritten     <- peek written
               let nWritten' = fromInteger . toInteger $ nWritten
               if ret && (nWritten > 0)
                  then emptybuf (ptr `plusPtr` (nWritten' * szWChar)) (nLeft - nWritten') remain
                  else fail "WriteConsoleW failed.\n"

    in  fillbuf 0 string

szWChar = sizeOf (0 :: CWchar)

makeConsoleInfo :: DWORD -> Handle -> IO (Either ConsoleInfo Handle)
makeConsoleInfo nStdHandle fallback
  = do handle     <- win32GetStdHandle nStdHandle
       is_console <- is_a_console handle
       let bufsize = 10000
       if not is_console then return $ Right fallback
         else do buf     <- mallocBytes (szWChar * bufsize)
                 written <- malloc
                 return . Left $ ConsoleInfo bufsize buf written handle

{-# NOINLINE stdoutConsoleInfo #-}
stdoutConsoleInfo :: Either ConsoleInfo Handle
stdoutConsoleInfo = unsafePerformIO $ makeConsoleInfo std_OUTPUT_HANDLE stdout

{-# NOINLINE stderrConsoleInfo #-}
stderrConsoleInfo :: Either ConsoleInfo Handle
stderrConsoleInfo = unsafePerformIO $ makeConsoleInfo std_ERROR_HANDLE stderr

interact     :: (String -> String) -> IO ()
interact f   = do s <- getContents
                  putStr (f s)

conPutChar ci  = writeConsole ci . replicate 1
conPutStr      = writeConsole
conPutStrLn ci = writeConsole ci . ( ++ "\n")

putChar      :: Char -> IO ()
putChar      = (either conPutChar  hPutChar ) stdoutConsoleInfo

putStr       :: String -> IO ()
putStr       = (either conPutStr   hPutStr  ) stdoutConsoleInfo

putStrLn     :: String -> IO ()
putStrLn     = (either conPutStrLn hPutStrLn) stdoutConsoleInfo

print        :: Show a => a -> IO ()
print        = putStrLn . show

getChar      = System.IO.getChar
getLine      = System.IO.getLine
getContents  = System.IO.getContents

readIO       :: Read a => String -> IO a
readIO       = System.IO.readIO

readLn       :: Read a => IO a
readLn       = System.IO.readLn

ePutChar     :: Char -> IO ()
ePutChar     = (either conPutChar  hPutChar ) stderrConsoleInfo

ePutStr     :: String -> IO ()
ePutStr      = (either conPutStr   hPutStr  ) stderrConsoleInfo

ePutStrLn   :: String -> IO ()
ePutStrLn    = (either conPutStrLn hPutStrLn) stderrConsoleInfo

ePrint       :: Show a => a -> IO ()
ePrint       = ePutStrLn . show

#else

import qualified System.IO
import Prelude (IO, Read, Show, String)

interact     = System.IO.interact
putChar      = System.IO.putChar
putStr       = System.IO.putStr
putStrLn     = System.IO.putStrLn
getChar      = System.IO.getChar
getLine      = System.IO.getLine
getContents  = System.IO.getContents
ePutChar     = System.IO.hPutChar System.IO.stderr
ePutStr      = System.IO.hPutStr System.IO.stderr
ePutStrLn    = System.IO.hPutStrLn System.IO.stderr

print        :: Show a => a -> IO ()
print        = System.IO.print

readIO       :: Read a => String -> IO a
readIO       = System.IO.readIO

readLn       :: Read a => IO a
readLn       = System.IO.readLn

ePrint       :: Show a => a -> IO ()
ePrint       = System.IO.hPrint System.IO.stderr

#endif

trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
    traceIO string
    return expr

traceIO :: String -> IO ()
traceIO = ePutStrLn

次に、標準ライブラリの代わりに、そこに含まれているI/O関数を使用します。出力がリダイレクトされているかどうかを検出します。そうでない場合(つまり、「実際の」コンソールに書き込んでいる場合)、通常のHaskell I / O関数をバイパスしWriteConsoleW、Unicode対応のwin32コンソール関数を使用してwin32コンソールに直接書き込みます。Windows以外のプラットフォームでは、条件付きコンパイルとは、ここでの関数が標準ライブラリの関数を呼び出すことを意味します。

stderrに出力する必要がある場合は、;ePutStrLnではなく(eg)を使用する必要があります。hPutStrLn stderrを定義しませんhPutStrLn。(1つを定義することは読者のための練習です!)

于 2012-05-28T03:50:06.200 に答える