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

foreign import capi "../csrc/fibo.c fibo" c_fibo :: CInt -> CInt

myfibo :: Int -> Int
myfibo = fromIntegral . c_fibo . fromIntegral

main :: IO ()
main = print $ map myfibo [0 .. 10]

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

foreign import capi "fiboc.h fibo" c_fibo :: CInt -> CInt
...

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
    ...
    extra-libraries:    fiboc

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
myfibo = fromIntegral . fibo . fromIntegral

foreign export capi myfibo :: CInt -> CInt

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;
    hs_init(&argc, &pargv);
}

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

foreign-library fibohs
    type:               native-shared
    cc-options:         -std=c++17 -Wall -Wextra
    c-sources:          csrc/binding.cpp csrc/native.cpp 
    install-includes:   csrc/binding.hpp csrc/native.hpp
    hs-source-dirs:     src
    other-modules:      Binding Fibo
    ...

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() {

    fibohsInit();
    for (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.