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