Кроссплатформенное GUI приложение на Haskell

10 августа 2011

В этой заметке речь пойдет о создании кроссплатформенных GUI приложений на языке программирования Haskell с использованием библиотеки wxWidgets. Особое внимание будет уделено уменьшению размера программы. Программа, размер которой изначально составлял 26 Мб, будет ужата до 3.9 Мб без потери функциональности. И это без использования UPX.

1. Установка wxHaskell

Несмотря на то, что библиотека wxWidgets написана на C++, мы можем легко использовать ее даже из чистого функционального языка программирования Haskell. Эта возможность реализована в библиотеке wxHaskell, установить которую можно с помощью cabal:

cabal update
cabal install wx

Предварительно следует установить саму библиотеку wxWidgets. Лично у меня она уже была установлена и cabal поставил wxHaskell безо всяких вопросов. В случае чего, установку wxWidgets я описывал ранее в заметке Примеры использования wxWidgets.

Мы вернемся к вопросу установки wxHaskell, когда речь зайдет об оптимизации размера программы.

2. Пишем интерфейс для mp3-плеера

Как выяснилось, интерфейс библиотеки wxHaskell не имеет совершенно ничего общего с интерфейсом wxWidgets. Все элементы управления переименованы. Лайоут строится с помощью функций row, column, margin и тп, а не на основе wxBoxSizer и ему подобных. Сайзеров в wxHaskell вообще нет. Также в wxHaskell нет умных указателей, костылей для поддержи юникода и тд. Впрочем, это понятно — последние нужны в Haskell так же, как козе пятая нога. Чего не скажешь о C++.

Несмотря на различия интерфейсов библиотек wxWidgets и wxHaskell, последняя показалась мне даже лучше. С ее помощью код получается, как любят говорить некоторые программисты, «очень декларативным». И пусть вас не смущает отсутствие кодогенераторов для wxHaskell типа wxFormBuilder или wxGlade. Они тут совершенно не нужны.

Ниже приведены наброски окна mp3-плеера. Аналогичное окно создавалось с помощью wxSmith в уже упомянутой заметке Примеры использования wxWidgets. Обратите внимание, насколько проще воспринимается приведенный код по сравнению с аналогичным кодом на C++.

import Graphics.UI.WX

{-

wxHaskell demo (eaxPlayer) v 0.1
(c) Alexandr A Alexeev 2011 | http://eax.me/

-}


main :: IO ()
main = start gui

gui :: IO ()
gui = do
  -- форма, на которой будут лежать все наши контролы
  wnd <- frame [ text := "wxHaskell demo (c) afiskon 2011 http://eax.me/" ]
 
  -- нередактируемое поле ввода с названием текущей песни
  txtTitle <- entry wnd [text := "Welcome to eaxPlayer!" , enabled := False ]
  -- ползунок громкости, выставленный на значении 50/100
  volumeSlider <- hslider wnd False {- hide labels -} 1 100 [ selection:= 50 ]

  -- несколько кнопок фиксированного размера
  beginButton <- button wnd [ text := "|<", clientSize := sz 25 25 ]
  playButton <- button wnd [ text := ">", clientSize := sz 25 25 ]
  endButton <- button wnd [ text := ">|", clientSize := sz 25 25 ]
 
  -- ползунок, отобращающий текущую позицию в играющем треке
  positionSlider <- hslider wnd False 1 100 [ ]
 
  -- чекбоксы loop, shuffle и mute
  loopCheckbox <- checkBox wnd [ text := "L" ]
  shuffleCheckbox <- checkBox wnd [ text := "S" ]
  muteCheckbox <- checkBox wnd [ text := "M" ]

  -- список песен
  playlist <- singleListBox wnd []

  -- кнопки для управления плейлистом
  addButton <- button wnd [ text := "Add" ]
  delButton <- button wnd [ text := "Delete" ]
  saveButton <- button wnd [ text := "Save" ]
  openButton <- button wnd [ text := "Open" ]
  openUrlButton <- button wnd [ text := "Open URL" ]

  -- прикрепляем контролы к окну
  set wnd [
    layout :=
      column 0 [
        {- верхний ряд -}
        margin 5 $ row 5 [
          -- название песни
          hfill $ minsize (sz 150 0) $ widget txtTitle,
          -- регулятор громкости
          widget volumeSlider
        ],
       
        {- средний ряд -}
        margin 5 $ row 5 [
          -- слева: кнопки
          row 5 [
            widget beginButton,        -- предыдущая песня
            widget playButton,         -- играть / пауза
            widget endButton           -- следующая песня
          ],
          -- по центру: позиция в текущей песне
          hfill $ widget positionSlider,
          -- справа: чекбоксы
          row 5 [
            widget loopCheckbox,       -- крутить одну песню
            widget shuffleCheckbox,    -- перемешать песни
            widget muteCheckbox        -- выключить звук
          ]
        ],
       
        {- плейлист -}
        margin 5 $ fill $ widget playlist,
       
        {- нижний ряд -}
        margin 5 $ row 5 [
          hfill $ widget addButton,    -- добавить трек
          hfill $ widget delButton,    -- удалить трек
          hfill $ widget saveButton,   -- сохранить плейлист
          hfill $ widget openButton,   -- открыть плейлист
          hfill $ widget openUrlButton -- открыть url
        ]
      ]
    ]
  return ()

Вот как будет выглядеть это хозяйство после компиляции:

Пример использования wxHaskell

Результат практически не отличается от окна, созданного с помощью wxSmith. Разве что галочки Loop, Shuffle и Mute находятся несколько выше, но это несложно исправить. Повторюсь, на мой взгляд, пользы от графического редактора интерфейса здесь будет не больше, чем от Kompozer при верстке веб-страниц.

До сих пор все было здорово и замечательно. Теперь поговорим о косяках.

Во-первых, в Винде помимо графического интерфейса наше приложение также будет иметь консольное окно. Найти флаг для GHC, который исправлял бы это, мне не удалось. Поэтому в release-версии программы нужно руками изменить тип приложения с помощью PE Tools или аналогичной программы. Делается это очень просто — открываем exe-шник, идем в Optional Header → Subsystem и меняем значение Subsystem на «Windows GUI». Если править программу вручную неохота, можно написать небольшую заплатку.

Дополнение: Плохо искал. Нужно использовать флаг -optl-mwindows.

Во-вторых, если в том же PE Tools посмотреть на таблицу импорта программы, то в ней мы увидим «лишние» библиотеки libstdc++-6.dll и wxmsw28_gcc.dll. Первая нашлась у меня в C:\MinGW4\bin, вторая — в C:\SourceCode\Libraries\wxWidgets2.8\lib\gcc_dll. Очевидно, что у рядового пользователя таких библиотек нет, а потому мы должны распространять их вместе с нашей программой. Лично меня смущает, что программа вместе с указанными библиотеками весит 26 Мб. При условии, что аналогичная программа на C++ будет весит около 1.5-2 Мб.

И в-третьих, каждый раз после изменения интерфейса программу нужно пересобирать, чтобы посмотреть на результат. Интерпретатор GHCi тут не поможет. На момент написания этой заметки, по не совсем понятными причинам он не мог справиться с программами, использующими wxHaskell.

3. «Тюнинг» wxHaskell

Если для вас большой размер программы не является проблемой, можете смело переходить к следующему разделу. В этом вы вряд ли найдете для себя что-то интересное. Хочу также отметить, что под UNIX заниматься описанной ниже оптимизацией не имеет большого смысла.

Во-первых, потому что за тем, чтобы у пользователя были нужные библиотеки, позаботится менеджер пакетов. Во-вторых, потому что в половине случаев программа будет собираться из исходных кодов. Таким образом, в плане трафика мы ничего не выиграем, и пересобрать wxHaskell с ключом —enable-split-objs (см далее) все равно не сможем. И хотя для дистрибутивов, использующих бинарные пакеты, пара советов все-таки могут оказаться полезными, далее речь пойдет об уменьшении размера программы только в контексте ОС семейства Windows.

Итак, каким же образом можно уменьшить размер нашей программы? Первое, что пришло мне в голову — линковать wxWidgets не динамически, а статически. Очевидно, что мы используем далеко не все функции из wxmsw28_gcc.dll. Следовательно, при статической линковке мы убьем сразу двух зайцев — избавимся от динамической библиотеки и уменьшим размер программы. Сказано — сделано:

set WXWIN=C:\SourceCode\Libraries\wxWidgets2.8
set WXCFG=gcc_lib\mswu
cd %APPDATA%\cabal\packages\hackage.haskell.org\wxcore\0.12.1.7
cabal --enable-split-objs install wxcore-0.12.1.7.tar.gz
cd ..\..\wx\0.12.1.6
cabal --enable-split-objs install wx-0.12.1.6.tar.gz

Обратите особое внимание на установку переменной окружения %WXCFG%. Во время установки wxHaskell будет использовать .lib файлы, находящиеся в каталоге %WXWIN%\%WXCFG%. Если это будут библиотеки для динамической линковки, будет использована динамическая линковка, если для статической — статическая.

Флаг —enable-split-objs говорит GHC «enable split library into smaller objects to reduce binary sizes». Как именно происходит это разбиение — не знаю, но оно действительно работает. Забегая вперед, скажу, что используя библиотеку, собранную без этого флага, наша программа будет иметь размер 21 Мб (выиграли около 5 Мб). Если же использовать флаг —enable-split-objs, программа уменьшится до 6.3 Мб. В связи с этим я решил вообще не останавливаться на сборке wxHaskell без —enable-split-objs.

Проблема с флагом —enable-split-objs заключается в том, что он сильно замедляет компиляцию. На моем двухъядерном AMD Turion II M500 компиляция wxcore заняла около полутора часов. В связи с этим рекомендую лишний раз проверить, что вы правильно выставили переменные окружения и не забыли прочие вещи в таком духе. В моей версии Haskell Platform оказалась не самая свежая версия MinGW. Где-то на середине процесса компилятор падал по причине internal error. Если у вас установлен MinGW, советую заранее заглянуть в каталог Haskell Platform и подменить имеющуюся там версию MinGW на более свежую. Разумеется, сохранив резервную копию оригинала.

Если компиляция завершилась успешно, пробуем собрать программу. У меня с первого раза это сделать не удалось. Компилятор ругался на множество необъявленных функций. Как выяснилось, нужно подправить файл %APPDATA%\Roaming\ghc\i386-mingw32-*\package.conf.d\wxcore-*.conf. Находим в нем строчку, начинающуюся с «extra-libraries». Библиотеки, имена которых начинаются с «wx» нужно перенести в начало списка. Как я понял, это одна из особенностей компилятора — сначала должны идти статические библиотеки, иначе он начинает путаться. Также мне пришлось добавить в список библиотеки wxpng, wxjpeg, wxtiff и wxzlib. В результате получилось следующее:

extra-libraries: wxexpat wxregexu wxmsw28u wxregexu wxpng
                 wxjpeg wxtiff wxzlib winmm winspool odbc32
                 wsock32 advapi32 rpcrt4 uuid oleaut32 ole32
                 comctl32 shell32 comdlg32 gdi32 user32 kernel32

Переходим в каталог с исходником программы и говорим:

ghc-pkg recache --user
ghc -optl-mwindows eaxPlayer.hs -o eaxPlayer.exe

Первая команда нужна для того, чтобы в действие вступили изменения wxcore-*.conf. Размер программы после компиляции составил 6.26 Мб. Еще пару мегабайт можно убрать с помощью утилиты strip (является частью MSYS):

strip --strip-all eaxPlayer.exe

На выходе получаем 4 Мб. Теперь, если открыть программу в PE Tools, можно заметить наличие в ней секции «.reloc».

Небольшое отступление для тех, кто не знает, для чего нужны релоки. Исполняемые файлы компилируются из расчета на то, что они будут загружены по определенному виртуальному адресу — image base. Если это сделать не удается, программа изменяется в соответствии с релоками (relocation table). Под Windows в виртуальную память сначала загружается exe-шник, и только потом динамические библиотеки. Всегда. Другими словами, в тот момент, когда мы пытаемся загрузить программу в виртуальную память, в ней еще ничего нет. Следовательно, программу всегда можно разместить по адресу image base и релоки использовать не приходится.

Короче, под виндой секция «.reloc» нужна только динамическим библиотекам, так что можно смело от нее избавиться (к вопросу о том, зачем нужно знать всякие низкоуровневые вещи):

strip -R .reloc eaxPlayer.exe

Теперь программа весит 3.8 Мб. Также в PE Tools можно заметить, что программа по прежнему импортирует функции из libstdc++-6.dll. Давайте уж избавимся от всех несистемных библиотек! Возвращаемся к файлу wxcore-*.conf и заменяем в нем строчку

ld-options: -lstdc++

… на:

ld-options: -static -lstdc++

Для того, чтобы не вводить лишние команды, или случайно не забыть про «ghc-pkg recache», создаем небольшой bat-скрипт:

del eaxPlayer.exe
ghc-pkg recache --user
ghc -optl-mwindows eaxPlayer.hs -o eaxPlayer.exe
sleep 1
strip --strip-all -R .reloc eaxPlayer.exe
pause

Программа немного увеличилась — до 3.88 Мб, но зато теперь она использует только системные библиотеки и msvcrt.dll. Но последняя, насколько я понимаю, есть у всех пользователей Windows.

В принципе, с этим уже можно жить. Меня до сих пор смущает наличие odbc32.dll в таблице импорта, а также (!) секция «.edata», которая вообще по идее должна быть только в динамических библиотеках. Путем перекомпиляции wxWidgets с флагами оптимизации и выпиливания ненужного функционала из этой библиотеки, мне удавалось уменьшить размер exe-шника до 3.16 Мб и избавиться от odbc32.dll. Думаю, это далеко не предел. Однако такая «экстремальная» оптимизация заслуживает отдельной заметки.

4. Бонус: статическая линковка PCRE

В качестве небольшого бонуса к этой заметке я решил написать про статическую линковку библиотеки PCRE, к которой я так неравнодушен. В GnuWin32 я не обнаружил .lib файлов для статической линковки PCRE, а в том, как их получить из исходников с помощью MinGW, разбираться не хотелось (подходящий Makefile отсутствовал).

На помощь пришел Google, который отыскал мне уже собранные статические библиотеки PCRE различных версий. Правда, собранные с помощью Visual Studio. С версией PCRE 7.9 мне не повезло — при компиляции программы на Haskell были получены ошибки:

Warning: .drectve `/DEFAULTLIB:"LIBCMT" /DEFAULTLIB:"OLDNAMES" ' unrecognized
C:\Users\afiskon\Desktop\pcre-7.9-static\pcre-7.9-static/pcre.lib(pcre_exec.obj):(.text+0xa):

undefined reference to `__security_cookie'

Зато с версией 6.7 все отлично собралось и заработало! Вот как я устанавливал библиотеку regex-pcre:

cabal install regex-pcre --reinstall --enable-split-objs \
  --extra-lib-dirs="D:\coding-stuff\pcre-6.7-static" \
  --extra-include-dirs="D:\coding-stuff\pcre-6.7-static"

Копию pcre-6.7-static.zip я на всякий случай зазеркалил здесь.

5. Полезные материалы

Небольшая подборка ссылок по теме:

Из примеров советую обратить особое внимание на samples/wx/Controls.hs и samples/wx/Layout.hs. Из первого вы узнаете название большинства элементов управления и их свойств, из второго — как эти элементы разместить нужным образом.

6. Благодарности

Большую помощь при написании этой заметки оказало ЖЖ-сообщество ru-lambda. Сергей Зефиров рассказал про утилиту strip, а Евгений Кирпичев поведал о своем опыте с уменьшением размера Gtk2Hs. Спасибо вам!

Дмитрий Никитинский целую неделю консультировал меня по электронной почте. Многое про «тюнинг» wxHaskell я узнал от него. Дмитрий, спасибо Вам за помощь!

Мне одному кажется поразительным, насколько грамотное и дружелюбное сообщество может возникнуть вокруг языка программирования, если «снабдить» его не слишком низким порогом вхождения?

7. Выводы

Во-первых, на Haskell можно писать GUI приложения. Возможно, с этой задачей он справляется даже лучше C++. Во-вторых, низкоуровневые вещи полезно знать даже при работе с высокоуровневыми языками программирования. В-третьих, «из коробки» модули Haskell могут идти не совсем в том виде, в котором вам хотелось бы. Но при желании их можно допилить вручную. В-четвертых, программы на Haskell обычно тяжелее программ на C++. В-пятых, когда вас спрашивают о недостатках Windows, не забудьте назвать dll hell и отсутствие менеджера пакетов.

В продолжение темы: Использование кода на Haskell из GUI приложения, созданного в wxFormBuilder.

Метки: , , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.