Подтвердить что ты не робот

Консоль ввода/вывода Unicode в Haskell на Windows

Кажется довольно сложно получить консольный ввод/вывод для работы с символами Unicode в Haskell под окнами. Вот сказка о горе:

  • (Предварительно). Прежде чем рассматривать возможность ввода Unicode ввода/вывода в консоли под окнами, вы должны убедиться, что используете консольный шрифт, который может отображать нужные вам символы. Растровые шрифты (по умолчанию) имеют бесконечно низкое покрытие (и не позволяют копировать вставку символов, которые они не могут представлять), а варианты передачи TrueType MS (consolas, консоль lucida) не имеют большого охвата (хотя это позволит копирование/вставка символов, которые они не могут представлять). Возможно, вы захотите установить DejaVu Sans Mono (следуйте инструкциям внизу здесь, вам, возможно, придется перезагрузиться, прежде чем это сработает). Пока это не будет отсортировано, никакие приложения не смогут делать много Unicode I/O; а не только Haskell.
  • Сделав это, вы заметите, что некоторые приложения смогут выполнять консольные операции ввода-вывода под окнами. Но заставить его работать остается довольно сложным. Существует два способа записи на консоль под окнами. (Что следует за любым языком, а не только с Haskell, не волнуйтесь, Haskell немного войдет в изображение!)...
  • Вариант A - использовать обычные функции ввода-вывода на основе байтов в библиотеке c-library; надежда состоит в том, что ОС будет интерпретировать эти байты в соответствии с некоторой кодировкой, которая может кодировать все странные и замечательные персонажи, которые вы хотите. Например, используя эквивалентную технику в Mac OS X, где стандартная системная кодировка обычно UTF8, это отлично работает; вы отправляете выход utf8, вы видите симпатичные символы.
  • В окнах это работает не так хорошо. Кодировка по умолчанию, которую ожидают окна, обычно не будет кодировкой, охватывающей все символы Юникода. Поэтому, если вы хотите увидеть симпатичные символы таким образом, так или иначе, вам нужно изменить кодировку. Одна из возможностей программы - использовать команду SetConsoleCP win32. (Итак, вам нужно привязать к библиотеке Win32.) Или, если вы этого не сделаете, вы можете ожидать, что пользователь вашей программы изменит вам кодовую страницу (тогда они должны были бы вызвать команду chcp до они запускают вашу программу).
  • Вариант B - использовать команды API консоли Win32, поддерживающие Unicode, такие как WriteConsoleW. Здесь вы отправляете UTF16 прямо в окна, что делает его счастливым: нет опасности несоответствия кодировки, потому что окна всегда ожидают UTF16 с этими функциями.

К сожалению, ни один из этих вариантов не очень хорошо работает с Haskell. Во-первых, нет библиотек, которые, как я знаю, используют вариант B, так что это не очень просто. Это оставляет опцию A. Если вы используете библиотеку ввода/вывода Haskell (putStrLn и т.д.), Это то, что сделает библиотека. В современных версиях Haskell он будет внимательно просить окна, какова текущая кодовая страница, и выводит ваши строки в правильной кодировке. С этим подходом существуют две проблемы:

  • Один из них не демонстратор, но раздражает. Как уже упоминалось выше, кодировка по умолчанию почти никогда не кодирует нужные символы: пользователю необходимо изменить кодировку. Таким образом, ваш пользователь должен chcp cp65001 перед запуском вашей программы (вам может показаться отвратительным заставить ваших пользователей сделать это). Или вам нужно привязать к SetConsoleCP и выполнить эквивалент внутри вашей программы (а затем использовать hSetEncoding, чтобы библиотеки Haskell отправляли выходные данные с использованием новой кодировки), что означает, что вам нужно обернуть соответствующую часть библиотек win32 чтобы сделать их видимыми Haskell.
  • Гораздо серьезнее, есть ошибка в окнах (разрешение: не будет исправлено), что приводит к ошибка в Haskell, что означает, что если вы выбрали любую кодовую страницу, такую ​​как cp65001, которая может охватывать все Unicode, процедуры ввода/вывода Haskell будут работать неправильно и сбой. Таким образом, даже если вы (или ваш пользователь) правильно установите кодировку на некоторую кодировку, которая охватывает все замечательные символы Юникода, а затем "сделайте все в порядке", говоря Haskell, чтобы выводить данные с использованием этой кодировки, вы все равно теряете.

Ошибка, указанная выше, все еще не решена и указана как низкий приоритет; основной вывод заключается в том, что Вариант A (в моей классификации выше) неработоспособный, и нужно переключиться на вариант B, чтобы получить надежные результаты. Неясно, какими будут сроки для этого решения, поскольку это выглядит как значительная работа.

Вопрос: тем временем, может ли кто-нибудь предложить обходной путь, позволяющий использовать консоли ввода-вывода Unicode в Haskell под окнами.

См. также запись базы данных об ошибках python, схватив ту же проблему в Python 3 (исправление предлагается, но еще не принято в codebase) и qaru.site/info/11792/..., дающий обходной путь для этой проблемы в Python (на основе "опции B" в моей классификации).

4b9b3361

Ответ 1

Я думал, что я отвечу на свой вопрос и перечислим в качестве одного из возможных ответов следующее: это то, что я сейчас делаю. Вполне возможно, что можно сделать лучше, вот почему я задаю вопрос! Но я подумал, что было бы разумно сделать следующее доступным людям. Это в основном перевод с 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 [email protected](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

то вы используете в нем функции ввода-вывода, а не стандартные библиотеки. Они будут определять, перенаправляется ли выход; если нет (т.е. если мы пишем на "настоящую" консоль), мы обойдем обычные функции ввода-вывода Haskell и напишем непосредственно на консоль win32 с помощью WriteConsoleW, функции консоли win32, поддерживающей unicode. На платформах, отличных от Windows, условная компиляция означает, что функции здесь просто называют стандартно-библиотечными.

Если вам нужно печатать на stderr, вы должны использовать (например) ePutStrLn, а не hPutStrLn stderr; мы не определяем a hPutStrLn. (Определение одного - упражнение для читателя!)