Quelques notes sur l'interpréteur de bytecode OCaml. Fichiers alloc.c et alloc.h: ---------------------------- Diverses fonctions d'allocation mémoire. Fichier array.c: ---------------- Fonctions de création et d'accès aux tableaux. Fichiers backtrace.c et backtrace.h: ------------------------------------ Gestion des backtraces d'exceptions. Les backtraces sont stockés dans un buffer global de taille fixe. Ils sont créés à la demande (cf. RAISE). Ils sont constitutés simplement d'une liste d'adresses de retour. Lorsqu'on désire les afficher, ils sont interprétés pour donner quelque chose de lisible. Cela demande de rouvrir le fichier bytecode pour en extraire la section "DBUG", laquelle est disponible si le programme a été compilé avec l'option -g. Fichiers callback.h et callback.c: ---------------------------------- Fonctions permettant d'invoquer une clôture Caml depuis une fonction C. Existent en deux variantes, selon que l'on veut que les exceptions soient propagées ou non. Fichiers compact.h et compact.c: -------------------------------- GC. Fichier config.h: ----------------- Règle un ensemble de paramètres liés à la gestion de la mémoire. Fichiers custom.c et custom.h: ------------------------------ Allocation et gestion des blocs `custom', c'est-à-dire dotés de fonctions propres pour la finalisation, la comparaison, le hashing, la sérialisation et désérialisation. Fichiers debugger.c et debugger.h: ---------------------------------- Interface avec le debugger. Lorsque la fonction [debugger] est appelée, on tombe dans un mode où on écoute les commandes envoyées par le debugger sur une socket, et on y répond. On sort de ce mode et on reprend l'exécution lorsque le debugger dit 'GO'. Fichiers dynlink.c et dynlink.h: -------------------------------- Gestion des librairies partagées. Fichier exec.h: --------------- Décrit la structure des fichiers bytecode. Fichier extern.c: ----------------- La mécanique de sérialisation (output_value). Fichiers fail.c et fail.h: -------------------------- Fournit diverses fonctions liées aux exceptions. Une série de fonctions permettent de lever des exceptions prédéfinies. Ces fonctions sont définies en termes de [raise_constant] et [raise_with_arg], qui allouent un objet exception à partir de son nom (un pointeur vers un bloc dans le tas) et d'un éventuel argument et appellent [mlraise]. Quant à [mlraise], il stocke un pointeur vers l'objet exception dans une variable globale, [exn_bucket], et effectue un [siglongjmp] à l'aide du [longjmp_buffer] [external_raise], qui doit avoir été défini au préalable, sans quoi l'exception est considérée comme non rattrapée et l'exécution cesse. Cette définition se situe dans le code d'initialisation de l'interprète (interp.c), où on trouve un appel à [sigsetjmp]. Fichiers finalise.c et finalise.h: ---------------------------------- GC. Fichiers fix_code.c et fix_code.h: ---------------------------------- Charge le bytecode en mémoire. En calcule un digest MD5, qui sert de signature lorsqu'on fait du marshalling de clôtures. Corrige le code si le processeur est big endian (ce genre de détail apparaît en d'autres endroits, dès qu'on lit des mots et non des octets). Si on souhaite utiliser le threaded code, réécrit le code en remplaçant tous les opcodes par des pointeurs dans la table de saut. Fichier floats.c: ----------------- La librairie standard sur les flottants. Fichiers freelist.c et freelist.h: ---------------------------------- GC. Fichiers gc_ctrl.c et gc_ctrl.h: -------------------------------- GC. Fichier gc.h: ------------- Macros reliés au GC. Fichiers globroots.c et globroots.h: ------------------------------------ GC (enregistrement des racines globales). Fichier hash.c: --------------- Implémente la fonction de hash primitive. Fichiers instrtrace.c et instrtrace.h: -------------------------------------- Permettent d'imprimer les instructions au fur et à mesure qu'elles sont exécutées. Contient donc un désassembleur primaire (les adresses sont imprimées sous formes d'offsets bruts). Ce mode est activé en passant l'option 't' à ocamlrun, à condition qu'il ait été compilé avec l'option DEBUG. Fichier instruct.h: ------------------- Définit le jeu d'instructions, associant ainsi un entier à chaque opcode symbolique. Fichiers int64_emul.h, int64_format.h, int64_native.h: ------------------------------------------------------ Émule l'arithmétique 64 bits et l'affichage des entiers 64 bits, pour les compilateurs C qui ne la supportent pas. Pour ceux qui la supportent, définit des macros triviales. Fichier intern.c: ----------------- La mécanique de désérialisation (input_value). Fichiers interp.c et interp.h: ------------------------------ Le coeur de l'interprète de bytecode. Les registres de l'interprète sont les suivants: [pc] est un pointeur vers la prochaine instruction à exécuter; [sp] est le pointeur de pile; [accu] l'accumulateur, un registre distingué manipulé par certaines instructions; [env] un pointeur un bloc dans le tas (en fait une clôture) contenant l'environnement courant; [trapsp] un pointeur vers le handler le plus récent dans la pile; [extra_args] un registre dédié à la gestion des sous- et sur- applications de fonctions. [accu] et [env] sont des racines potentielles pour le GC, mais ne lui sont pas directement visibles; ils sont copiés sur la pile avant chaque appel au GC et restaurés ensuite. [sp] est copié dans la variable globale [extern_sp] lors des appels au GC, aux primitives C, etc. On utilise des "macros d'ordre supérieur": l'interprète définit les macros [Setup_for_gc] et [Restore_after_gc], qui sont elles-mêmes appelées par la macro [Alloc_small] (memory.h), qui va servir à chaque allocation de bloc. L'interprète est apparemment conçu pour être réentrant: on a un compteur [callback_depth] qui compte combien de fois il a été invoqué au-dessus de lui-même. Ce compteur est incrémenté lorsque l'interprète démarre. Il est décrémenté lorsque l'interprète termine sur une exception non rattrapée (instruction RAISE) ou termine normalement (instruction STOP). Au démarrage, l'interprète effectue un [sigsetjmp] destiné à rattraper les exceptions lancées depuis une primitive C (fail.c). Lorsqu'une telle exception est rattrapée, il restaure les racines locales du GC, le pointeur de pile tel qu'il était avant l'appel à C, place l'exception dans l'accumulateur, et exécute ensuite l'instruction RAISE, de façon à propager l'exception. Il initialise ensuite les registres. [sp] est lu depuis [extern_sp], ce qui doit permettre de reprendre la pile là où elle en était en cas d'appel réentrant. L'environnement initial est vide. Ensuite, on attaque la boucle d'exécution. Voici maintenant la description des instructions. Je ne discute pas ici précisément le codage binaire des instructions, qui n'est pas très intéressant. En particulier, les sauts relatifs utilisent des offsets dont l'origine est en général l'instruction dans laquelle ils se trouvent, plus une constante. Je ne précise pas cela, ce qui revient à supposer que les sauts sont absolus. Opérations de manipulation de la pile. ACC n Place la valeur du n-ième mot de la pile dans l'accumulateur. Les éléments de la pile sont numérotés à partir de 0. Existe en version ACCn pour n entre 0 et 7 inclus. PUSH Sauvegarde l'accumulateur sur la pile, dont la hauteur augmente donc de 1. PUSHACC n Équivalent à la séquence PUSH; ACC n. À noter que PUSHACC0 est donc équivalent à PUSH. Existe en version PUSHACCn pour n entre 0 et 7 inclus. POP n Détruit n mots au sommet de la pile, dont la hauteur diminue donc de n. ASSIGN n Copie l'accumulateur dans le n-ième mot de la pile, sans changer la hauteur de celle-ci. L'accumulateur est ensuite effacé et reçoit un pointeur vers la valeur [unit]. Accès à l'environnement. ENVACC n Place la valeur de la n-ième entrée de l'environnement dans l'accumulateur. Les entrées de l'environnement sont numérotées à partir de 0, mais l'entrée 0 est en général un pointeur de code, dans la mesure où l'environnement n'est autre que la clôture de la fonction en cours d'exécution. Existe en version ENVACCn pour n compris entre 1 et 4 inclus. PUSHENVACC n Équivalent à la séquence PUSH; ENVACC n. Existe en version PUSHENVACCn pour n compris entre 1 et 4 inclus. Applications de fonctions. PUSH_RETADDR n Sauvegarde, dans l'ordre, sur la pile: + la valeur courante du registre [extra_args]; + la valeur courante du registre [env]; + une adresse de retour, calculée comme l'adresse de l'instruction suivante + n - 1; pour pouvoir implanter correctement les appels terminaux, l'adresse de retour n'est donc pas forcément l'adresse de l'instruction suivante. APPLY n Exige qu'un pointeur vers une clôture soit présent dans l'accumulateur. Règle [extra_args] à n - 1, de sorte que celui-ci devient nul dans le cas courant de l'application unaire (n=1). Met à jour [pc] et [env] pour l'exécution de la clôture. Si le pointeur de pile a atteint la zone seuil, exige une extension de la pile (stacks.c). Si un signal a eu lieu, traite celui-ci (signals.c). APPLYn Attention, cet opcode n'est pas équivalent à APPLY n! On peut en résumer l'effet comme suit: + Retire n mots de la pile, qui représentent les arguments de la fonction; + Simule PUSH_RETADDR 1 (l'adresse de retour est donc l'instruction suivante); + Replace les n arguments sur la pile; + Simule APPLY n. Existe pour n entre 1 et 3 inclus. APPTERM n s Exige s >= n. + Détruit le contenu de la pile entre les positions n (inclue) et n+s (exclue), ce qui correspond au scénario où on se prépare à effectuer un appel terminal, donc on jette la frame courante, de taille s, sauf les n arguments de la fonction à laquelle on transfère le contrôle; + Simule APPLY n, à cette différence près que [extra_args] ne *reçoit* pas la valeur n - 1, mais *est incrémenté* de n - 1, ce qui est important, puisque, contrairement au cas de l'application normale, [extra_args] n'est pas sauvegardé. Existe en version APPTERMn s pour n entre 1 et 3 inclus. RETURN n + Détruit les n mots situés au sommet de la pile. + Si [extra_args] est nul, le sommet de la pile doit alors être constitué d'une frame de retour (cf. PUSH_RETADDR). L'instruction rétablit alors [pc], [env], et [extra_args], et détruit cette frame. + Sinon, le sommet de la pile doit être constitué d'arguments supplémentaires, et l'accumulateur (qui par convention contient le résultat de l'appel précédent) doit contenir une clôture. On décrémente alors [extra_args], et on met à jour [pc] et [env] pour exécuter cette clôture, ce qui correspond à un appel terminal. RESTART Si l'environnement courant contient 1+n entrées (l'entrée numéro 0 étant toujours le pointeur de code, ignoré), alors les n-1 dernières entrées sont copiées sur la pile (l'entrée numéro 2 devenant donc le nouveau sommet de pile), et l'entrée numéro 1 est écrite dans [env]. GRAB n Cette instruction doit être précédée d'un RESTART. + Si [extra_args] est supérieur ou égal à n, il est décrémenté de n. + Sinon, on alloue une clôture, dont les composantes sont les suivantes: * Le pointeur de code est le RESTART précédent. * L'entrée numéro 1 est la valeur courante de l'environnement. * Les entrées suivantes sont les [extra_args + 1] mots situés au sommet de la pile, qui sont dépilés. Cette clôture est placée dans l'accumulateur. Ensuite, on remet [extra_args] à zéro, et on simule RETURN. CLOSURE n k Alloue et place dans l'accumulateur un pointeur vers une clôture dont les composantes sont les suivantes: + Le pointeur de code est situé à l'offset k; + L'environnement est constitué des n mots obtenus en concaténant la valeur initiale de l'accumulateur et les n-1 mots au sommet de la pile, qui sont dépilés. CLOSUREREC f n k_0 k_1 .. k_f Exige f >= 1. Alloue une clôture dont les composantes sont les suivantes: + D'abord, un pointeur de code, situé à l'offset k_0; + Ensuite, pour chaque indice i entre 1 inclus et f exclus, * Un header de bloc portant le tag "infixe", de façon à ce qu'il soit permis (du point de vue du GC) de créer un pointeur vers cette position du bloc. * Un pointeur de code, situé à l'offset k_i; + Enfin, comme dans le cas CLOSURE, les n mots obtenus en concaténant la valeur initiale de l'accumulateur et les n-1 mots au sommet de la pile, qui sont dépilés. À l'issue de l'instruction, on trouve sur la pile les [f] clôtures partagées ainsi créées, celle située au sommet de la pile étant celle d'indice [f]. OFFSETCLOSURE n Charge dans l'accumulateur *l'adresse* de la n-ième entrée de l'environnement. Cela est utile dans le cas où on a créé des fonctions mutuellement récursives à l'aide de CLOSUREREC: lorsqu'on est en train d'exécuter une de ces fonctions, l'environnement contient donc un pointeur (éventuellement infixe) vers la clôture partagée, et l'instruction OFFSETCLOSURE permet donc de charger dans l'accumulateur un autre pointeur (lui aussi éventuellement infixe) vers cette même clôture. Autrement dit, si f et g sont deux fonctions mutuellement récursives, l'accès à f depuis g (ou vice-versa) se fait par OFFSETCLOSURE. Existe en version OFFSETCLOSUREn pour n=-2,0,2. PUSHOFFSETCLOSURE n Équivalent à la séquence PUSH; OFFSETCLOSURE n. Existe en version PUSHOFFSETCLOSUREn pour n=-2,0,2. Accès aux variables globales. GETGLOBAL n Charge dans l'accumulateur le contenu de la variable globale numéro n. PUSHGETGLOBAL n Équivalent à la séquence PUSH; GETGLOBAL n. GETGLOBALFIELD n k Charge dans l'accumulateur le contenu du k-ième champ de la variable globale numéro n. PUSHGETGLOBALFIELD n k Équivalent à la séquence PUSH; GETGLOBALFIELD n k. SETGLOBAL n Copie l'accumulateur dans la variable globale numéro n. L'accumulateur reçoit ensuite la valeur [unit]. Allocation de blocs. ATOM n L'accumulateur reçoit un pointeur vers l'atome numéro n (lequel est pré-alloué). Existe en version ATOMn pour n=0. PUSHATOM n Équivalent à la séquence PUSH; ATOM n. Existe en version PUSHATOMn pour n=0. MAKEBLOCK s t Alloue un bloc mémoire de taille s et d'étiquette t. Le premier champ reçoit la valeur courante de l'accumulateur, les s-1 autres champs sont pris sur la pile. L'adresse du bloc est placée dans l'accumulateur. Existe en version MAKEBLOCKs t pour s entre 1 et 3 inclus. MAKEFLOATBLOCK s Alloue un bloc mémoire de taille appropriée pour recevoir s flottants et d'étiquette [Double_array_tag]. Le premier champ reçoit la valeur courante de l'accumulateur, les s-1 autres champs sont pris sur la pile. (L'accumulateur, ainsi que chaque case de la pile, est capable de contenir un flottant.) L'adresse du bloc est placée dans l'accumulateur. Accès aux composantes des blocs. GETFIELD n Place dans l'accumulateur la valeur du n-ième champ de l'objet vers lequel pointe l'accumulateur. Existe en version GETFIELDn pour n entre 0 et 3 inclus. GETFLOATFIELD n Place dans l'accumulateur la valeur *boxée* du n-ième champ de l'objet vers lequel pointe l'accumulateur. SETFIELD n Dépile un mot, et le copie dans le n-ième champ de l'objet vers lequel pointe l'accumulateur. L'accumulateur reçoit la valeur [unit]. Existe en version SETFIELDn pour n entre 0 et 3 inclus. SETFLOATFIELD n Dépile l'adresse d'un flottant *boxé*, et en copie le contenu dans le n-ième champ de l'objet vers lequel pointe l'accumulateur. L'accumulateur reçoit la valeur [unit]. Opérations sur les tableaux. VECTLENGTH Place dans l'accumulateur la longueur du tableau (en nombre d'éléments, que celui-ci soit un tableau de flottants ou non) situé dans l'accumulateur. GETVECTITEM Accède au i-ème élément du tableau situé dans l'accumulateur, où i est le premier mot sur la pile (qui est dépilé). Le résultat est placé dans l'accumulateur. SETVECTITEM Écrit la valeur v dans le i-ème élément du tableau situé dans l'accumulateur, où i est le premier mot sur la pile et v le second (qui sont dépilés). L'accumulateur est inchangé. Opérations sur les chaînes de caractères. GETSTRINGCHAR Similaire à GETVECTITEM, mais pour les tableaux de caractères. SETSTRINGCHAR Similaire à SETVECTITEM, mais pour les tableaux de caractères. L'accumulateur reçoit la valeur [unit]. Branchements. BRANCH k Branchement inconditionnel à l'offset k. BRANCHIF k Branchement si l'accumulateur ne contient pas le booléen false. BRANCHIFNOT k Branchement si l'accumulateur contient le booléen false. SWITCH m n j_1 .. j_m k_1 .. k_n (m et n sont codés comme deux entiers de 16 bits.) + Si l'accumulateur contient un entier i, effectue un saut à l'offset j_i. + Si l'accumulateur contient un pointeur vers un bloc d'étiquette i, effectue un saut à l'offset k_i. BOOLNOT Remplace l'accumulateur par sa négation. Exceptions. PUSHTRAP k Pousse sur la pile une frame contenant, du sommet vers le fond: + L'adresse du handler, situé à l'offset k; + La valeur courante de [trapsp], de sorte que les frames de handlers vont former une liste chaînée dans la pile; + La valeur courante de [env], de sorte qu'on stocke effectivement une clôture sur la pile, à savoir la continuation exceptionnelle; + La valeur courante de [extra_args]; Pour finir, [trapsp] prend la valeur courante de [sp], c'est-à-dire qu'il vient pointer vers cette nouvelle frame. POPTRAP Exige qu'une frame de handler soit située au sommet de la pile. On rétablit alors l'ancienne valeur de [trapsp] et on supprime la frame. Cela correspond à la sortie normale d'un handler. Une petite subtilité est qu'il faut traiter les éventuels signaux en attente avant de sortir, sinon on risque de ne pas lever une exception au bon endroit. (Cela dit, dans la mesure où ces signaux auraient pu arriver plus tard, puisqu'ils sont asynchrones, je ne suis pas sûr qu'on violerait la sémantique en ne faisant pas cela.) RAISE Si les backtraces sont activées, construit une backtrace pour cette exception en parcourant la pile de son sommet jusqu'à la première trap frame et en y identifiant toutes les adresses de retour de fonctions. (Les tail calls sont donc invisibles!) Ce mécanisme est rusé, car cela signifie que les backtraces ne coûtent rien tant qu'aucune exception n'est lancée, d'une part, et d'autre part que lorsqu'une exception est lancée, on ne paye d'abord que pour remonter jusqu'au premier handler; on ne construit pas tout de suite un backtrace depuis le début du monde, ce qui serait (difficile et) inutile si l'exception est rattrapée. Ensuite: + Si la pile contient une trap frame (donnée par [trapsp]), alors on l'utilise pour restaurer [pc], [trapsp], [env] et [extra_args], et on dépile tout jusqu'à (et y compris) cette frame. + Sinon, l'exception est considérée comme non rattrapée, et cette instance de l'interprète termine en renvoyant l'exception comme résultat. Signaux. CHECK_SIGNALS Traite les signaux en attente. Implicitement exécuté à chaque appel de fonction. Appel de fonctions C. C_CALLN n Pousse [accu] sur la pile, dont les n premiers mots forment alors les n arguments de la primitive C. Sauvegarde [pc] et [sp] dans des variables globales, et [env] sur la pile. Appelle la primitive C en lui fournissant le segment de pile où sont stockés ses n paramètres, et place le résultat dans l'accumulateur. Restaure ensuite [sp] et [env], et dépile les n paramètres. La primitive C peut également lancer une exception à l'aide de [mlraise], auquel cas la valeur sauvegardée de [pc] participera à la construction du backtrace. Elle semble par ailleurs inutilisée. C_CALL1 à C_CALL5 Versions spécialisées de C_CALLN pour N de 1 à 5. Le protocole d'appel de la fonction C n'est pas le même: lorsque N est connu statiquement, on lui passe directement N arguments, tandis que dans C_CALLN, on passe un pointeur vers un tableau de N arguments (un segment de pile, en fait), ainsi que N lui-même. Constantes entières. CONSTINT n Place la valeur entière n dans l'accumulateur. Existe en version CONSTn pour n de 0 à 3. PUSHCONSTINT n Équivalent à la séquence PUSH; CONSTINT n. Existe en version PUSHCONSTn pour n de 0 à 3. Arithmétique entière. NEGINT Remplace l'accumulateur par sa négation entière. ADDINT SUBINT MULINT Dépile un entier et l'ajoute/le soustrait/le multiplie à l'accumulateur. DIVINT MODINT Dépile un entier et divise/module l'accumulateur par cet entier. Lance une exception si le diviseur est nul. ANDINT ORINT XORINT LSLINT LSRINT ASRINT Dépile un entier et le combine à l'accumulateur par l'opération logique appropriée. EQ NEQ LTINT LEINT GTINT GEINT ULTINT UGEINT Dépile un entier et le compare à l'accumulateur par l'opération logique appropriée. Le résultat est placé dans l'accumulateur. BEQ n k BNEQ n k BLTINT n k BLEINT n k BGTINT n k BGEINT n k BULTINT n k BUGEINT n k B k est équivalent à la séquence PUSH; CONSTINT n; ; BRANCHIF k. (Ce n'est pas forcément intuitif.) OFFSETINT n Incrémente l'accumulateur de n. OFFSETREF n Incrémente le premier champ de l'objet situé dans l'accumulateur de n. L'accumulateur reçoit la valeur [unit]. ISINT Place dans l'accumulateur un booléen indiquant si la valeur initiale de l'accumulateur est un entier ou un pointeur. Opérations orientées objet. GETMETHOD Suppose un objet situé dans le premier mot de la pile (il n'est pas dépilé), et un label de méthode situé dans l'accumulateur. L'accumulateur est remplacé par la clôture associée à cette méthode pour cet objet. Celle-ci est obtenue par trois indirections (la première extrait du premier champ de l'objet l'adresse de la table de dispatch, et les deux suivantes se font à travers la table). Debugging et contrôle de la machine. STOP Provoque l'arrêt normal de l'interprète. La valeur courante de l'accumulateur en constitue le résultat. EVENT Décrémente le compteur d'événements, et provoque un appel au debugger si celui-ci tombe à zéro. Exécute ensuite l'instruction masquée par EVENT. (Lorsque le debugger est utilisé, l'interprète maintient deux exemplaires du code, l'original dans [saved_code], et une copie potentiellement patchée par le debugger pour exécution.) BREAK Appelle le debugger, puis exécute l'instruction masquée par BREAK. Fichier intext.h: ----------------- Header commun aux fichiers extern.c et intern.c. Fichier ints.c: --------------- La librairie standard sur les entiers (affichage, opérations sur les entiers 32 et 64 bits). Fichiers io.c et io.h: ---------------------- Implémente le type [channel] de la librairie standard. Fichier lexing.c: ----------------- Interprète pour les lexers engendrés par ocamllex. Fichiers macintosh.c et macintosh.h: ------------------------------------ Implémentations (ou non-implémentations) de certaines routines sous MacOS. Fichier main.c: --------------- La fonction main, qui appelle caml_main. Fichiers major_gc.c et major_gc.h: ---------------------------------- GC. Fichiers md5.c et md5.h: ------------------------ Code domaine public pour calculer des digests md5. Fichiers memory.c et memory.h: ------------------------------ Macros et fonctions d'allocation. Fichier meta.c: --------------- Fonctions magiques pour le toplevel. Fichiers minor_gc.c et minor_gc.h: ---------------------------------- GC. Fichiers misc.c et misc.h: -------------------------- Diverses routines de rapport d'erreurs, ainsi que du code pour gérer des tables extensibles. Fichier mlvalues.h: ------------------- Un tas de macros permettant de manipuler des valeurs Caml depuis C. Fichier obj.c: -------------- Implémentation de certaines fonctions des modules [Obj] et [Lazy]. Fichier osdeps.h: ----------------- Déclare une série de fonctions OS dépendantes (parsing de chemins, librairies partagées, accès aux répertoires, accès au nom de l'exécutable courant, etc.) Fichier parsing.c: ------------------ Interprète pour les parsers engendrés par ocamlyacc. Fichier prims.h: ---------------- Déclare la table des primitives et la façon d'y accéder. Fichiers printexc.c et printexc.h: ---------------------------------- Affichage des exceptions non rattrapées, des backtraces, appel des fonctions enregistrées via [at_exit]. Fichier reverse.h: ------------------ Gestion de l'endianness. Fichiers roots.c et roots.h: ---------------------------- GC. Fichiers signals.c et signals.h: -------------------------------- Gère les signaux. Une liste de clôtures est située dans la variable globale [signal_handlers]. On extrait celle correspondant au numéro du signal, et on l'exécute (callback.c), ce qui déclenche un appel réentrant à l'interprète. Si l'exécution produit une exception, elle est propagée (manuellement). Contient également les fonctions [enter_blocking_section] et [leave_blocking_section]. Le premier exécute tous les signaux en attente, puis active le drapeau [async_signal_mode]. Le second efface ce même drapeau. Ce drapeau est utilisé par le gestionnaire de signaux [handle_signal]: si le drapeau est activé, un signal est exécuté dès sa réception, mais sinon, il est mis en attente. L'exécution d'un gestionnaire de signaux est toujours située dans une section bloquante, de sorte qu'un signal ne peut interrompre la gestion d'un signal précédent. Fichier stacks.c et stacks.h: ----------------------------- Code de gestion de la pile. Celle-ci est initialement allouée par un appel à malloc. La pile croît vers le bas. La partie inférieure de la pile (par défaut, les 256 positions inférieures) forment le `seuil' de la pile. Pour gagner du temps, la plupart des instructions de la machine virtuelle font croître la pile sans se prémunir contre les overflows; c'est seulement une fois de temps en temps (à chaque appel de fonction, en fait) qu'on teste si le pointeur de pile se trouve dans la zone seuil, et si oui, on demande une extension de la taille de la pile. Il ne faut donc pas engendrer de code contenant plus de 256 instructions PUSH au sein d'une même fonction... Lorsqu'une extension de la pile est demandée, on teste si celle-ci dépasserait alors une certaine valeur limite, et si oui, la requête est refusée, ce qui évite d'allouer gloutonnement toute la mémoire disponible sur la machine. Si la requête est acceptée, alors la taille de la pile est doublée: un nouveau bloc est allouée, et l'ancienne pile recopiée dedans. Tous les pointeurs vers la pile sont réécrits, ce qui inclut certains registres globaux ainsi que toute la chaîne des handlers d'exceptions. Fichiers startup.c et startup.h: -------------------------------- Alloue et initialise la table globale des 256 atomes (constructeurs de données non paramétrés). Initialise le GC et la pile. Analyse la ligne de commande, la variable $CAMLRUNPARAM. Détermine où trouver le bytecode: soit dans un fichier dont le nom est spécifié sur la ligne de commande, soit dans l'exécutable courant lui-même. Ouvre le fichier de bytecode et en extrait le trailer, dont la structure, décrite dans [exec.h], est la suivante: un entier non signé de 32 bits [num_sections], et un magic number dépendant de la release. Juste avant le trailer, vient la table des matières (TOC), constituée de 8 octets par section, à savoir un nom (de 4 caractères) et une longueur en octets (sur 32 bits). Le reste du fichier est constitué des sections indiquées dans la table, dans l'ordre où elles apparaissent dans la table. Charge le code, situé dans la section "CODE" du fichier. Charge le chemin des librairies partagées, situé dans la section "DLPT", ainsi que la liste des librairies partagées, située dans la section "DLLS". Charge la liste des primitives, située dans la section "PRIM". Utilise ces trois informations pour charger toutes les primitives requises. Charge les variables globales préinitialisées, situées dans la section "DATA". Celles-ci sont dans un format lisible par [input_value]. Pour terminer, lance l'exécution de l'interprète, et affiche le résultat si celui-ci est une exception non rattrapée. Fichier str.c: -------------- La librairie standard sur les strings. Fichier sys.c et sys.h: ----------------------- Implémentation du module [Sys] de la librairie standard. Fichier terminfo.c: ------------------- Implémente apparemment un module de librairie (quoique je ne vois pas où ces fonctions sont accessibles en Caml). Fichier unix.c: --------------- Implémente osdeps.h pour Unix. Fichiers weak.c et weak.h: -------------------------- GC. Fichier win32.c: --------------- Implémente osdeps.h pour Windows.