Как правильно связать объектные файлы, написанные на Haskell? - PullRequest
0 голосов
/ 28 мая 2018

Примерно следуя этому уроку , мне удалось заставить этот игрушечный проект работать.Он вызывает функцию 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, GCC5.4.0), печатая 893 - но это не очень надежно, а именно, если я удаляю фактический вызов функции Haskell, то есть строку std::cout << foo(37, 19) << "\n";, то он завершается неудачно при связываниишаг, с сообщением об ошибке

/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)

Ответы [ 2 ]

0 голосов
/ 23 июня 2018

Этот ответ объясняет, что происходит во время связывания, почему работает решение с -Wl,--no-as-needed и что нужно сделать вместо этого, чтобы иметь более надежный подход.

В двух словах: -lHSrts-ghcXXX.so зависит от libHSbaseXXX.so, libHSinteger-gmpXXX.so и libHSghc-primXXX.so, которые должны быть предоставлены компоновщику во время связывания.

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

Или, возможно, вам будет хорошо с использованием -Wl,--no-as-needed -разрешения, потому что вы знаете, что происходит за кулисами.


Давайте начнем с пошагового процесса связывания дляверсия без вызова foo, несколько упрощенно ( здесь - отличная статья от Эли Бендерского, даже если речь идет о статической связи):

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

  2. Определение символа main найдено егообъект-файл 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_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

Должна быть утилита для автоматического получения путей и имен библиотек (кажется, что cabalсделать это при сборке foo.so), но я не знаю, как это сделать, потому что у меня нет опыта работы с haskell / cabal.

0 голосов
/ 20 июня 2018

Обычно ghc связывает исполняемые файлы с параметром -Wl,--no-as-needed, и вы должны также использовать его.(Вы можете проверить, как 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).Так что если вы не позвоните foo, то base.so не загружается.Но ghc нужно, чтобы все библиотеки были загружены (я не знаю почему).Опция --no-as-needed заставляет загружать все библиотеки.

Обратите внимание, что опции --no-as-needed зависят от позиции, поэтому поместите их перед общей библиотекой.

...