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

в 10:11, , рубрики: c++, ffi, 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 можно почерпнуть с этой подборки ссылок.

Автор: pinocchio964

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js