如何正确链接用Haskell编写的目标文件?

时间:2018-05-28 15:36:16

标签: c++ haskell linker ghc ffi

大致跟随this tutorial,我设法让这个玩具项目起作用。它从C ++程序调用Haskell函数。

  • {-# 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
    
  • 构建脚本

    893

这有效(Ubuntu 16.04,GCC 5.4.0),打印std::cout << foo(37, 19) << "\n"; - 但它不是很健壮,即,如果我删除 Haskell函数的实际调用,即/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' ... 行,然后在链接步骤失败,并显示错误消息

foo

显然,包含Haskell项目需要额外的库文件。我如何明确地依赖所有必要的东西,以避免这种脆弱?

包含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)

let uid = Auth.auth().currentUser.uid
let posts = Database.database().reference().child("user_posts")
let query = posts.queryOrdered(byChild: "").queryEqual(toValue: uid)
query.observe(.childAdded) {(snapshot: DataSnapshot) in
  ...

2 个答案:

答案 0 :(得分:2)

通常,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选项将强制加载所有库。

请注意,--no-as-needed选项取决于位置,因此请将其放在共享库之前。

答案 1 :(得分:2)

此答案说明了链接期间发生的情况,使用-Wl,--no-as-needed的解决方案为何有效以及应该采取什么措施才能拥有更强大的方法。

简而言之:-lHSrts-ghcXXX.so取决于libHSbaseXXX.solibHSinteger-gmpXXX.solibHSghc-primXXX.so,它们必须在链接期间提供给链接器。

此处提出的解决方案取决于大量的人工工作,并且伸缩性不是很好。但是,我对cabal知之甚少,无法告诉您如何自动执行此操作,但是我希望您可以迈出最后一步。

或者也许使用-Wl,--no-as-needed解决方案就可以了,因为您知道幕后发生的事情。


让我们首先以某种简化的方式逐步完成版本的链接过程,而无需调用foohere是Eli Bendersky的一篇很棒的文章,即使它涉及静态链接):

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

  2. 在对象文件main中找到了符号test.o的定义。但是,功能main使用功能hs_iniths_exit。因此,我们找到了main的定义,但是除非我们知道hs_iniths_exit的定义,否则它不会起作用。所以现在我们必须寻找它们的定义。

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

  4. 链接器查看-lHSrts-ghcXXX.so。在那里找到hs_iniths_exit的定义。因此,使用了共享库的全部内容,但是需要定义诸如base_GHCziTopHandler_flushStdHandles_closure之类的符号。这意味着链接器开始寻找这些符号的定义。

  5. 但是在命令行上没有更多的库,因此链接器一无所有,并且链接失败/失败,因为缺少某些符号的定义。

使用foo的情况有什么不同? 2.步骤之后,不仅需要hs_iniths_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-标志)以及可以在系统上找到它们的位置(RPATH)。找到/打开/处理这些库(即标记为需要),因此存在所有必需的定义。

您可以通过添加

来跟踪整个过程
g++ ...
    -Wl,--trace-symbol=base_GHCziTopHandler_flushStdHandles_closure \
    -Wl,--verbose \
    -o test

链接步骤。

如果我们强制按照{Yuras的建议,通过foo.so-Wl,--no-as-needed包含到生成的可执行文件中,则会发生同样的事情。

此分析的结果是什么?

我们应该在命令行上(-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时,cabal似乎可以做到),但是我不知道该怎么做,因为我没有使用过haskell / cabal的经验