Interopérabilité Haskell/C, avec Nix
Voir aussi : video youtube - code source
Beaucoup de langages proposent une interface C, notamment pour pouvoir écrire des sections de code en C ou appeler une bibliothèque C existante. Haskell permet d’appeler du code C et, réciproquement, d’être appelé dans du code C. Cet article présente quelques cas d’utilisation classiques de l’interopérabilité Haskell/C, avec un peu de Nix.
Utiliser du code C dans un projet Haskell
Dans ce premier cas d’utilisation, on veut tout simplement écrire une section de code en C, à l’intérieur d’un projet Haskell.
Projet fibo1
Il suffit d’écrire le code C dans un fichier :
// fibo1/csrc/fibo.c
int fibo(int n) {
...
puis d’importer les éléments voulus. Par exemple, le code suivant importe la fonction fibo
précédente sous le nom c_fibo
. Généralement, on écrit une fonction (myfibo
) qui convertit les types Haskell/C et que l’on peut appeler directement dans du code Haskell classique :
-- fibo1/src/Main.hs
{-# LANGUAGE CApiFFI #-}
import Foreign.C.Types
import capi "../csrc/fibo.c fibo" c_fibo :: CInt -> CInt
foreign
myfibo :: Int -> Int
= fromIntegral . c_fibo . fromIntegral
myfibo
main :: IO ()
= print $ map myfibo [0 .. 10] main
et c’est tout, la compilation devrait prendre en compte le fichier fibo.c
automatiquement :
[nix-shell]$ cabal run
...
[0,1,1,2,3,5,8,13,21,34,55]
Importer une lib C dans un projet Haskell
Dans ce deuxième cas d’utilisation, on a une bibliothèque C et on veut l’utiliser dans un projet Haskell.
Bibliothèque fiboc
Comme d’habitude en C, on écrit le code de la bibliothèque dans des fichiers .h
et .c
:
// fibo2/fiboc/include/fiboc.h
int fibo(int n);
// fibo2/fiboc/src/fiboc.c
#include <fiboc.h>
int fibo(int n) {
...
Pour configurer la compilation et l’installation, on peut utiliser CMake :
# fibo2/fiboc/CMakeLists.txt
cmake_minimum_required( VERSION 3.0 )
project( fiboc )
include_directories( include )
add_library( fiboc SHARED src/fiboc.c )
set_target_properties( fiboc PROPERTIES PUBLIC_HEADER "include/fiboc.h" )
install( TARGETS fiboc LIBRARY DESTINATION lib PUBLIC_HEADER DESTINATION include )
On pourrait ensuite compiler et installer le projet manuellement mais ici on va plutôt utiliser le système de paquets de Nix. Pour cela, il suffit d’écrire le fichier suivant :
# fibo2/fiboc/default.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
name = "fiboc";
src = ./.;
buildInputs = with pkgs; [ cmake ];
}
Nix se charge alors de construire et d’installer tout ce qu’il faut. On peut le vérifier avec :
[fibo2/fiboc]$ nix-build
...
[fibo2/fiboc]$ tree result
result
├── include
│ └── fiboc.h
└── lib
└── libfiboc.so
Projet fibo2
Au niveau du projet Haskell, il suffit d’importer la fonction désirée, comme pour le premier cas d’utilisation :
-- fibo2/src/Main.hs
{-# LANGUAGE CApiFFI #-}
import Foreign.C.Types
import capi "fiboc.h fibo" c_fibo :: CInt -> CInt
foreign ...
Cependant, la fonction fibo
n’est plus ici dans le projet mais dans la bibliothèque fiboc
. Il faut donc ajouter celle-ci dans la configuration de projet :
-- fibo2/fibo2.cabal
...
executable fibo2...
-libraries: fiboc extra
Enfin, on peut expliciter la dépendance à la bibliothèque fiboc
avec Nix :
# fibo2/default.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.haskellPackages.callCabal2nix "fibo2" ./. {
fiboc = (pkgs.callPackage ./fiboc/default.nix {});
}
Ainsi, pour construire et exécuter la bibliothèque et le projet, il suffit de lancer :
[fibo2]$ nix-build
...
[fibo2]$ ./result/bin/fibo2
[0,1,1,2,3,5,8,13,21,34,55]
Utiliser du code Haskell dans un projet C++
Dans ce dernier cas d’utilisation, on a du code Haskell et on veut l’utiliser dans un projet C++. Ceci est un peu plus compliqué car le code Haskell s’exécute dans un runtime, qu’il faut donc également intégrer dans le projet C++.
Bibliothèque fibohs
Cette fois-ci la fonction fibo
est implémentée dans un module en Haskell :
-- fibo3/fibohs/src/Fibo.hs
module Fibo where
fibo :: Int -> Int
= ... fibo
Au niveau du binding, on écrit une fonction (myfibo
) avec des types utilisables en C et on l’exporte :
-- fibo3/fibohs/src/Binding.hs
{-# LANGUAGE CApiFFI #-}
module Binding where
import Fibo
import Foreign.C.Types
myfibo :: CInt -> CInt
= fromIntegral . fibo . fromIntegral
myfibo
myfibo :: CInt -> CInt foreign export capi
Enfin, on écrit la partie en C du binding, pour exporter la fonction myfibo
et lancer/arrêter le runtime Haskell :
// fibo3/fibohs/csrc/binding.hpp
void fibohsInit(void);
void fibohsExit(void);
#ifndef __GLASGOW_HASKELL__
extern "C" {
int myfibo(int);
}
#endif
// fibo3/fibohs/csrc/binding.cpp
#include "binding.hpp"
#include "HsFFI.h"
void fibohsInit(void) {
char *argv[] = { (char *)"+RTS", (char *)"-A32m", 0 };
int argc = sizeof(argv)/sizeof(argv[0]) - 1;
char **pargv = argv;
(&argc, &pargv);
hs_init}
void fibohsExit(void) {
();
hs_exit}
Au niveau de la configuration Cabal, on demande de construire une bibliothèque externe (on peut également inclure du code C++ natif supplémentaire) :
-- fibo3/fibohs/fibohs.cabal
...
-library fibohs
foreigntype: native-shared
-options: -std=c++17 -Wall -Wextra
cc-sources: csrc/binding.cpp csrc/native.cpp
c-includes: csrc/binding.hpp csrc/native.hpp
install-source-dirs: src
hs-modules: Binding Fibo
other...
Enfin, on peut packager le tout avec Nix :
# fibo3/fibohs/default.nix
{ pkgs ? import <nixpkgs> {} }:
let drv = pkgs.haskellPackages.callCabal2nix "fibohs" ./. {};
in with pkgs; stdenv.mkDerivation {
name = "fibohs";
src = ./.;
installPhase = ''
mkdir -p $out/{include,lib}
cp `find ${drv} -name "*.so"` $out/lib/
cp `find ${drv} -name "*.hpp"` $out/include/
'';
}
Projet fibo3
On peut alors utiliser notre fonction myfibo
dans du code C++, après avoir initialisé le runtime :
// fibo3/main.cpp
#include <binding.hpp>
#include <iostream>
int main() {
();
fibohsInitfor (int i=0; i<=5; ++i)
std::cout << "myfibo(" << i << ") = " << myfibo(i) << std::endl;
();
fibohsExit...
Il faut bien-entendu inclure notre bibliothèque fibohs
dans la configuration CMake :
# fibo3/CMakeLists.txt
cmake_minimum_required( VERSION 3.0 )
project( fibo3 )
add_executable( fibo3 main.cpp )
target_link_libraries( fibo3 fibohs )
install( TARGETS fibo3 DESTINATION bin )
et indiquer la dépendance dans la configuration Nix :
# fibo3/default.nix
{ pkgs ? import <nixpkgs> {} }:
let fibohs = pkgs.callPackage ./fibohs/default.nix {};
in pkgs.stdenv.mkDerivation {
name = "fibo3";
src = ./.;
buildInputs = with pkgs; [ cmake fibohs ];
}
Exemple d’exécution :
[fibo3]$ nix-build
...
[fibo3]$ ./result/bin/fibo3
myfibo(0) = 0
myfibo(1) = 1
myfibo(2) = 1
myfibo(3) = 2
myfibo(4) = 3
myfibo(5) = 5
Conclusion
Haskell est interopérable avec C. On peut interfacer assez facilement du code C ou une bibliothèque C, dans un projet en Haskell et, réciproquement, on peut utiliser une bibliothèque Haskell dans un projet en C. Et accessoirement, Nix permet de simplifier la gestion des dépendances.
Enfin, Haskell propose également d’autres outils pour écrire du code C directement dans un module Haskell (inline-c
), pour générer automatiquement des bindings, pour manipuler des objets/pointeurs, etc.