Précédent Index Suivant

Exploration des valeurs Objective CAML en C

Le langage Objective CAML ne construit pas les valeurs de la même façon que le langage C, y compris pour les valeurs simples comme les entiers. Cela provient de la nécessité pour le GC d'Objective CAML de conserver des informations supplémentaires pour effectuer la récupération mémoire. Comme la représentation des valeurs en Objective CAML est uniforme, elles seront vues en C comme des valeurs d'un type unique, nommé value.

Quand Objective CAML appelle une fonction C et lui passe des arguments, ceux-ci doivent être décodés pour être utilisés en C. Il en est de même pour le résultat d'une fonction C appelée depuis Objective CAML. Celui-ci sera encodé avant d'être transmis à Objective CAML.

Un certain nombre de macros et de fonctions C réalisent ces opérations de conversion de valeurs. Elles sont regroupées dans les fichiers décrits à la figure 12.3 qui sont fournis dans la distribution d'Objective CAML. Ces fichiers se trouvent dans le répertoire LIBOCAML/camlLIBOCAML est le répertoire où les bibliothèques d'Objective CAML sont installées3.


   
mlvalues.h définition du type value et macros de manipulations de ces valeurs.
alloc.h fonctions d'allocations de valeurs Objective CAML.
memory.h macros de plus haut niveau pour la manipulation de valeurs.

Figure 12.3 : Fichiers utiles pour l'interfaçage avec C


Classification des valeurs de type value

Une valeur de type value peut être soit : Le tas d'Objective CAML est la zone d'allocation gérée par le GC d'un programme Objective CAML. Un programme C, peut pour sa part créer et manipuler des valeurs dans sa propre zone d'allocation et communiquer une référence de cette valeur à Objective CAML.

On dispose des macros de vérification et de conversion de types décrites à la figure 12.4.

   
Is_long(v) v est un entier Objective CAML?
Is_block(v) v est un pointeur Objective CAML?
   
Long_val(v) retourne un entier C long
Int_val(v) retourne un entier C
Bool_val(v) retourne un ``booléen'' C (0 pour false)

Figure 12.4 : Conversion des valeurs immédiates


Il est à noter que C possède plusieurs types pour les entier : short, int et long alors qu'Objective CAML n'a qu'une seule représentation des entiers : le type int.

Accès aux valeurs immédiates

Les entiers servent à encoder toutes les valeurs immédiates d'Objective CAML : Le programme suivant définit une fonction C inspecte d'inspection une valeur de type value :
#include <stdio.h>
#include <caml/mlvalues.h>
value inspecte (value v)
{
if (Is_long(v))
printf ("v est un entier (%d) : %d",(int) v,Long_val(v));
else if (Is_block(v)) printf ("v est un pointeur") ;
else printf ("v n'est ni un entier ni un pointeur ???") ;
printf(" ");
fflush(stdout) ;
return v ;
}
La fonction inspecte teste si son argument est un entier Objective CAML. Si oui elle en affiche la valeur vue comme un int C puis cette même valeur convertie par la macro Long_val en entier C (long).

La représentation des entiers d'Objective CAML diffère de celle de C comme le montre l'utilisation suivante de la fonction inspecte.

# external inspecte : 'a -> 'a = "inspecte" ;;
external inspecte : 'a -> 'a = "inspecte"
# inspecte 123 ;;
v est un entier (247) : 123 - : int = 123
# inspecte max_int;;
v est un entier (2147483647) : 1073741823 - : int = 1073741823
D'autres valeurs de type prédéfinis, comme char et bool, sont représentées par des valeurs immédiates.

# inspecte 'A' ;;
v est un entier (131) : 65 - : char = 'A'
# inspecte true ;;
v est un entier (3) : 1 - : bool = true
# inspecte false ;;
v est un entier (1) : 0 - : bool = false
# inspecte [] ;;
v est un entier (1) : 0 - : '_a list = []


Soit le type foo défini en Objective CAML :

# type foo = C1 | C2 of int | C3 | C4 ;;


La fonction inspecte affichera des résultats de nature différente en présence d'un constructeur constant ou d'un constructeur fonctionnel.

# inspecte C1 ;;
v est un entier (1) : 0 - : foo = C1
# inspecte C4 ;;
v est un entier (5) : 2 - : foo = C4
# inspecte (C2 1) ;;
v est un pointeur - : foo = C2 1


Lorsqu'elle reconnaît une valeur immédiate, la fonction inspecte affiche entre parenthèses le contenu << physique >> de cette valeur (i.e. sa valeur vue comme un entier signé sur un mot -- type int de C), puis elle affiche son contenu << logique >> (la valeur obtenue par les macros de décodage). Nos exemples manifestent la différence entre ces deux contenus. Celle-ci provient du bit de tag4 qu'utilise le GC pour distinguer une valeur immédiate d'un pointeur (voir chapitre 9, page ??).

Discrimination des valeurs structurées

Les valeurs structurées d'Objective CAML correspondent à toutes les valeurs non immédiates : n-uplets, listes non vides, constructeurs avec au moins un argument, vecteurs, enregistrements, fermetures et valeurs de type abstrait. Elles sont allouées dans le tas Objective CAML sous forme de blocs mémoire. Un bloc possède un en-tête contenant une information sur la nature du bloc ainsi que sa taille en mots machine. La figure 12.5 décrit la structure d'un bloc pour une machine à mots de 32 bits.


Figure 12.5 : communication de valeurs entre Objective CAML et C


Les deux bits << couleur >> sont utilisés par le GC lors de son exploration du graphe des valeurs du tas (voir chapitre 9, page ??). Le champ << tag >> (que nous appellerons tag, pour faire court) indique le << type >> de valeurs contenues dans le suite du bloc. Les fonctions de la figure 12.6 retournent ces informations.

   
Wosize_val(v) retourne la taille du bloc (sans l'en-tête)
Tag_val(v) retourne le tag du bloc

Figure 12.6 : Informations sur les blocs mémoire


Les différentes valeur de tag sont données à la figure 12.7.

de 0 à No_scan_tag-1 un tableau Objective CAML: un tableau de value
Closure_tag une fermeture
String_tag une chaîne de caractères
Double_tag un flottant en double-précision
Double_array_tag un tableau de flottants
Abstract_tag un type de données abstrait
Final_tag idem avec une fonction de finalisation

Figure 12.7 : Description des marques des blocs mémoire


Suivant la valeur retournée par Tag_val, on utilise différentes macros pour accéder aux contenus des blocs mémoire. Lorsque cette valeur est inférieure à No_scan_tag, le bloc mémoire est structuré comme un tableau de value. Les macros d'accès sont décrites à la figure 12.8. En accord avec les conventions de C et d'Objective CAML le premier élément d'un tableau se situe à l'indice 0.

   
Field(v,n) retourne la n ème value d'un tableau.
Code_val(v) retourne le pointeur de code d'une fermeture.
string_length(v) retourne la longueur d'une chaîne.
Byte(v,n) retourne le n ème caractère d'une chaîne comme char.
Byte_u(v,n) idem mais retourne un unsigned char.
String_val(v) retourne la chaîne de caractère comme (char *).
Double_val(v) retourne le flottant codé dans v.
Double_field(v,n) retourne le n ème élément d'un tableau de flottants.

Figure 12.8 : Accès aux éléments d'un bloc


Comme nous l'avons fait pour les valeurs immédiates, nous allons définir une fonction d'inspection des blocs mémoires. La fonction C print_bloc teste si son argument (de type value) est une valeur immédiate ou un bloc mémoire. Dans ce dernier cas elle affiche le type du bloc et sa valeur vue de C. Elle est appelée depuis inspecte_bloc qui sera utilisée à partir d'un programme Objective CAML.

#include <stdio.h>
#include <caml/mlvalues.h>

void marge (int n)
{ for (;n>0;n--) printf("."); return; }

void print_bloc (value v,int m)
{
int taille,i ;
marge(m);
if (Is_long(v))
{ printf("valeur immediate (%d)\n", Long_val(v)); return; };
printf ("bloc : taille=%d - ", taille=Wosize_val(v));
switch (Tag_val(v))
{
case Closure_tag :
printf("fermeture - Arguments (%d) :\n",taille==1?0:taille-2);
marge(m+4); printf("pointeur de code : %d\n",Code_val(v)) ;
for (i=1;i<taille;i++) print_bloc(Field(v,i), m+4);
break;
case String_tag :
printf("string : %s (%s)\n", String_val(v),(char *) v);
break;
case Double_tag:
printf("flottant : %g\n", Double_val(v));
break;
case Double_array_tag :
printf ("tableau de flottants : ");
for (i=0;i<(taille/2);i++) printf(" %g", Double_field(v,i));
printf("\n");
break;
case Abstract_tag : printf("type abstrait\n"); break;
case Final_tag : printf("type abstrait + final\n"); break;
default:
if (Tag_val(v)>=No_scan_tag) { printf("inconnu"); break; };
printf("tableau de valeurs (Tag=%d) :\n",Tag_val(v));
for (i=0;i<taille;i++) print_bloc(Field(v,i),m+4);
}
return ;
}

value inspecte_bloc (value v)
{ print_bloc(v,4); fflush(stdout); return v; }

Les différentes marques de blocs se retrouvent dans les cas du switch. On redéfinit ensuite une fonction inspecte :

# external inspecte : 'a -> 'a = "inspecte_bloc" ;;
external inspecte : 'a -> 'a = "inspecte_bloc"
Celle-ci est utilisée pour décrire expérimentalement les représentations des valeurs structurées Objective CAML. On évitera de lui passer une structure circulaire car son parcours bouclerait.

Tableaux, n-uplets et enregistrements

Les tableaux (array) et les n-uplets sont codés par des tableaux de value.

# inspecte [| 1; 2; 3 |] ;;
....bloc : taille=3 - tableau de valeurs (Tag=0) :
........valeur immediate (1)
........valeur immediate (2)
........valeur immediate (3)
- : int array = [|1; 2; 3|]
# inspecte ( 10 , true , () ) ;;
....bloc : taille=3 - tableau de valeurs (Tag=0) :
........valeur immediate (10)
........valeur immediate (1)
........valeur immediate (0)
- : int * bool * unit = 10, true, ()


Les enregistrements sont eux aussi codés par des tableaux de value dans le même ordre que celui de la déclaration du type. Le fait qu'un champ soit modifiable ne change pas sa représentation physique.

# type foo = { ch1 : int ; mutable ch2:int } ;;
type foo = { ch1: int; mutable ch2: int }
# inspecte { ch1=10 ; ch2=20 } ;;
....bloc : taille=2 - tableau de valeurs (Tag=0) :
........valeur immediate (10)
........valeur immediate (20)
- : foo = {ch1=10; ch2=20}


Warning


Rien n'empêche de modifier physiquement un champ non modifiable d'un enregistrement Objective CAML à partir de fonctions C. C'est au programmeur de prendre garde à ne pas introduire d'incohérence en utilisant une fonction C.


Types somme

Nous avons vu précédemment que les constructeurs constants sont représentés par des entiers. Les autres constructeurs sont codés par le tableau de leurs arguments. Le constructeur est identifié par la valeur de tag. Il représente le numéro d'apparition du constructeur non constant dans la déclaration du type : 0 correspond au premier constructeur non constant, 1 au deuxième et ainsi de suite.

# type foo = C1 of int * int * int | C2 of int | C3 | C4 of int * int ;;
type foo = | C1 of int * int * int | C2 of int | C3 | C4 of int * int
# inspecte (C1 (1,2,3)) ;;
....bloc : taille=3 - tableau de valeurs (Tag=0) :
........valeur immediate (1)
........valeur immediate (2)
........valeur immediate (3)
- : foo = C1 (1, 2, 3)
# inspecte (C4 (1,2)) ;;
....bloc : taille=2 - tableau de valeurs (Tag=2) :
........valeur immediate (1)
........valeur immediate (2)
- : foo = C4 (1, 2)


Remarque


Le type list est un type somme dont la déclaration est :
type 'a list = [] | :: of 'a * 'a list. Il possède un seul constructeur non constant infixe (::) qui a un tag égal à 0.


Chaînes de caractères

Comme un caractère est codé sur un octet, la représentation des chaînes de caractères utilisera un mot pour quatre caractères.

Warning


Les chaînes de caractères Objective CAML peuvent contenir le caractère de code ASCII 0 qui est le caractère de fin de chaîne en C.


#include <stdio.h>
#include <caml/mlvalues.h>

value explore_string (value v)
{
char *s;
int i,taille;
s = (char *) v;
taille = Wosize_val(v) * sizeof(value);
for (i=0;i<taille;i++)
{
int p = (unsigned int) s[i] ;
if ((p>31) && (p<128)) printf("%c",s[i]) ;
else printf("(#%u)",p) ;
}
printf("\n");
fflush(stdout);
return v;
}

La fin d'une chaîne Objective CAML est déterminée par la taille du bloc la constituant, ainsi que par le dernier octet du dernier mot du bloc qui indique le nombre d'octets non utilisés dans le dernier mot. Les exemples ci-dessous permettent de comprendre le rôle exact que joue ce dernier octet.

# external explore : string -> string = "explore_string" ;;
external explore : string -> string = "explore_string"
# ignore(explore "");
ignore(explore "a");
ignore(explore "ab");
ignore(explore "abc");
ignore(explore "abcd");
ignore(explore "abcd\000") ;;
(#0)(#0)(#0)(#3)
a(#0)(#0)(#2)
ab(#0)(#1)
abc(#0)
abcd(#0)(#0)(#0)(#3)
abcd(#0)(#0)(#0)(#2)
- : unit = ()
Dans les deux derniers cas ("abcd" et "abcd\000") les chaînes explorées sont respectivement de longueur 4 et 5. C'est pour cela que le dernier octet change de valeur pour ces deux valeurs.

Flottants et tableaux de flottants

Objective CAML ne possède qu'un seul type (float) pour les nombres à virgule flottante. Les valeurs de type float sont allouées, elles sont représentées par un bloc de taille 2.

# inspecte 1.5 ;;
....bloc : taille=2 - flottant : 1.5
- : float = 1.5
# inspecte 0.0;;
....bloc : taille=2 - flottant : 0
- : float = 0


Les tableaux de flottants possèdent une structure spéciale qui permet d'optimiser leur occupation mémoire. C'est pourquoi ils ont un tag distinct de celui des autres tableaux ainsi qu'une macro d'accès particulière.

# inspecte [| 1.5 ; 2.5 ; 3.5 |] ;;
....bloc : taille=6 - tableau de flottants : 1.5 2.5 3.5
- : float array = [|1.5; 2.5; 3.5|]
Cette optimisation permet d'utiliser Objective CAML pour effectuer des calculs numériques qui manipulent ce genre de données beaucoup plus rapidement que s'il fallait passer par un pointeur dans le tas.

Warning


Si l'on alloue un bloc en C pour un tableau de flottants Objective CAML, il sera nécessaire d'allouer un bloc de taille double du nombre d'éléments voulus.


En dehors des float array, les flottants contenus dans d'autres structures conservent leur représentation habituelle, c'est-à-dire qu'ils sont vus comme une valeur structurée allouée dans le tas. L'exemple suivant inspecte une liste de flottants.

# inspecte [ 3.14; 1.2; 7.6];;
....bloc : taille=2 - tableau de valeurs (Tag=0) :
........bloc : taille=2 - flottant : 3.14
........bloc : taille=2 - tableau de valeurs (Tag=0) :
............bloc : taille=2 - flottant : 1.2
............bloc : taille=2 - tableau de valeurs (Tag=0) :
................bloc : taille=2 - flottant : 7.6
................valeur immediate (0)
- : float list = [3.14; 1.2; 7.6]
La liste est vue comme un bloc de taille 2 : sa tête et sa queue. La tête est un flottant, bloc de taille 2 aussi.

Fermetures

Une valeur fonctionnelle est représentée d'une part par le code qui doit être exécuté à son application, et d'autre part par son environnement (voir chapitre 2, page??). Il y a deux façons d'obtenir une valeur fonctionnelle : en utilisation explicitement une abstraction (comme dans fun x -> x+1 ou en appliquant partiellement une fonction (comme dans (fun x -> fun y -> x+y) 1).

L'environnement d'une fermeture peut contenir trois catégories de variables : celles déclarées globalement, celles déclarées localement et les paramètres instanciées par une application partielle. Du point de vue implantation, ces trois catégories de variables ont un traitement différent. Les variables globales font partie d'un environnement global et ne figurent pas explicitement dans les fermetures. Les variable locales et les paramètres instanciés par application partielle peuvent apparaître dans la fermeture comme nous allons l'illustrer. Ainsi, du point de vue implantation, l'environnement d'une fermeture de concerne que des variables locales ou des paramètres abstraits.

Si la fermeture a un environnement vide, elle est uniquement constituée du pointeur vers le code :

# let f = fun x y z -> x+y+z ;;
val f : int -> int -> int -> int = <fun>
# inspecte f ;;
....bloc : taille=1 - fermeture - Arguments (0) :
........pointeur de code : 134893716
- : int -> int -> int -> int = <fun>
Si elle est obtenue par application partielle d'une abstraction sans déclaration locale, elle contient une valeur pour chacun des paramètres et une fermeture sans environnement.

# let a1 = f 1 ;;
val a1 : int -> int -> int = <fun>
# inspecte (a1) ;;
....bloc : taille=3 - fermeture - Arguments (1) :
........pointeur de code : 134893712
........bloc : taille=1 - fermeture - Arguments (0) :
............pointeur de code : 134893716
........valeur immediate (1)
- : int -> int -> int = <fun>
# let a2 = a1 2 ;;
val a2 : int -> int = <fun>
# inspecte (a2) ;;
....bloc : taille=4 - fermeture - Arguments (2) :
........pointeur de code : 134893712
........bloc : taille=1 - fermeture - Arguments (0) :
............pointeur de code : 134893716
........valeur immediate (1)
........valeur immediate (2)
- : int -> int = <fun>
La figure 12.9 illustre graphiquement l'affichage précédent.


Figure 12.9 : représentation des fermetures


La fonction f ne contient pas de variable libre. Le pointeur de code d'une fermeture sans environnement pointe vers le code à appliquer lorsque tous ses arguments sont passés, c'est-à-dire ici, le code correspondant à x+y+z. Une fermeture avec environnement pointe vers un code unique (c'est le même pour a1 et a2). Ce code vérifie quand on lui passe un nouvel argument si tous les arguments sont là. Si oui il empile les arguments et déclenche le code initial, sinon il crée une nouvelle fermeture. C'est ce qui se passe quand a1 est appliquée à 2. On peut remarquer qu'une fermeture avec environnement contient toujours, comme premier élément de l'environnement, le pointeur vers la fermeture sans environnement dont l'application partielle est issue. Cela permettra, d'une part, de déclencher le code effectif et d'autre part, d'avoir une référence sur soi même pour les fonctions récursives.

En présence d'une déclaration locale, l'application partielle donne la représentation suivante :

# let g x = let y=2 in fun z -> x+y+z ;;
val g : int -> int -> int = <fun>
# let a1 = g 1 ;;
val a1 : int -> int = <fun>
# inspecte a1 ;;
....bloc : taille=3 - fermeture - Arguments (1) :
........pointeur de code : 134893792
........valeur immediate (1)
........valeur immediate (2)
- : int -> int = <fun>


L'appel d'une fermeture C à partir d'Objective CAML est détaillé à la section 12.

Types abstraits

Une valeur d'un type abstrait est aussi représentée par un tableau de value. En fait l'information sur le type n'est utile que pour le synthétiseur de types. À aucun moment de l'exécution d'un programme l'information de type n'est utile, seule la représentation mémoire doit être connue du GC ainsi que la taille de la valeur.

Si on crée une valeur d'un type abstrait, comme 'a Stack.t, en fait sa représentation mémoire correspond à celle de son implantation, c'est-à-dire ici comme une référence sur une liste.

# let p = Stack.create();;
val p : '_a Stack.t = <abstr>
# Stack.push 3 p;;
- : unit = ()
# inspecte p;;
....bloc : taille=1 - tableau de valeurs (Tag=0) :
........bloc : taille=2 - tableau de valeurs (Tag=0) :
............valeur immediate (3)
............valeur immediate (0)
- : int Stack.t = <abstr>
Par contre certains types abstraits sont implantés en utilisant une représentation non exprimable en Objective CAML. C'est par exemple le cas pour les vecteurs de pointeurs faibles ou les canaux d'entrées-sorties. Pour ceux-là la marque Abstract_tag sera utilisée.

# let w = Weak.create 10;;
val w : '_a Weak.t = <abstr>
# Weak.set w 0 (Some p);;
- : unit = ()
# inspecte w;;
....bloc : taille=11 - type abstrait
- : int Stack.t Weak.t = <abstr>
Certains de ces types possèdent aussi une fonction de finalisation, permettant une dernière action avant de disparaître (en particulier la libération de la mémoire qu'ils occupent). C'est particulièrement utile pour rendre une ressource externe, comme un tampon d'entrées-sorties. C'est pour cela que l'inspection de la sortie standard indiquera que le type out_channel possède une fonction de finalisation :

# inspecte (stdout) ;;
....bloc : taille=2 - type abstrait + final
- : out_channel = <abstr>



Précédent Index Suivant