24

在 Windows 下的 Haskell 中,让控制台 I/O 与 Unicode 字符一起工作似乎相当困难。这是悲惨的故事:

  1. (初步的。)在您考虑在 windows 下的控制台中执行 Unicode I/O 之前,您需要确保您使用的控制台字体可以呈现您想要的字符。光栅字体(默认设置)的覆盖范围极差(并且不允许复制粘贴它们无法表示的字符),MS 提供的 truetype 选项(consolas、lucida 控制台)的覆盖范围不是很大(尽管这些将允许复制/粘贴他们无法代表的字符)。您可能会考虑安装 DejaVu Sans Mono(按照此处底部的说明进行操作;您可能需要重新启动才能正常工作)。在排序之前,没有应用程序能够进行大量的 Unicode I/O;不仅仅是哈斯克尔。
  2. 完成此操作后,您会注意到一些应用程序将能够在 Windows 下进行控制台 I/O。但让它发挥作用仍然相当复杂。在windows下写入控制台基本上有两种方式。(以下内容适用于任何语言,不只是 Haskell;别担心,Haskell 稍后会出现!)...
  3. 选项 A 是使用通常的 c 库风格的基于字节的 i/o 函数;希望操作系统会根据某种编码来解释这些字节,这些编码可以编码你想要的所有奇怪而美妙的字符。例如,在标准系统编码通常为 UTF8 的 Mac OS X 上使用等效的技术,效果很好;你发出 utf8 输出,你会看到漂亮的符号。
  4. 在 Windows 上,它的效果不太好。windows 期望的默认编码通常不会是覆盖所有 Unicode 符号的编码。因此,如果您想以这种或另一种方式看到漂亮的符号,您需要更改编码。一种可能是您的程序使用SetConsoleCPwin32 命令。(那么你需要绑定到 Win32 库。)或者,如果你不想这样做,你可以期望你的程序的用户为你更改代码页(然后他们必须chcp在运行你的之前调用命令程序)。
  5. 选项 B 是使用支持 Unicode 的 win32 控制台 API 命令,例如WriteConsoleW. 在这里,您将 UTF16 直接发送到 windows,这很高兴地呈现它:不存在编码不匹配的危险,因为 windows总是期望 UTF16 具有这些功能。

不幸的是,这些选项在 Haskell 中都不能很好地工作。首先,据我所知,没有使用选项 B 的库,所以这不是很容易。这留下了选项 A。如果您使用 Haskell 的 I/O 库(putStrLn等等),这就是库将执行的操作。在现代版本的 Haskell 中,它会仔细询问窗口当前代码页是什么,并以正确的编码输出您的字符串。这种方法有两个问题:

  • 一个不是炫耀,但很烦人。如上所述,默认编码几乎不会对您想要的字符进行编码:您是用户需要更改为这样的编码。因此,您的用户chcp cp65001在运行您的程序之前需要这样做(您可能会发现强迫您的用户这样做是令人反感的)。或者您需要在SetConsoleCP程序中绑定并执行等效操作(然后使用hSetEncoding以便 Haskell 库将使用新编码发送输出),这意味着您需要包装 win32 库的相关部分以使它们在 Haskell 中可见.
  • 更严重的是,windows 中存在一个错误(解决方案:不会修复)导致Haskell 中的错误,这意味着如果您选择了任何代码页,如 cp65001 可以覆盖所有 Unicode,Haskell 的 I/O 例程将故障和失败。因此,本质上,即使您(或您的用户)将编码正确设置为涵盖所有精彩 Unicode 字符的某种编码,然后在告诉 Haskell 使用该编码输出内容时“做所有正确的事情”,您仍然会输。

上面列出的错误仍未解决,列为低优先级;基本结论是选项 A(在我上面的分类中)是不可行的,需要切换到选项 B 以获得可靠的结果。目前尚不清楚解决此问题的时间表,因为这看起来需要做一些相当大的工作。

问题是:与此同时,任何人都可以提出一种解决方法来允许在 Windows 下的 Haskell 中使用 Unicode 控制台 I/O。

另请参阅此python 错误跟踪器数据库条目,解决 Python 3 中的相同问题(已提出修复,但尚未被代码库接受),以及此 stackoverflow 答案,为 Python 中的此问题提供解决方法(基于“选项 B”在我的分类中)。

4

1 回答 1

20

我想我会回答我自己的问题,并列出一个可能的答案,以下是我目前正在做的事情。很有可能一个人可以做得更好,这就是我问这个问题的原因!但我认为将以下内容提供给人们是有意义的。对于同一问题,它基本上是从 Python 到 Haskell 的这个 python 解决方法的翻译。它使用问题中提到的“选项 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 函数而不是标准库函数。他们会检测输出是否被重定向;WriteConsoleW如果不是(即如果我们正在写入“真实”控制台),那么我们将绕过通常的 Haskell I/O 函数并使用支持 unicode 的 win32 控制台函数直接写入win32 控制台。在非 Windows 平台上,条件编译意味着这里的函数只是调用标准库的函数。

如果您需要打印到标准错误,您应该使用 (eg) ePutStrLn,而不是hPutStrLn stderr; 我们没有定义一个hPutStrLn. (定义一个是读者的练习!)

于 2012-05-28T03:50:06.200 回答