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/caml où LIBOCAML 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 :
-
un entier ;
- un pointeur vers le tas d'Objective CAML ;
- un pointeur hors du tas d'Objective CAML.
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 :
-
les entiers sont codés par leur valeur,
- les caractères sont codés par leur code ASCII,
- les constructeurs constants sont codés par un entier
dénotant leur place dans la déclaration du type : le
n ème constructeur constant d'un type
énuméré sera codé par n-1.
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
1
2
3
;;
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
(
1
0
,
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=
1
0
;
ch2=
2
0
}
;;
....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
.
1
4
;
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
1
0
;;
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>