Как правильно связать файлы объектов, написанные в Haskell?
Грубо следуя этому руководству, мне удалось запустить этот игрушечный проект. Он вызывает функцию Haskell из программы C++.
-
Foo.hs
{-# LANGUAGE ForeignFunctionInterface #-}
module Foo where
foreign export ccall foo :: Int -> Int -> IO Int
foo :: Int -> Int -> IO Int
foo n m = return . sum $ f n ++ f m
f :: Int -> [Int]
f 0 = []
f n = n : f (n-1)
-
bar.C++
#include "HsFFI.h"
#include FOO // Haskell module (path defined in build script)
#include <iostream>
int main(int argc, char *argv[]) {
hs_init(&argc, &argv);
std::cout << foo(37, 19) << "\n";
hs_exit();
return 0;
}
-
call-haskell-from-cxx.cabal
name: call-haskell-from-cxx
version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10
executable foo.so
main-is: Foo.hs
build-depends: base >=4.10 && <4.11
ghc-options: -shared -fPIC -dynamic
extra-libraries: HSrts-ghc8.2.1
default-language: Haskell2010
-
скрипт сборки
#!/bin/bash
hs_lib="foo.so"
hs_obj="dist/build/$hs_lib/$hs_lib"
ghc_version="8.2.1" # May need to be tweaked,
ghc_libdir="/usr/local/lib/ghc-$ghc_version" # depending on system setup.
set -x
cabal build
g++ -I "$ghc_libdir/include" -D"FOO=\"${hs_obj}-tmp/Foo_stub.h\"" -c bar.c++ -o test.o
g++ test.o "$hs_obj" \
-L "$ghc_libdir/rts" "-lHSrts-ghc$ghc_version" \
-o test
env LD_LIBRARY_PATH="dist/build/$hs_lib:$ghc_libdir/rts:$LD_LIBRARY_PATH" \
./test
Это работает (Ubuntu 16.04, GCC 5.4.0), печать 893
- но это не очень надежный, а именно, если я удалю фактический вызов функции Haskell, то есть std::cout << foo(37, 19) << "\n";
line, то он не работает на этапе связывания с сообщением об ошибке
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to 'base_GHCziTopHandler_flushStdHandles_closure'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to 'base_GHCziStable_StablePtr_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to 'base_GHCziPtr_FunPtr_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to 'base_GHCziWord_W8zh_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to 'base_GHCziIOziException_cannotCompactPinned_closure'
...
По-видимому, включение проекта Haskell втягивает в него дополнительные файлы библиотеки. Как я могу явно зависеть от всего необходимого, чтобы избежать такой хрупкости?
Вывод скрипта сборки при включении вызова foo
с ldd
в конечном исполняемом файле:
++ cabal build
Preprocessing executable 'foo.so' for call-haskell-from-C-0.1.0.0..
Building executable 'foo.so' for call-haskell-from-C-0.1.0.0..
Linking a.out ...
Linking dist/build/foo.so/foo.so ...
++ g++ -I /usr/local/lib/ghc-8.2.1/include '-DFOO="dist/build/foo.so/foo.so-tmp/Foo_stub.h"' -c bar.c++ -o test.o
++ g++ test.o dist/build/foo.so/foo.so -L /usr/local/lib/ghc-8.2.1/rts -lHSrts-ghc8.2.1 -o test
++ env LD_LIBRARY_PATH=dist/build/foo.so:/usr/local/lib/ghc-8.2.1/rts: sh -c 'ldd ./test; ./test'
linux-vdso.so.1 => (0x00007fff23105000)
foo.so => dist/build/foo.so/foo.so (0x00007fdfc5360000)
libHSrts-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so (0x00007fdfc52f8000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdfc4dbe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdfc49f4000)
libHSbase-4.10.0.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/base-4.10.0.0/libHSbase-4.10.0.0-ghc8.2.1.so (0x00007fdfc4020000)
libHSinteger-gmp-1.0.1.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/integer-gmp-1.0.1.0/libHSinteger-gmp-1.0.1.0-ghc8.2.1.so (0x00007fdfc528b000)
libHSghc-prim-0.5.1.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/ghc-prim-0.5.1.0/libHSghc-prim-0.5.1.0-ghc8.2.1.so (0x00007fdfc3b80000)
libgmp.so.10 => /usr/lib/x86_64-linux-gnu/libgmp.so.10 (0x00007fdfc3900000)
libffi.so.6 => /usr/local/lib/ghc-8.2.1/rts/libffi.so.6 (0x00007fdfc36f3000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdfc33ea000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fdfc31e2000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdfc2fde000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fdfc2dc1000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdfc5140000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdfc2bab000)
Ответы
Ответ 1
Обычно ghc
связывает исполняемые файлы с -Wl,--no-as-needed
, и вы также должны использовать ее. (Вы можете проверить, как ghc
исполняемые файлы ghc
например, с помощью cabal build --ghc-options=-v3
.)
Вы можете найти более подробную информацию здесь. Я понимаю следующее: foo.so
требует, чтобы libHSbase-4.10.0.0-ghc8.2.1.so
загружался во время выполнения по мере необходимости, т. readelf -a dist/build/foo.so/foo.so | grep NEEDED
Когда нам нужен символ из него (проверьте readelf -a dist/build/foo.so/foo.so | grep NEEDED
). Поэтому, если вы не вызываете foo
, то base.so
не загружается. Но ghc нуждается во всех загружаемых библиотеках (я не знаю почему). Опция --no-as-needed
all заставляет загружать все библиотеки.
Обратите внимание, что --no-as-needed
параметры зависят от позиции, поэтому помещаем их перед общей библиотекой.
Ответ 2
В этом ответе объясняется, что происходит во время связи, почему решение с -Wl,--no-as-needed
работает, и что нужно сделать, чтобы иметь несколько более надежный подход.
В двух словах: -lHSrts-ghcXXX.so
зависит от libHSbaseXXX.so
, libHSinteger-gmpXXX.so
и libHSghc-primXXX.so
которые должны быть предоставлены компоновщику во время связи.
Предлагаемое здесь решение зависит от большого количества ручной работы и не очень масштабируемо. Однако я не знаю достаточно о cabal
, чтобы сказать вам, как автоматизировать это, но я надеюсь, что вы можете сделать последний шаг.
Или, может быть, вам будет хорошо с помощью -Wl,--no-as-needed
-solution, потому что вы знаете, что происходит за кулисами.
Давайте начнем с того, что перейдем к процессу компоновки для версии, не вызвав foo
, несколько упрощенным способом (вот отличная статья от Эли Бендерски, даже если речь идет о статической привязке):
-
Компилятор поддерживает таблицу символов и должен найти определения/машинный код для всех них. Давайте упростим и предположим, что вначале он имеет только символ main
в таблице, и определение этого символа неизвестно.
-
Определение символа main
находит его object-file test.o
Однако функция main
использует функции hs_init
и hs_exit
. Таким образом, мы нашли определение main
, но оно не работает, если мы не знаем определения hs_init
и hs_exit
. Итак, теперь мы должны искать их определения.
-
На следующем этапе компоновщик смотрит на foo.so
, но foo.so
не определяет какой-либо символ, который нам интересен (foo
не используется!), И компоновщик просто пропускает foo.so
и никогда не оглядывается назад.
-
Линкер смотрит на -lHSrts-ghcXXX.so
. Там он находит определения hs_init
и hs_exit
. Таким образом, используется все содержимое разделяемой библиотеки, но для этого нужны определения таких символов, как например base_GHCziTopHandler_flushStdHandles_closure
. Это означает, что компоновщик начинает искать определения этих символов.
-
Однако в командной строке больше нет библиотек, поэтому компоновщику нечего смотреть, и связь не выполняется/не выполняется, поскольку определения некоторых символов отсутствуют.
Чем отличается тот случай, когда используется foo
? После 2. шага hs_init
не только hs_init
и hs_exit
, но и foo
, которые находятся в foo.so
Поэтому foo.so
должен быть включен.
Из-за того, как была создана библиотека foo.so
, содержится следующая информация:
>>> readelf -d dist/build/foo.so/foo.so | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libHSrts-ghc7.10.3.so]
0x0000000000000001 (NEEDED) Shared library: [libHSbase-4.8.2.0-HQfYBxpPvuw8OunzQu6JGM-ghc7.10.3.so]
0x0000000000000001 (NEEDED) Shared library: [libHSinteger-gmp-1.0.0.0-2aU3IZNMF9a7mQ0OzsZ0dS-ghc7.10.3.so]
0x0000000000000001 (NEEDED) Shared library: [libHSghc-prim-0.4.0.0-8TmvWUcS1U1IKHT0levwg3-ghc7.10.3.so]
0x0000000000000001 (NEEDED) Shared library: [libgmp.so.10]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
>>> readelf -d dist/build/foo.so/foo.so | grep RPATH
0x000000000000000f (RPATH) Library rpath: [
/usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM:
/usr/lib/ghc/rts:
/usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3:
/usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS]
Из этой информации линкер знает, какие разделяемые библиотеки необходимы (NEEDED
-flag) и где их можно найти в вашей системе (RPATH
). Эти библиотеки найдены/открыты/обработаны (т.е. отмечены как необходимо) и, следовательно, все необходимые определения присутствуют.
Вы можете следить за всем процессом, добавляя
g++ ...
-Wl,--trace-symbol=base_GHCziTopHandler_flushStdHandles_closure \
-Wl,--verbose \
-o test
к этапу привязки.
То же самое происходит, если мы убеждаемся, что foo.so
входит в результирующий исполняемый файл через -Wl,--no-as-needed
как было предложено @Yuras.
Каков результат этого анализа?
Мы должны предоставить необходимые библиотеки в командной строке (после -lHSrts-ghcXXX.so
) и не зависеть от того, что они добавляются за один шанс через другие разделяемые библиотеки. Очевидно, что несколько загадочных имен действительны только для моей установки:
g++ ...
-L/usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM -lHSbase-4.8.2.0-HQfYBxpPvuw8OunzQu6JGM-ghc7.10.3 \
-L/usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS -lHSinteger-gmp-1.0.0.0-2aU3IZNMF9a7mQ0OzsZ0dS-ghc7.10.3 \
-L/usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3 -lHSghc-prim-0.4.0.0-8TmvWUcS1U1IKHT0levwg3-ghc7.10.3 \
...
-o test
Теперь он строит, но не загружается во время выполнения (после того, как правильный rpath
установлен только в foo.so
но foo.so
не используется). Чтобы исправить это, мы могли либо расширить LD_LIBRARY_PATH
либо добавить -rpath
ссылку-командной строки:
g++ ...
-L... -lHSbase-... -Wl,-rpath,/usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM \
-L... -lHSinteger-gmp-... -Wl,-rpath,/usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS \
-L... -lHSghc-prim-... -Wl,-rpath,/usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3 \
...
-o test
Должна быть утилита, чтобы автоматически получать пути и имена библиотек (каббал, похоже, делает это при создании foo.so
), но я не знаю, как это сделать, потому что у меня нет опыта работы с haskell/cabal.