5 августа 2012 в 14:11

Основы подключения C/C++-кода к Haskell проекту из песочницы

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

Этот топик для тех, кто хотел бы опробовать Haskell на деле, но имеет горы полезного C и C++ кода с которым требуется считаться.

Переход на другой язык программирвания всегда сопряжён с желанием сохранить возможность свободного использования предыдущих наработок, библиотек и т.д. Для этих целей в Haskell есть библиотека Foreign.C которая реализует механизм интерфейса с функциями других языков (Foreign Function Interface — FFI).

Рассмотрим подробнее как это происходит.

Подключение C-кода через Foreign.C

Пусть у нас есть некоторый исходный код:

hello.h

void hello(char* name);

hello.c

#include "stdio.h"
#include "hello.h"

void hello(char* name)
{
    printf("Hello %s!\n", name);
}

Мы хотим что бы функция hello была доступна из некоторого модуля Hello следующим образом:

Main.hs

module Main where
import Hello

main =
    Hello.hello "World"

Для этого нужно:
  • создать файл Hello.hs;
  • подключить Haskell FFI через прагму {-# LANGUAGE ForeignFunctionInterface #-};
  • добавить все необходимые модули из Foreign.C, в нашем случае это Foreign.C.String;
  • описать сигнатуру нужных нам функций из файла-заголовка hello.h;
  • обернуть при необходимости FFI-функции что бы избавиться от Foreign.C типов.

После чего модуль Hello.hs должен выглядеть примерно так:

Hello.hs

{-# LANGUAGE ForeignFunctionInterface #-}
module Hello where
import Foreign.C
import Foreign.C.String

foreign import ccall "hello" hello_ffi :: CString -> IO ()

hello :: String -> IO ()
hello name = hello_ffi =<< newCString name

Теперь наш код готов. Обычно при компиляции GHC требуется только *.hs файлы. Если мы попробуем провернуть такую штуку с нашим проектом, то получим ошибку:

$ ghc -o main Main.hs Hello.hs
Linking main ...
Hello.o: In function `s11h_info':
(.text+0x8e): undefined reference to `hello'
collect2: ld returned 1 exit status

Что бы такого не происходило нужно внимательно следить — доступны ли все транслируемые единицы (либо их результирующие объектные файлы) для GHC:

$ ghc -o main Main.hs Hello.hs hello.c
Linking main ...

Окей, теперь можно проверить программу:

$ ./main
Hello World!

Итак, наш модуль сработал корректно — слово World было передано в нашу библиотеку, и выведено на экран через printf.

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

Добавление C-исходников в cabal-проект

Сначала сгенерируем стандартный проект командой:

$ cabal init

Должно получиться что-то подобное:

name: hello-example
version: 0.1.0.0
synopsis: Example of cabal package with ffi
build-type: Simple
cabal-version: >=1.8

executable hello-example
  main-is: Main.hs
  build-depends: base ==4.5.*

Теперь достаточно добавить c-sources: hello.c, т.е. получить:

name: hello-example
version: 0.1.0.0
synopsis: Example of cabal package with ffi
build-type: Simple
cabal-version: >=1.8

executable hello-example
  main-is: Main.hs
  c-sources: hello.c
  build-depends: base ==4.5.*

Можно проверить работоспособность cabal-пакета:

$ cabal configure
$ cabal build
$ cabal install

Теперь наша программа должна быть всегда доступна из командной строки:

$ hello-example
Hello World!

Особенности подключения C++ кода

К сожалению, при обращении с C++ наши возможности ограничены той же библиотекой Foreign.C, поэтому проще всего приводить все интерфейсы к C-совместимому виду.
Для примера заменим hello.c на hello.cpp реализованный через iostream:

hello.cpp

#include <iostream>
#include "hello.h"

void hello(char* name)
{
    std::cout << "Hello " << name << "!" << std::endl;
}

Что бы получать корретные объектные файлы следует обрамлять экспортируемые функции extern-конструкцией:

hello.h

#ifdef __cplusplus
extern "C"
{
#endif
void hello(char* name);
#ifdef __cplusplus
}
#endif

Теперь если мы попробуем скомпилировать проект, то получим много-много однотипных ошибок:

$ ghc -o main Main.hs Hello.hs hello.cpp
cc1plus: warning: command line option ‘-Wimplicit’ is valid for C/ObjC but not for C++ [enabled by default]
Linking main ...
hello.o: In function `hello':
hello.cpp:(.text+0xf): undefined reference to `std::cout'
.........................................................
collect2: ld returned 1 exit status

Ошибки компоновки происходят из-за того что требуется явно указывать линковку со стандартной библиотекой, для gcc на linux обычно это библиотека -lstdc++.

$ ghc -o main Main.hs Hello.hs hello.cpp -lstdc++
cc1plus: warning: command line option ‘-Wimplicit’ is valid for C/ObjC but not for C++ [enabled by default]
Linking main ...

После этого можно подготовить cabal-проект. Кроме указания c-source для C++ требуется указывать ещё и extra-libraries:

name: hello-example
version: 0.1.0.0
synopsis: Example of cabal package with ffi
build-type: Simple
cabal-version: >=1.8

executable hello-example
  main-is: Main.hs
  c-sources: hello.cpp
  extra-libraries: stdc++
  build-depends: base ==4.5.*

Иногда даже при явном указании stdc++ проблемы с линковкой могут всё равно оставаться. В этом случае следует указывать ещё и --make опцию, несмотря на то что это избыточно (о чём вам и сообщит сборщик):

name: hello-example
version: 0.1.0.0
synopsis: Example of cabal package with ffi
build-type: Simple
cabal-version: >=1.8

executable hello-example
  main-is: Main.hs
  c-sources: hello.cpp
  extra-libraries: stdc++
  ghc-options: --make
  build-depends: base ==4.5.*


Остальную информацию об особенностях и тонкостях работы с Haskell FFI можно почерпнуть с этой подборки ссылок.
+22
52

комментарии (8)

+1
Monnoroch, #
А не существует возможности класс C++ смаппить на тип хаскелла?
+2
burjui, #
Обычно связке <ЯП> + C++ мешает проблемы name mangling и ABI, которые для C++ не стандартизированы, так что разработчики компиляторов вольны городить свои варианты, что они и делают. У языка C в этом плане всё хорошо: имена переменных и функций при компиляции остаются неизменными, попадая в объектные файлы в первозданном виде, соглашения вызова функций прописаны в стандарте, и никаких классов, шаблонов, виртуальных таблиц в нём нет.
0
Monnoroch, #
Это понятно и легко обходимо: можно сэмулировать класс обычной структурой и функциями на C. Дело скорее не в этом, а в том, что хотелось бы реализовать вызов сишного деструктора, при сборке (или при пометке на сборку, не важно) уничтоженного обьекта в haskell.
+1
burjui, #
Так это уже совсем другой вопрос — о возможности назначить финализатор для объекта Haskell, к классам C++ не имеющий отношения. Кажется, вам нужен ForeignPtr.
0
Monnoroch, #
Да, похоже на то, спасибо.
0
pinocchio964, #
Кроме вышеперечисленного, не очень хорошо классы вписываются в Haskell. В ООП языках специализация поведения объекта, как правило, реализуется за счёт динамического перекрытия виртуальных функций, что совершенно не вписывается в архитектуру ФП приложений. Так что намного проще сводить всё к какому-то простенькому интерфейсу.
0
Monnoroch, #
Да знаю я, просто у меня есть сишная либа, которая предоставляет интерфейс для работы с неким ссылочным типом данных A, при этом осуществляется сборка мусора: надо для каждой полученной из либы ссылки делать release(), я сделал отличную C++ обертку, которая действует в частности и как умный указатель, то есть берет на себя заботу об уничтожении ссылок. Однако тип данных очень круто ложится на haskell, и очень хотелось бы уметь в хаскеле работать с ним, но при этом надо, чтобы осуществлялось освобождение ссылок и прочие условности. Вот как-то так.
+5
afiskon, #
Кстати, использовать Haskell-библиотеки в проектах на C++ не намного сложнее: eax.me/cpp-and-haskell/

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

Сложность ERP систем
Wiren Board — встраиваемый компьютер с Wi-Fi, GPRS, GPS, NFC и Ethernet из коробки
Общие заблуждения о сенсорных экранах