Précédent Index Suivant

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)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 = 1000000000 ;;
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)  : 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 10 and tab2 = cia_create 10
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)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.


Précédent Index Suivant