Programmation:C:OCaml

De wiki-prog
Aller à : navigation, rechercher

Introduction

Le but de ce cours est de présenter quelques guides pour faire coopérer du code OCaml avec du code C. De manière générale, la documentation d'OCaml sur le sujet est bien plus complète, ce cours est surtout une introduction au sujet et quelques conseils pour éviter les principaux pièges. La mise en pratique en TP complètera bien plus ce sujet.

Principes

C et OCaml sont deux langages très différent, l'un est assez proche de la machine et principalement impératif tandis que l'autre s'appuie sur un modèle abstrait fonctionnel fondé sur une base théorique forte. Bien évidement, ce sont deux langages compilés qui au final produisent des exécutables machines utilisant le même code binaire et les mêmes facilités du système.

Il serait donc logique de pouvoir faire cohabiter du code des deux langages. Bien sûr la réalité n'est pas aussi simple.

Faire cohabiter les deux mondes

Le problème principal réside dans la représentation des données. Le C utilise un modèle simple avec peu de type de donnée et peu d'abstraction. OCaml quant à lui dispose de structures de haut niveau assez complexes.

Pour pouvoir interfacer du code C et du code OCaml, il faut donc être capable de traduire les structures de données des deux langages. Le C offrant plus de libertés dans ce domaine, ce sera donc le langage que nous choisirons (et qui nous est imposé) pour effectuer l'interface entre les deux mondes.

Le premier travail à effectuer est donc la traduction des données qui seront échangées entre les deux mondes. La solution la plus propre est d'écrire des stubs: de petites fonctions dont le rôle est de traduire une structure d'une représentation vers une autre.

Il est important de comprendre que ce passage d'un monde à l'autre n'est pas gratuit: il faut systématiquement traduire les données (même pour de simples entiers) ce qui a un coût. Il est donc pertinent de limiter les interractions, ou tout du moins limiter les interractions qui impliquent des traductions.

Heureusement, il est aussi possible de manipuler certains données du C en OCaml comme des types opaques: la données ne sera pas utilisée directement mais uniquement au travers des opérations fournies (et écrite en C.)

Une bonne approche est d'établir un diagramme d'interraction entre les différentes parties de votre code: vous identifierez les acteurs de votre programme et leur rôle, puis vous indiquerez les points de communication entre ces parties.

Exemple d'interraction entre acteurs du programme.

L'image accompagnant ce cours présente un schéma d'interraction entre différente partie d'un petit programme. Ce programme est composé d'une interface utilisateur (UI), un gestionnaire d'entrées/sorties sur le disque (IO) et un algorithme de lissage d'image (Lissage). En plus des acteurs, il y a une donnée principale (que l'on peut voir comme un acteur): l'image (Img.)

Sur le schéma, les interractions sont représentés par des flèches entre les acteurs: UI (qui reçoit les commandes de l'utilisateur) déclenche les actions des différents acteurs: elle peut demander le chargement d'une image à IO, le lissage de l'image chargée à Lissage, la sauvegarde sur le disque de l'image à IO ...

Lorsque IO charge l'image, il crée l'entité Img qui sera manipulée plus tard par Lissage. Si l'on suppose que Lissage est en OCaml et les deux autres éléments en C, il pourrait être astucieux de représenter l'image directement en OCaml (on peut faire plusieurs lissages de l'image.) Donc, en toute logique, IO charge l'image et construit la valeur OCaml pour la représentée.

Par contre, si l'UI a besoin d'afficher l'image également, il faut choisir une stratégie alternative: l'image reste un matrice de pixel construite en C mais Img est une valeur OCaml qui encapsule cette image (avec les informations de taille en valeur OCaml pour éviter les traductions) et on fournit juste deux opérations get_pixel et set_pixel. Comme ça, la mise à jour de l'affichage dans l'UI ne nécessite pas de traduction et les opérations sur les pixels ne nécessite que la traduction des coordonnées (on peut s'arranger pour les couleurs.)

On le voit, il est important de réfléchir assez rapidement aux interractions entre les différentes parties du programme lorsque celles-ci ne sont pas dans le même langage. Mais en réalité, ce travail devrait avoir déjà été fait dans la phase d'analyse/conception.

Qui appelle qui ?

Il est possible d'appeller des fonctions C en OCaml ou d'appeller des clotures OCaml (la représentation des fonctions) en C. Il faut choisir ce qui correspondra le mieux à nos besoins. En pratique, il est plus simple d'appeller les fonctions C en OCaml que l'inverse.

Là encore, il faut réfléchir à l'architecture de votre programme. Si vous souhaitez faire une interface en C, il vous faudra appeller vos parties OCaml depuis le C (même si votre programme principale reste en OCaml.)

L'appelle de fonction en OCaml est complexe, en effet, il est possible de faire un appel à partir de la cloture d'une fonction (la valeur fonctionnelle) mais cela nécessite d'avoir la cloture. Pour simplifier ce problème, OCaml fournit la possibilité d'enregistrer des fonctions pour les appeller en C.

Dans tous les cas, vous devrez identifier les fonctions qui seront appellées (d'un côté comme de l'autre) et créer (en C) les stubs nécessaires pour assurer la communication entre les deux mondes.

Programme principale en C

L'exécution d'un programme OCaml nécessite certaines initialisations (démarrage du garbage collector, enregistrement des fonctions pour leur réutilisation en C ... ) voir nécessite le chargement d'un bloc de bytecode et du code de la VM pour l'exécuter. On ne peut donc pas se contenter d'écrire un bout de code C qui va bêtement appeller des fonctions OCaml comme ça, il faut prévoir un peu l'initialisation.

Il y a deux possibilités:

  • Appeller caml_main(argv) qui exécutera le code OCaml comme si le programme principal était en OCaml.
  • Compiler le code OCaml avec -output-obj qui produira un bloc de code objet (fichier .o) avec entre autre la fonction caml_startup (qui prendra également argv comme paramètre.)

Voici un exemple utilisant la seconde possibilité:

Tout d'abord le code OCaml:

(* evaluator.c *)
(* Expr eval *)
 
type expr =
    Int of int
  | BinOp of (int -> int -> int) * string * expr * expr
  | UniOp of (int -> int) * string * expr
 
(* functions building expr to call in C *)
let make_addexpr e1 e2 = BinOp((+), "+", e1, e2)
let make_mulexpr e1 e2 = BinOp(( * ), "*", e1, e2)
let make_difexpr e1 e2 = BinOp((-), "-", e1, e2)
let make_divexpr e1 e2 = BinOp((/), "/", e1, e2)
let make_minexpr e = UniOp((~-), "-", e)
let make_intexpr i = Int i
 
let rec eval = function
    Int i -> i
  | BinOp(op, _, e1, e2) ->
      op (eval e1) (eval e2)
  | UniOp(op, _, e) ->
      op (eval e)
 
let pp_expr e =
  let rec pp = function
      Int i -> Format.printf "%d@;" i
    | BinOp(_,op,e1,e2) ->
	begin
	  Format.printf "@[<b 2>(@,";
	  pp e1;
	  Format.printf "%s " op;
	  pp e2;
	  Format.printf ")@]@;"
	end
    | UniOp(_,op,e) ->
	begin
	  Format.printf "@[<b 2>(%s " op;
	  pp e;
	  Format.printf ")@]@;"
	end
  in
    Format.printf "@[<b 2>";
    pp e;
    Format.printf "@]@."
 
(* registering functions *)
let _ =
  begin
    Callback.register "make_addexpr" make_addexpr;
    Callback.register "make_mulexpr" make_mulexpr;
    Callback.register "make_difexpr" make_difexpr;
    Callback.register "make_divexpr" make_divexpr;
    Callback.register "make_minexpr" make_minexpr;
    Callback.register "make_intexpr" make_intexpr;
    Callback.register "eval" eval;
    Callback.register "pp_expr" pp_expr;
  end

Et maintenant le code C:

/* eval.c */
/* eval using ocaml */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>
 
value addexpr(value e1, value e2)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_addexpr");
  return caml_callback2(*make, e1, e2);
}
 
value mulexpr(value e1, value e2)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_mulexpr");
  return caml_callback2(*make, e1, e2);
}
 
value difexpr(value e1, value e2)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_difexpr");
  return caml_callback2(*make, e1, e2);
}
 
value divexpr(value e1, value e2)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_divexpr");
  return caml_callback2(*make, e1, e2);
}
 
value minexpr(value e)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_minexpr");
  return caml_callback(*make, e);
}
 
value intexpr(int i)
{
  static value	       *make = NULL;
  if (!make)
    make = caml_named_value("make_intexpr");
  return caml_callback(*make, Val_int(i));
}
 
 
void pretty_printer(value e)
{
  static value	       *pp = NULL;
  if (!pp)
    pp = caml_named_value("pp_expr");
  caml_callback(*pp, e);
}
 
int eval(value e)
{
  static value	       *ev = NULL;
  if (!ev)
    ev = caml_named_value("eval");
  return Int_val(caml_callback(*ev,e));
}
 
int main(int argc, char *argv[])
{
  int			res;
  value			e;
  res = argc;
  caml_startup(argv);
  e = mulexpr(intexpr(42), intexpr(666));
  pretty_printer(e);
  printf("%d\n",eval(e));
  return 0;
}

Il faut maintenant compiler tout ça:

> ocamlopt.opt -output-obj -o caml_evaluator.o evaluator.ml
> gcc -O2 -W -Wall -Werror -std=c99 -o eval eval.c caml_evaluator.o \
   -L`ocamlc -where` -lunix -lasmrun -I`ocamlc -where` -lm
> ./eval
(42 * 666 )
27972
>

Représentation interne des valeurs OCaml

Nous allons faire une passe rapide sur la représentation des valeurs OCaml. À partir de cette représentation, nous pourrons plus simplement comprendre et traduire les valeurs OCaml en valeur C (et vice et versa.)

Valeurs immédiates et valeurs construites

OCaml manipule deux (en fait 3) sortes de valeurs:

  • les valeurs immédiates: int, bool, char et les constructeurs constants des types sommes.
  • les valeurs construites: tous les autres types.

Une valeur OCaml est donc soit un entier immédiat, soit un pointeur sur la valeur construite correspondant. Il est aussi possible d'avoir des pointeurs C directement (dans ce cas les programmes OCaml ne pourront pas utiliser ces valeurs directement.)

Dans tous les cas, les valeurs OCaml seront représentées par un seul type de valeur en C (le type value) correspondant donc à un entier ou un pointeur (ces valeurs auront donc la même taille que les entiers natifs, c'est à dire 32bits ou 64bits.)

Les valeurs immédiates sont marquées: le bit de poids faible est toujours à un (ce qui permet au garbage collector de les différencier des pointeurs.)

Une valeur construite est directement un pointeur sur un bloc décrivant la valeur en elle même. Ce bloc est composé d'un sous-bloc de méta-donnée et d'un tableau de valeur OCaml. Les méta-données contiennent des informations sur le type de données, la taille du bloc et d'autres infos pour le garbage collector.

Les tags qui indiquent le type de donnée sont résumé dans le tableau suivant:

Tag Contenu du block
0 à No_scan_tag-1 Bloc structuré (tableaux, records, nuplet ... )
Closure_tag Cloture (valeur fonctionnelle): contient le pointeur sur la fonction et l'environnement.
String_tag Une chaîne de caractères
Double_tag Un floattant en double précision
Double_array_tag Un tableau (ou un record) de floattant en double précision
Abstract_tag Un bloc représentant un valeur abstraite.
Custom_tag Un bloc contenant une valeur abstraite avec une fonction de finalisation, une fonction de comparaison, une fonction de hashage et les fonctions de sérialisation/désérialisation.

OCaml fournit des macros et fonctions pour accéder à toutes ces informations en C. Voici un petit example qui prend un valeur OCaml en paramètre et affiche quelques informations intéressantes:

/* ocaml_explore.c */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>
 
void margin (int n)
{
  while (n-- > 0) printf(".");
  return;
}
 
void print_block (value v,int m)
{
  int			size, i;
  margin(m);
  if (Is_long(v))
    {
      printf("valeur immédiate (%d)\n", Int_val(v));
      return;
    };
  printf ("valeur construite: taille=%d\n", size=Wosize_val(v));
  margin(m+2);
  switch (Tag_val(v))
    {
    case Closure_tag :
      printf("Cloture\n");
      break;
    case String_tag :
      printf("string: %s (%s)\n", String_val(v),(char *) v);
      break;
    case Double_tag:
      printf("float: %g\n", Double_val(v));
      break;
    case Double_array_tag :
      printf ("float array: ");
      for (i=0;i<size/Double_wosize;i++)
	printf("  %g", Double_field(v,i));
      printf("\n");
      break;
    case Abstract_tag :
      printf("abstract type\n");
      break;
    case Custom_tag :
      printf("custom type\n");
      break;
    default:
      if (Tag_val(v)>=No_scan_tag)
	{
	  printf("unknown tag");
	  break;
	};
      printf("structured block (tag=%d):\n",Tag_val(v));
      for (i=0;i<size;i++)
	print_block(Field(v,i),m+4);
    }
  return ;
}
 
value inspect_block (value v)
{
  print_block(v,2);
  fflush(stdout);
  return v;
}

Code que l'on peut appeller en OCaml:

(* explore.ml *)
external inspect : 'a -> 'a = "inspect_block" ;;
 
module M : sig type t val make: int -> t end =
struct
  type t = Nil | Cons of int * t
  let rec make = function
      0 -> Nil
    | n -> Cons (n, make (n-1))
end
 
let _ =
  begin
    ignore (inspect 1);
    ignore (inspect (1::2::[]));
    ignore (inspect [|1;2;3|]);
    ignore (inspect (Some(3.14)));
    ignore (inspect (M.make 5));
    ignore (inspect (object val a = 0 method get = a end));
  end

La compilation et l'exécution nous donne:

> ocamlopt.opt -o explore ocaml_explore.c explore.ml
> ./explore 
..valeur immédiate (1)
..valeur construite: taille=2
....structured block (tag=0):
......valeur immédiate (1)
......valeur construite: taille=2
........structured block (tag=0):
..........valeur immédiate (2)
..........valeur immédiate (0)
..valeur construite: taille=3
....structured block (tag=0):
......valeur immédiate (1)
......valeur immédiate (2)
......valeur immédiate (3)
..valeur construite: taille=1
....structured block (tag=0):
......valeur construite: taille=2
........float: 3.14
..valeur construite: taille=2
....structured block (tag=0):
......valeur immédiate (5)
......valeur construite: taille=2
........structured block (tag=0):
..........valeur immédiate (4)
..........valeur construite: taille=2
............structured block (tag=0):
..............valeur immédiate (3)
..............valeur construite: taille=2
................structured block (tag=0):
..................valeur immédiate (2)
..................valeur construite: taille=2
....................structured block (tag=0):
......................valeur immédiate (1)
......................valeur immédiate (0)
..valeur construite: taille=3
....structured block (tag=248):
......valeur construite: taille=4
........structured block (tag=0):
..........valeur immédiate (1)
..........valeur immédiate (3)
..........valeur construite: taille=3
............Cloture
..........valeur immédiate (5144726)
......valeur immédiate (0)
......valeur immédiate (0)

Exemple: taille d'une liste OCaml en C

Le code C:

/* clist.c */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>
 
value list_len(value l)
{
  int			r = 0;
  for (;Is_block(l); l = Field(l,1))
    ++r;
  return Val_int(r);
}

Le code OCaml pour tester:

(* list_ex.ml *)
external len : 'a list -> int = "list_len"
 
let _ =
  begin
    Printf.printf "%d\n" (len [1;2;3;4;5;6;7;8;9;10]);
    exit 0;
  end

Et on compile et on teste:

> ocamlopt.opt -o list-ex clist.c list_ex.ml
> ./list-ex
10
Cours Partie
Cours de Programmation EPITA/spé Programmation:C