Совместное использование 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:

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++ и, вообще-то говоря, большинстве других языков.

Метки: , , , .


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