Créer et modifier des valeurs Objective CAML en C
L'appel d'une fonction C à partir d'Objective CAML peut modifier ses
arguments ou retourner une nouvelle valeur. Cette valeur doit
correspondre à un type Objective CAML. Pour les types simples, plusieurs
macros C sont prédéfinies pour faciliter les conversions. Pour une
valeur d'un type structuré, elle doit être allouée de la bonne taille
et ses champs remplis par des valeurs du bon type. Il est en effet
facile de faire n'importe quoi en C, ce qui risque de provoquer un
arrêt de l'exécution du programme Objective CAML appelant ce morceau de C.
Il est important que les nouvelles valeurs Objective CAML créées en C soient
connues du GC d'Objective CAML. Ces valeurs, le temps de leur portée,
doivent être considérées comme de nouvelles racines pour le GC. Pour
cela plusieurs macros sont définies pour ajouter des racines à
conserver par le GC.
De plus, il est possible d'allouer à partir de C des valeurs Objective CAML
mais aussi C, dans le tas d'Objective CAML. Cela permet de profiter du
mécanisme de récupération automatique de mémoire d'Objective CAML dans les
programmes C. Pour des valeurs dont le type n'est connu qu'en C, il
est nécessaire d'indiquer au GC leur fonction de finalisation pour
la libération d'un bloc de ce type.
Modification des valeurs Objective CAML
Valeurs immédiates
Les macros suivantes permettent de créer des valeurs immédiates
de type value à partir de valeur C ainsi que d'en modifier:
|
|
Val_long(l) |
à partir d'un long int |
Val_int(i) |
à partir d'un int |
Val_bool(x) |
à partir d'un int (false si x=0, true sinon) |
Val_true |
|
Val_false |
|
Val_unit |
|
|
|
Store_field(b,n,v) |
met la valeur v dans le champ n d'un bloc b |
Store_double_field(b,n,d) |
idem pour un tableau de
flottants |
Figure 12.10 : Création et modification de valeurs immédiates
Il est en outre possible d'utiliser l'une des macros Field, Byte
et Byte_u pour affecter les champs d'un bloc :
Store_field(b,n,v) ÜÞ Field(b,n) = v.
Voici un exemple pour inverser une chaîne de caractères :
#include <caml/mlvalues.h>
value swap_char(value v, int i, int j)
{ char c=Byte(v,i); Byte(v,i)=Byte(v,j); Byte(v,j)=c; }
value swap_string (value v)
{
int i,j,t = string_length(v) ;
for (i=0,j=t-1; i<t/2; i++,j--) swap_char(v,i,j) ;
return v ;
}
# external
miroir
:
string
->
string
=
"swap_string"
;;
external miroir : string -> string = "swap_string"
# miroir
"abcdefg"
;;
- : string = "gfedcba"
Allocation de nouveaux blocs
Il est important d'utiliser les macros suivantes pour allouer des valeurs dans
le tas d'Objective CAML, sans cela, les blocs n'auraient pas les
informations nécessaires au GC pour les manipuler. La figure
12.11 liste les différentes fonctions d'allocation de
blocs dans le tas Objective CAML.
|
|
alloc(n, t) |
retourne un nouveau bloc de taille n et de
Tag t |
alloc_tuple(n) |
idem mais avec le Tag 0 |
alloc_string(n) |
retourne une chaîne (non initialisée) de
longueur n |
copy_string(s) |
retourne un bloc à partir d'une chaîne C |
copy_double(d) |
retourne un bloc à partir d'un double |
alloc_array(f, a) |
retourne un bloc tableau à partir d'une fonction f |
|
de conversion
et d'un tableau de pointeurs |
copy_string_array(p) |
retourne un tableau de chaîne à
partir d'un |
|
(char**), le tableau p doit se finir par un |
|
pointeur nul |
Figure 12.11 : Fonctions d'allocation de blocs
La macro alloc_array prend un tableau de pointeurs (a) se
terminant par le pointeur nul et une fonction f qui prend un
pointeur et rend une valeur. Elle rend un tableau de valeur obtenu
en appliquant f à chacun des pointeurs de a. La fonction
make_str_array de l'exemple ci-dessous utilise la macro
alloc_array pour créer des copies de chacune des chaînes
d'un tableau :
#include <caml/mlvalues.h>
value make_str (char *s) { return copy_string(s); }
value make_str_array (char **p) { return alloc_array(make_str,p) ; }
Il est aussi possible de définir des blocs de taille 0, c'est par
exemple le cas d'un tableau Objective CAML vide. Un tel bloc est appelé un
atome.
# inspecte
[|
|]
;;
....bloc : taille=0 - tableau de valeurs (Tag=0) :
- : '_a array = [||]
Il est possible de l'invoquer en C par la macro Atom(t) où t est le tag du bloc créé. En fait, de tels blocs sont
constants et ne sont pas alloués dans la partie dynamique du tas
Objective CAML.
Des valeurs C dans le tas Objective CAML
On peut souhaiter conserver dans le tas Objective CAML des valeurs qui ne
proviennent pas de ce langage et qui ne sont donc pas soumis aux
contraintes imposées par Objective CAML pour le GC. Dans ce cas, on
utilisera les blocs abstraits.
L'exemple le plus naturel est de manipuler avec Objective CAML des entiers C
utilisant 32 (ou 64) bits. Une telle valeur va être conservée dans
un bloc de taille 1 (la taille d'une valeur est celle d'un entier
long) de tag Abstract_tag.
#include <caml/mlvalues.h>
#include <stdio.h>
value Cint_of_OCAMLint (value v)
{
value res = alloc(1,Abstract_tag) ;
Field(res,0) = Long_val(v) ;
return res ;
}
value OCAMLint_of_Cint (value v) { return Val_long(Field(v,0)) ; }
value Cplus (value v1,value v2)
{
value res = alloc(1,Abstract_tag) ;
Field(res,0) = Field(v1,0) + Field(v2,0) ;
return res ;
}
value printCint (value v)
{
printf ("%d",(long) Field(v,0)) ; fflush(stdout) ;
return Val_unit ;
}
# type
cint
external
cint_of_int
:
int
->
cint
=
"Cint_of_OCAMLint"
external
int_of_cint
:
cint
->
int
=
"OCAMLint_of_Cint"
external
plus_cint
:
cint
->
cint
->
cint
=
"Cplus"
external
print_cint
:
cint
->
unit
=
"printCint"
;;
Nous pouvons maintenant utiliser des entiers sans << bit de tags >>, donc
dans le format courant en C tout en conservant le GC d'Objective CAML. Par
contre, ce ne sont plus des valeurs immédiates ce qui sera
plus coûteux pour les opérations arithmétiques simples.
# let
a
=
1
0
0
0
0
0
0
0
0
0
;;
val a : int = 1000000000
# a+
a
;;
- : int = -147483648
# let
c
=
let
b
=
cint_of_int
a
in
plus_cint
b
b
;;
val c : cint = <abstr>
# print_cint
c
;
print_newline
()
;;
2000000000
- : unit = ()
# int_of_cint
c
;;
- : int = -147483648
Fonction de finalisation
Il est aussi possible de stocker dans un bloc abstrait des pointeurs
vers des blocs alloués hors du tas Objective CAML. Nous savons qu'une valeur
Objective CAML qui n'est plus utilisée par le programme est susceptible
d'être récupérée par le GC. Mais que devient alors un bloc du
tas C référencé par une valeur qui a disparue ? Pour éviter
cette fuite de mémoire, nous pouvons associer une fonction dite de
finalisation qui est exécutée par le GC avant de récupérer
le bloc abstrait.
Pour ce faire nous utiliserons un bloc marqué par le tag Final_tag. Un tel bloc contient en plus des valeurs souhaitées, une
fonction prenant comme argument le bloc lui-même et qui est
exécutée avant la récupération de la valeur.
L'allocation de tels blocs se fait en utilisant
alloc_final (n, f, used, max) :
-
n est la taille du bloc. Attention, la première case
du bloc est utilisée pour stocker la fonction de finalisation.
- f est la fonction de finalisation : void f (value);
- used : place mémoire (hors du tas Objective CAML) supposée
utilisée par la valeur.
- max : place mémoire maximale que l'on accepte de ne pas
récupérer systématiquement.
Pour être plus efficace, le GC ne récupère pas un bloc dès
qu'il devient inutilisé. Le rapport used/max permet de
spécifier le nombre de blocs abstraits que le GC peut laisser
alloués alors qu'ils ne sont plus utilisables.
Le programme suivant manipule des tableaux d'entiers C alloués dans le
tas C par appel à malloc. Pour que ces tableaux puissent être
bien récupérés lors d'un GC d'Objective CAML, la fonction create crée
un bloc contenant le pointeur vers le tableau d'entiers (type
IntTab) et la fonction de finalisation finalize_it.
#include <malloc.h>
#include <stdio.h>
#include <caml/mlvalues.h>
typedef struct {
int size ;
long * tab ; } IntTab ;
IntTab *alloc_it (int s)
{
IntTab *res = malloc(sizeof(IntTab)) ;
res->size = s ;
res->tab = (long *) malloc(sizeof(long)*s) ;
return res ;
}
void free_it (IntTab *p) { free(p->tab) ; free(p) ; }
void put_it (int n,long q,IntTab *p) { p->tab[n] = q ; }
long get_it (int n,IntTab *p) { return p->tab[n]; }
void finalize_it (value v)
{
IntTab *p = (IntTab *) Field(v,1) ;
int i;
printf("recuperation d'un IntTab par finalisation [") ;
for (i=0;i<p->size;i++) printf("%d ",p->tab[i]) ;
printf("]\n"); fflush(stdout) ;
free_it ((IntTab *) Field(v,1)) ;
}
value create (value s)
{
value bloc ;
bloc = alloc_final (2, finalize_it,Int_val(s)*sizeof(IntTab),1000) ;
Field(bloc,1) = (value) alloc_it(Int_val(s)) ;
return bloc ;
}
value put (value n,value q,value t)
{
put_it (Int_val(n), Long_val(q), (IntTab *) Field(t,1)) ;
return Val_unit ;
}
value get (value n,value t)
{
long res = get_it (Int_val(n), (IntTab *) Field(t,1)) ;
return Val_long(res) ;
}
Les fonctions visibles à partir d'Objective CAML seront : create,
put et get.
# type
c_int_array
external
cia_create
:
int
->
c_int_array
=
"create"
external
cia_get
:
int
->
c_int_array
->
int
=
"get"
external
cia_put
:
int->
int
->
c_int_array
->
unit
=
"put"
;;
On peut désormais manipuler notre nouvelle structure de données :
# let
tab
=
cia_create
1
0
and
tab2
=
cia_create
1
0
in
for
i=
0
to
9
do
cia_put
i
(i*
2
)
tab
done
;
for
i=
0
to
9
do
print_int
(cia_get
i
tab)
;
print_string
" "
done
;
print_newline
()
;
for
i=
0
to
9
do
cia_put
(9
-
i)
(cia_get
i
tab)
tab2
done
;
for
i=
0
to
9
do
print_int
(cia_get
i
tab2)
;
print_string
" "
done
;;
0 2 4 6 8 10 12 14 16 18
18 16 14 12 10 8 6 4 2 0 - : unit = ()
On force un GC pour provoquer l'appel à la fonction de finalisation :
# Gc.full_major
()
;;
recuperation d'un IntTab par finalisation [18 16 14 12 10 8 6 4 2 0 ]
recuperation d'un IntTab par finalisation [0 2 4 6 8 10 12 14 16 18 ]
- : unit = ()
La fonction de finalisation peut aussi être utilisée pour fermer un
fichier, terminer un processus, etc.
Paramètres, variables locales des fonctions C et GC
Il est possible d'invoquer le GC depuis une fonction C, les primitives
pour le faire sont définies dans le fichier memory.h. La
fonction void Garbage_collection_function ()
est la primitive C
d'invocation du GC. Selon que le programme est compilé en code-octet
ou en code natif le GC mineur ou majeur est invoqué.
Examinons l'exemple suivant et voyons quelle erreur a pu se produire.
#include <caml/mlvalues.h>
#include <caml/memory.h>
value identite (value x)
{
Garbage_collection_function() ;
return x;
}
# external
id
:
'a
->
'a
=
"identite"
;;
external id : 'a -> 'a = "identite"
# id
[
1
;2
;3
;4
;5
]
;;
- : int list = [539709628; 539709622; 539709616; 539709610; 539709604]
La liste passée en paramètre de id, donc de la fonction
C identite, est susceptible d'être déplacée ou
récupérée par le GC. Nous avons, dans notre exemple, forcé
l'appel au GC, mais toute allocation d'un bloc aurait pu en faire tout
autant. La liste, anonyme, passée en argument à id a
été modifiée par le passage du GC car elle n'est pas connue dans
l'ensemble des racines. Pour éviter celà, il faut utiliser une
déclaration spécifiques des paramètres et des variables locales,
qui sont des value, utilisés par les primitives C pour qu'ils
soient correctement traités par le GC en utilisant les macro suivantes :
CAMLparam1(v) |
: |
pour une valeur, |
CAMLparam2(v1,v2) |
: |
pour deux valeurs, |
... |
|
... |
CAMLparam5(v1,...,v5) |
: |
pour cinq valeurs, |
CAMLparam0 ; |
: |
obligatoire quand il n'y a pas de valeurs. |
Si il y a plus de cinq paramètres de type value, les cinq
premiers sont déclarés avec la macro CAMLparam5 et les
suivants en utilisant le nombre de fois nécessaires les macros CAMLxparam1, ..., CAMLxparam5.
CAMLparam5(v1,...,v5); |
CAMLxparam5(v6,...,v10); |
CAMLxparam2(v11,v12); |
: |
pour douze valeurs |
Les variables locales de type value doivent être elles-aussi
déclarées auprès du GC en utilisant les macros CAMLlocal1,
..., CAMLlocal5. La déclaration d'un tableau de valeurs se
fait avec la macro CAMLlocalN(tab,n) où n est la taille
du tableau tab. Enfin, si la primitive retourne une value, nous utiliserons la macro CAMLreturn.
Voici donc la version correcte de l'exemple précédent :
#include <caml/mlvalues.h>
#include <caml/memory.h>
value identite2 (value x)
{
CAMLparam1(x) ;
Garbage_collection_function() ;
CAMLreturn x;
}
# external
id
:
'a
->
'a
=
"identite2"
;;
external id : 'a -> 'a = "identite2"
# let
a
=
id
[
1
;2
;3
;4
;5
]
;;
val a : int list = [1; 2; 3; 4; 5]
Cette fois nous obtenons le résultat attendu.
Appel d'une fermeture Objective CAML depuis C
Pour, dans une fonction C, appliquer une fermeture (i.e. une
valeur fonctionnelle Objective CAML) à un ou plusieurs arguments, nous
utiliserons les macros définies dans le fichier callback.h.
callback(f,v) |
: |
une fermeture f appliquée à un
argument v, |
callback2(f,v1,v2) |
: |
à deux arguments, |
callback3(f,v1,v2,v3) |
: |
à trois arguments, |
callbackN(f,n,tab) |
: |
à n arguments stockés dans le
tableau tab. |
Toutes ces macros retournent une value résultat de l'application.
Déclarations de fonctions Objective CAML
L'utilisation des macros callback nécessite d'avoir la
fonction à appliquer sous la forme d'une fermeture, c'est-à-dire
d'une valeur qu'il aura fallu passer en argument de la primitive. Il
est aussi possible d'enregistrer une fermeture depuis un programme
Objective CAML en lui associant un nom et de la récupérer dans un
programme C grâce à son nom.
La fonction register du module Callback associe un
nom (string) à une fermeture (de type quelconque donc
'a). Cette fermeture peut-être retrouvée depuis C grâce
à la fonction C caml_named_value qui prend une chaîne de
caractères et rend un pointeur vers la fermeture associée au nom,
si elle existe, ou le pointeur nul, sinon.
Voyons cela sur un petit exemple :
# let
plus
x
y
=
x
+
y
;;
val plus : int -> int -> int = <fun>
# Callback.register
"plus3_ocaml"
(plus
3
);;
- : unit = ()
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/callback.h>
value plus3_C (value v)
{
CAMLparam1(v);
CAMLlocal1(f);
f = * caml_named_value("plus3_ocaml") ;
CAMLreturn callback(f,v) ;
}
# external
plusC
:
int
->
int
=
"plus3_C"
;;
external plusC : int -> int = "plus3_C"
# plusC
1
;;
- : int = 4
# Callback.register
"plus3_ocaml"
(plus
5
);;
- : unit = ()
# plusC
1
;;
- : int = 6
Il n'y a pas exactement correspondance entre la déclaration
d'une primitive C par external et l'enregistrement d'une
fermeture Objective CAML par la fonction register. Dans le premier
cas, il s'agit d'une déclaration statique, le lien entre les deux
noms se fait à l'édition de lien. Alors que dans l'autre cas, il
s'agit d'une liaison dynamique qui intervient durant l'exécution. En
particulier, rien n'interdit de remplacer la liaison nom--fermeture en
remplaçant la fermeture et en modifiant ainsi le comportement des
fonctions C utilisant ce nom.