Подключение 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 можно почерпнуть с этой подборки ссылок.