代码之家  ›  专栏  ›  技术社区  ›  leftaroundabout

如何正确链接用haskell编写的对象文件?

  •  19
  • leftaroundabout  · 技术社区  · 7 年前

    大致遵循 this tutorial 我设法使这个玩具项目运作起来。它从一个C++程序调用一个Haskell函数。

    • 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"; 行,然后在链接步骤失败,并显示错误消息

    /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 回复  |  直到 7 年前
        1
  •  2
  •   Yuras    7 年前

    通常 ghc 链接可执行文件 -Wl,--no-as-needed 选项,您也应该使用它。(你可以检查一下 GHC 链接可执行文件,例如使用 cabal build --ghc-options=-v3 )

    你可以找到更多的细节 here . 我的理解如下: 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 选项强制加载所有库。

    注意 --不,根据需要 选项取决于位置,因此请将其放在共享库之前。

        2
  •  2
  •   ead    7 年前

    -Wl,--no-as-needed 工作,以及应该做什么,而不是有一个更强大的方法。

    简而言之: -lHSrts-ghcXXX.so 取决于 libHSbaseXXX.so , libHSinteger-gmpXXX.so libHSghc-primXXX.so 必须在链接过程中提供给链接器。

    这里提出的解决方案依赖于大量的手工工作,并且不具有很强的可扩展性。但是我不太了解 cabal 告诉你如何使这自动化,但我希望你能迈出最后一步。

    或者你可以用 -WL,——无需 -解决方案,因为你知道幕后发生了什么。


    让我们从单步执行版本的链接过程开始,而不调用 foo 以某种简化的方式( here 是来自EliBendersky的一篇很好的文章,即使是关于静态链接的):

    1. 链接器维护一个符号表,必须找到所有符号的定义/机器代码。让我们简化并假设,开始时它只有一个符号 main 在表中,此符号的定义未知。

    2. 符号的定义 主要的 找到它的对象文件 test.o . 但是,函数 主要的 使用函数 hs_init hs_exit . 因此我们发现了 主要的 但是如果我们不知道 高速出口 . 所以现在我们必须寻找它们的定义。

    3. 在下一步中,链接器将查看 foo.so 但是 所以 没有定义任何我们感兴趣的符号( 未使用!)链接器只是跳过 所以 永远不会回头。

    4. 链接器查看 -lhsrts-ghcxx.so. . 在那里它找到了 初始化 高速出口 . 因此,可以使用共享库的全部内容,但它需要定义这些符号,例如 base_GHCziTopHandler_flushStdHandles_closure . 这意味着链接器开始寻找这些符号的定义。

    5. 但是,命令行中没有更多的库,因此链接器没有任何可查看的内容,并且链接失败/不成功,因为缺少某些符号的定义。

    哪种情况不同? 是否使用?2号之后。不仅如此 初始化 高速出口 被通缉,但也 ,在 所以 . 所以 所以 必须包括在内。

    因为图书馆的方式 所以 已生成,包含以下信息:

    >>> 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 -标记)以及它们在您的系统中的位置( RPATH

    您可以通过添加

    g++ ...
        -Wl,--trace-symbol=base_GHCziTopHandler_flushStdHandles_closure \
        -Wl,--verbose \
        -o test
    

    到悬挂机构步骤。

    如果我们强制执行 所以 通过 -WL,——无需 如@yuras所建议。

    这种分析的结果是什么?

    我们应该在命令行上提供所需的库(在 -lhsrts-ghcxx.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 只设置在 所以 但是 所以 未使用)。为了修复它,我们可以扩展 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在构建时似乎会这样做) 所以 但我不知道该怎么做,因为我对哈斯克尔/卡巴没有经验。