Совместное использование C++ и Haskell
23 мая 2012
Помните, я как-то писал про разработку 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:
А вот — та же программа, скомпилированная под Windows:
Давайте разберемся, как это работает. От кода на C++ требуется очень простая вещь. При нажатии на кнопку m_solveButton нужно взять две строки из полей m_startText и m_goalText, после чего передать их в следующую функцию на языке Haskell:
Затем результат выполнения функции требуется вывести в поле m_solutionText. Также нужно проводить некоторые проверки, например, действительно ли оба слова, введенных пользователем, состоят из четырех букв, но это уже мелочи. Проблема заключается в том, что C++, очевидно, ничего не знает ни о типах данных в языке Haskell, ни о самой функции solveAlchemyTask.
Поэтому необходимо написать обертку для функции solveAlchemyTask. Она должна принимать аргументы, тип которых известен C++, например, wchar_t* или std::wstring. Затем эти аргументы должны быть преобразованы в String и переданы функции solveAlchemyTask. Возвращаемое значение должно быть приведено к одному из типов C++, например, wchar_t**. Обертка должна вернуть результат этого приведения.
В переводе на язык Haskell это выглядит так:
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, отвечающая за освобождение памяти, выделяемой оберткой.
Если теперь скомпилировать приведенный код:
… будут получены два файла — AlchemyTaskWrap.o и AlchemyTaskWrap_stub.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() при запуске и закрытии приложения соответственно:
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 и вывести пользователю результат. При этом важно не накосячить с преобразованием типов и не забыть освободить память, выделенную оберткой:
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);
}
};
Код написан, осталось правильно его собрать. Я написал два скрипта для сборки проекта — под винду и под юниксы. Рассмотрим крипт сборки проекта для юниксов:
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-libs.pl интереснее. Он выводит флаги, которые нужно передать g++ для линковки заданных Haskell-библиотек. Например, команда:
… выведет что-то вроде:
Да, флаги для линковки HSrts выводятся всегда. В общем случае для определения требуемых библиотек и, что не менее важно, их порядка, можно написать тестовое приложение, вроде такого:
import AlchemyTask
writeList lst = do
putStrLn $ concatMap (++ " ") lst
main = do
writeList $ solveAlchemyTask "муха" "слон"
… и собрать его командой:
Будет выведено много информации, которую 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++ и, вообще-то говоря, большинстве других языков.
Метки: C/C++, Haskell, Кроссплатформенность, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.