c++ haskell linker ghc ffi

c++ - ¿Cómo vincular correctamente los archivos de objetos escritos en Haskell?



linker ghc (2)

Esta respuesta explica qué sucede durante el enlace, por qué la solución con -Wl,--no-as-needed y qué se debe hacer para tener un enfoque algo más sólido.

En pocas palabras: -lHSrts-ghcXXX.so depende de libHSbaseXXX.so , libHSinteger-gmpXXX.so y libHSghc-primXXX.so que deben proporcionarse al vinculador durante el enlace.

La solución aquí propuesta depende de mucho trabajo manual y no es muy escalable. Sin embargo, no sé lo suficiente sobre cabal para decirle cómo automatizar esto, pero espero que pueda dar el último paso.

O tal vez esté bien con el uso de la -Wl,--no-as-needed porque sabe lo que sucede detrás de escena.

Comencemos por pasar por el proceso de vinculación de la versión sin llamar a foo , de una manera un tanto simplificada ( here hay un gran artículo de Eli Bendersky, incluso si se trata de un enlace estático):

  1. El enlazador mantiene una tabla de símbolos y tiene que encontrar definiciones / código de máquina para todos ellos. Simplifiquemos y asumamos que, al principio, solo tiene el símbolo main en la tabla y la definición de este símbolo es desconocida.

  2. La definición de símbolo main se encuentra en el archivo de objeto test.o Sin embargo, la función main usa las funciones hs_init y hs_exit . Así encontramos la definición de main , pero no funciona a menos que sepamos las definiciones de hs_init y hs_exit . Así que ahora tenemos que buscar sus definiciones.

  3. En el siguiente paso, el enlazador mira foo.so , pero foo.so no define ningún símbolo en el que estemos interesados ​​(¡no se usa foo !) Y el enlazador simplemente omite foo.so y nunca mirará hacia atrás.

  4. El enlazador se ve en -lHSrts-ghcXXX.so . Allí encuentra las definiciones de hs_init y hs_exit . Por lo tanto, se utiliza todo el contenido de la biblioteca compartida, pero necesita definiciones de tales símbolos como, por ejemplo, base_GHCziTopHandler_flushStdHandles_closure . Eso significa que el enlazador comienza a buscar definiciones de estos símbolos.

  5. Sin embargo, no hay más bibliotecas en la línea de comandos, por lo tanto, el vinculador no tiene nada que ver y el enlace falla / no tiene éxito, porque faltan las definiciones de algunos símbolos.

¿Qué es diferente para el caso donde se usa foo ? Después del paso 2. no solo se hs_init y hs_exit , sino también foo , que se encuentra en foo.so Así que foo.so debe ser incluido.

Debido a la forma en que se construyó la biblioteca foo.so se foo.so la siguiente información:

>>> 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]

A partir de esta información, el enlazador sabe qué bibliotecas compartidas son necesarias ( NEEDED opción) y dónde se pueden encontrar en su sistema ( RPATH ). Estas bibliotecas se encuentran / abren / procesan (es decir, se marcan según sea necesario) y, por lo tanto, están presentes todas las definiciones necesarias.

Puedes seguir todo el proceso añadiendo

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

a la vinculación-paso.

Lo mismo sucede si foo.so que foo.so esté incluido en el ejecutable resultante a través de -Wl,--no-as-needed como sugiere @Yuras.

¿Cuál es la consecuencia de este análisis?

Deberíamos proporcionar las bibliotecas necesarias en la línea de comandos (después de -lHSrts-ghcXXX.so ) y no depender de que se agreguen por casualidad a través de otras bibliotecas compartidas. Obviamente, los nombres un tanto crípticos solo son válidos para mi instalación:

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

Ahora se construye, pero no se carga en el tiempo de ejecución (después de todo, la rpath correcta solo se establece en foo.so pero foo.so no se usa). Para solucionarlo, podemos extender el LD_LIBRARY_PATH o agregar -rpath el enlace-línea de comando:

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

Debe haber una utilidad para obtener las rutas y los nombres de las bibliotecas de forma automática (cabal parece hacerlo al crear foo.so ), pero no sé cómo hacerlo porque no tengo experiencia con haskell / cabal.

Aproximadamente siguiendo este tutorial , logré hacer funcionar este proyecto de juguete. Llama a una función de Haskell desde un programa de 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

  • construir script

    #!/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

Esto funciona (Ubuntu 16.04, GCC 5.4.0), imprimiendo 893 , pero no es realmente robusto, es decir, si elimino la invocación real de la función Haskell, es decir, el std::cout << foo(37, 19) << "/n"; línea, luego falla en el paso de enlace, con el mensaje de error

/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'' ...

Aparentemente, la inclusión del proyecto Haskell extrae archivos de biblioteca adicionales que son necesarios. ¿Cómo dependo explícitamente de todo lo necesario para evitar esa fragilidad?

Salida del script de compilación cuando se incluye la llamada foo , con ldd en el ejecutable final:

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


Por ghc general, los ghc ejecutables de enlaces de ghc con -Wl,--no-as-needed , y usted también debería usarlos. (Puede verificar cómo ejecutables de ghc links, por ejemplo, usando cabal build --ghc-options=-v3 ).

Puedes encontrar más detalles here . Entiendo lo siguiente: foo.so requiere que libHSbase-4.10.0.0-ghc8.2.1.so sea ​​cargado en el tiempo de ejecución, según sea necesario, es decir, cuando necesitamos un símbolo de él (ver readelf -a dist/build/foo.so/foo.so | grep NEEDED ). Entonces, si no llamas a foo , entonces la base.so no está cargada. Pero ghc necesita que se carguen todas las bibliotecas (no sé por qué). La --no-as-needed obliga a cargar todas las bibliotecas.

Tenga en cuenta que las opciones no --no-as-needed posición dependen de la posición, por lo que debe colocarlas antes de la biblioteca compartida.