← На главную

Совместное использование C++ и Haskell

Помните, я как-то писал про разработку GUI приложений на Haskell с использованием библиотеки wxWidgets? Мне стало интересно, а нельзя ли сгенерировать код GUI на языке C++ в wxGlade или Code::Blocks, а затем связать этот код с кодом на Haskell, реализующим собственно функционал приложения? Оказалось, что можно, и довольно просто.

В чем заключается профит при таком подходе?

  • Библиотека wxWidgets написана на C++, так может разумнее использовать ее на родном языке? Как мы с вами помним, без дополнительной и довольно трудоемкой оптимизации, программа на Haskell, использующая библиотеку wxWidgets, получается намного «жирнее» аналогичной программы на С++. Кроме того, работая с библиотекой напрямую, мы гарантированно получаем доступ абсолютно ко всем ее возможностям.
  • Для wxWidgets написано множество генераторов кода, например, wxFormBuilder, wxGlade и wxSmith. Но ни один из них не умеет генерировать код на Haskell. XRC файлы библиотекой wxHaskell на данный момент также не поддерживаются. Если нам нужен не самый тривиальный GUI, видимо, будет быстрее и проще создать его в wxFormBuilder, чем писать на wxHaskell.
  • Последняя (на момент написания этих строк) версия wxHaskell работает только с wxWidgets 2.9. Однако для Debian, к примеру, не планируется создавать соответствующий deb-пакет. А собирать wxWidgets из исходников что-то не хочется.

Потратив один вечер на чтение документации и кодинг, мне удалось написать свое первое гибридное приложение. В wxFormBuilder был сгенерирован код графического интерфейса для программы на Haskell, решающую задачу о превращении мухи в слона. Затем код на C++ и код на Haskell были связаны воедино так, чтобы пользователь мог вводить в форму пару четырехбуквенных слов, нажимать кнопку и видеть цепочку превращения первого слова во второе.

Так это выглядит в Xubuntu:

GUI к программе, решающей задачу о превращении мухи в слона

А вот – та же программа, скомпилированная под Windows:

Та же программа, собранная под Windows

Давайте разберемся, как это работает. От кода на C++ требуется очень простая вещь. При нажатии на кнопку m_solveButton нужно взять две строки из полей m_startText и m_goalText, после чего передать их в следующую функцию на языке Haskell:

solveAlchemyTask :: String -> String -> [String]

Затем результат выполнения функции требуется вывести в поле m_solutionText. Также нужно проводить некоторые проверки, например, действительно ли оба слова, введенных пользователем, состоят из четырех букв, но это уже мелочи. Проблема заключается в том, что C++, очевидно, ничего не знает ни о типах данных в языке Haskell, ни о самой функции solveAlchemyTask.

Поэтому необходимо написать обертку для функции solveAlchemyTask. Она должна принимать аргументы, тип которых известен C++, например, wchar_t* или std::wstring. Затем эти аргументы должны быть преобразованы в String и переданы функции solveAlchemyTask. Возвращаемое значение должно быть приведено к одному из типов C++, например, wchar_t**. Обертка должна вернуть результат этого приведения.

В переводе на язык Haskell это выглядит так:

{-# LANGUAGE ForeignFunctionInterface #-} module AlchemyTaskWrap where import AlchemyTask import Foreign.C import Foreign.Ptr import Foreign.Marshal.Alloc import Foreign.Marshal.Array alchemyTaskSolve_hs :: CWString -> CWString -> IO (Ptr CWString) alchemyTaskSolve_hs cwstart cwgoal = do start <- peekCWString cwstart goal <- peekCWString cwgoal let solution = solveAlchemyTask start goal ptrList <- mapM newCWString solution rslt <- newArray0 nullPtr ptrList return rslt alchemyTaskFreeRslt_hs :: Ptr CWString -> IO () alchemyTaskFreeRslt_hs rslt = do ptrList <- peekArray0 nullPtr rslt mapM_ free ptrList free rslt return () foreign export ccall alchemyTaskSolve_hs :: CWString -> CWString -> IO (Ptr CWString) foreign export ccall alchemyTaskFreeRslt_hs :: Ptr CWString -> IO ()

Прекрасную документацию по использованным здесь типам и функциям вы найдете на Hackage. Тут все довольно просто. Например, CWString – это аналог wchar_t*. Функция peekCWString считывает CWString в String, а newCWString создает новый CWString из заданного String. Функция newArray0 создает массив, заканчивающийся заданным значением и возвращает указатель на первый его элемент. Помимо обертки над solveAlchemyTask здесь также объявлена функция alchemyTaskFreeRslt_hs, отвечающая за освобождение памяти, выделяемой оберткой.

Если теперь скомпилировать приведенный код:

ghc -c AlchemyTaskWrap.hs

… будут получены два файла – AlchemyTaskWrap.o и AlchemyTaskWrap_stub.h. Содержимое последнего:

#include "HsFFI.h" #ifdef __cplusplus extern "C" { #endif extern HsPtr alchemyTaskSolve_hs(HsPtr a1, HsPtr a2); extern void alchemyTaskFreeRslt_hs(HsPtr a1); #ifdef __cplusplus } #endif

HsPtr – это, если что, просто другое название void*. На этом интерфейс со стороны Haskell готов, теперь нужно правильно подцепить его в коде на C++.

Во-первых, требуется добавить вызовы hs_init() и hs_exit() при запуске и закрытии приложения соответственно:

bool SolverApp::OnInit() { int argc = 1; const char* prog_name = "main"; char** argv = const_cast<char**>(&prog_name); hs_init(&argc, &argv); // does supports unicode? AppFrame *frame = new AppFrame(NULL); frame->Show(TRUE); this->SetTopWindow(frame); return TRUE; } int SolverApp::OnExit() { hs_exit(); return wxApp::OnExit(); }

Я не был уверен, поддерживает ли hs_init() юникод, поэтому решил полностью подменить argv. Кстати, через эту функцию можно задавать различные RTS опции.

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

#include "AlchemyTaskWrap_stub.h" class AppFrame: public SolverFrame { public: AppFrame( wxWindow* parent ) : SolverFrame(parent) { } void onSolveButtonClick( wxCommandEvent& event ) { wxString start = this->m_startText->GetValue(); wxString goal = this->m_goalText->GetValue(); if(start.Len() != 4 || goal.Len() != 4) { wxMessageBox(wxT("Invalid start or goal length!")); return; } wxTextCtrl* solText = this->m_solutionText; solText->Clear(); HsPtr haskellSolution = alchemyTaskSolve_hs( static_cast<HsPtr>(const_cast<wxChar*>( start.c_str() )), static_cast<HsPtr>(const_cast<wxChar*>( goal.c_str() )) ); wxChar** solution = static_cast<wxChar**>(haskellSolution); if(*solution == NULL) { wxMessageBox(wxT("No solution!")); } else { wxString solutionStr = start + wxT("\n"); for(wxChar** curr = solution; *curr != NULL; curr++) { solutionStr += *curr; solutionStr += wxT("\n"); } solText->SetValue(solutionStr); } alchemyTaskFreeRslt_hs(haskellSolution); } };

Код написан, осталось правильно его собрать. Я написал два скрипта для сборки проекта – под винду и под юниксы. Рассмотрим крипт сборки проекта для юниксов:

#!/bin/sh ghc -c Vocabulary.hs AlchemyTask.hs AlchemyTaskWrap.hs g++ -Wall -c `wx-config --cppflags` `./ghc-cppflags.pl` \ gui.cpp main.cpp g++ main.o gui.o AlchemyTaskWrap.o AlchemyTask.o Vocabulary.o \ `wx-config --libs` \ `./ghc-libs.pl astar containers deepseq array PSQueue base \ integer-gmp ghc-prim` \ -lgmp -lffi -o main

Примечание: Я заметил, что GHC 7.0 генерирует файл AlchemyTaskWrap_stub.o, в то время, как GHC 7.4 этого не делает. В зависимости от используемой вами версии GHC может потребоваться соответствующим образом подправить скрипт.

Как видите, тут используется два вспомогательных скрипта – ghc-cppflags.pl и ghc-libs.pl. Первый скрипт по большому счету просто выводит строку:

`ghc --print-libdir`/include

Скрипт ghc-libs.pl интереснее. Он выводит флаги, которые нужно передать g++ для линковки заданных Haskell-библиотек. Например, команда:

./ghc-libs.pl base

… выведет что-то вроде:

-L/usr/lib/ghc -lHSrts -L/usr/lib/ghc/base-4.5.0.0 -lHSbase-4.5.0.0

Да, флаги для линковки HSrts выводятся всегда. В общем случае для определения требуемых библиотек и, что не менее важно, их порядка, можно написать тестовое приложение, вроде такого:

module Main where import AlchemyTask writeList lst = do putStrLn $ concatMap (++ " ") lst main = do writeList $ solveAlchemyTask "муха" "слон"

… и собрать его командой:

ghc -O Main.hs -v

Будет выведено много информации, которую GHC обычно не выводит. Помимо прочего, в секции «Linker» можно будет подсмотреть, какие библиотеки и в каком порядке линкуются.

Все исходники к этой заметке вы можете скачать здесь. Чтобы собрать проект, просто запустите build-unix.sh или build-windows.sh, в зависимости от того, под какой системой вы работаете. Со сборкой под виндой могут возникнуть сложности, поскольку вам придется вручную установить MinGW, MSYS, Perl, wxWidgets и Haskell Platform. Также от вас может потребоваться изменить значение пары переменных окружения в скрипте build-windows.sh. Независимо от используемой ОС, вам понадобится установить библиотеку astar с помощью cabal.

На счет размера приложения. Под виндой у меня получился exe-шник размером 3.2 Мб безо всяких «лишних» библиотек в таблице импорта и прочего мусора. Для сравнения, при использовании wxHaskell получаются программы размером около 26 Мб. Путем сильных извращений их размер удается уменьшить до 3.9 Мб, а путем очень сильных извращений – до 3.2 Мб.

Таким образом, профит действительно имеет место быть по каждому из трех пунктов, названных в начале заметки. Кроме того, теперь мы можем смело использовать любой код, написанный на прекрасном языке программирования Haskell (включая кучу модулей, представленных на Hackage), в своих проектах на C++ и, вообще-то говоря, большинстве других языков.