Денис Крешихин

Денис
Крешихин

iOS-разработчик с 15+ летним опытом

Тимлид/сеньор по обстоятельствам

Интересы: swift, uikit, rxswift, oop/ood, devops, agile

2024 © Денис Крешихин

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