Как правильно связать файлы объектов, написанные в 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, несколько упрощенным способом (вот отличная статья от Эли Бендерски, даже если речь идет о статической привязке):

  1. Компилятор поддерживает таблицу символов и должен найти определения/машинный код для всех них. Давайте упростим и предположим, что вначале он имеет только символ main в таблице, и определение этого символа неизвестно.

  2. Определение символа main находит его object-file test.o Однако функция main использует функции hs_init и hs_exit. Таким образом, мы нашли определение main, но оно не работает, если мы не знаем определения hs_init и hs_exit. Итак, теперь мы должны искать их определения.

  3. На следующем этапе компоновщик смотрит на foo.so, но foo.so не определяет какой-либо символ, который нам интересен (foo не используется!), И компоновщик просто пропускает foo.so и никогда не оглядывается назад.

  4. Линкер смотрит на -lHSrts-ghcXXX.so. Там он находит определения hs_init и hs_exit. Таким образом, используется все содержимое разделяемой библиотеки, но для этого нужны определения таких символов, как например base_GHCziTopHandler_flushStdHandles_closure. Это означает, что компоновщик начинает искать определения этих символов.

  5. Однако в командной строке больше нет библиотек, поэтому компоновщику нечего смотреть, и связь не выполняется/не выполняется, поскольку определения некоторых символов отсутствуют.

Чем отличается тот случай, когда используется 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.