Suivez-nous sur X
|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : Conventions des bibliothèques partagées
(Article écrit par Max et extrait d'Amiga News Tech - août 1992)
|
|
Certes, le système multitâche de l'Amiga fait des merveilles. Mais pour que tout fonctionne au mieux dans le meilleur
des mondes, un certain nombre de restrictions et de conventions a du être arrêté.
Nous ne nous attarderons pas ici sur les restrictions, dont je suis d'ailleurs sûr que personne n'a rien à f...,
mais plutôt sur les conventions, qui définissent et régulent le déroulement interne des opérations.
Ainsi par exemple, il a été convenu par les auteurs du logiciel système, que chacune de ses "couches"
serait séparée des autres et fournirait au programmeur d'applications diverses un certains nombre de fonctionnalités
destinées à l'aider dans sa tâche. C'est normal, il en va ainsi sur tous les ordinateurs du monde, banlieue comprise.
Cependant, l'originalité de l'Amiga (qui fait aussi sa puissance et, à moindre titre, sa complexité) est de permettre
à plusieurs applications différentes de tourner "en même temps". Ainsi, il n'est pas rare de voir Deluxe
Paint dans un coin, Excellence! dans un autre et Professional Page par-dessus tout ça (c'est ma configuration
personnelle que je vous décris là), les trois disposant des mêmes ressources au même moment (écrans, fenêtres,
menus, accès disques...).
Il fallait donc trouver un moyen de permettre aux applications d'avoir accès aux ressources du système sans entrer
en conflit avec d'autres déjà chargées. Ce moyen existe, bien sûr : ce sont les bibliothèques partagées (de l'anglais
"shared library").
Quèsaco ?
Le principe est donc simple : la bibliothèque met à la disposition du programmeur ses fonctions et, lorsque deux
ou plusieurs programmes y accèdent en même temps, une seule copie de la bibliothèque est effectivement chargée en
mémoire (les programmes se partagent, le temps de l'appel de la fonction, les mêmes adresses d'exécution).
Par convention encore, chaque bibliothèque "standard" se charge d'un domaine particulier du système :
Exec pour le multitâche, DOS pour les accès disque, Graphics pour l'affichage et le dessin, Intuition
pour les fenêtres, menus et compagnie... Ces bibliothèques-là sont brûlées en ROM, mais le système
autorise également le chargement de bibliothèques supplémentaires depuis le disque (plus précisément,
depuis le répertoire LIBS: de la disquette Workbench). C'est justement cette possibilité qui nous intéresse,
car elle permet à l'utilisateur de créer ses propres bibliothèques de fonctions et de les ajouter au
système. Cela demande cependant un minimum d'efforts de programmation.
La structure Library
Dans l'Amiga, tout est géré au moyen de structures de données, et les bibliothèques n'échappent pas
à la règle.
Cette structure commence par une autre structure, de type Node, qui permet de relier entre elles
toutes les bibliothèques chargées. Elle contient également et notamment le numéro de version de
la bibliothèque, ainsi qu'un compteur d'ouverture : lorsqu'une application veut utiliser les
fonctions d'une bibliothèque donnée, elle doit d'abord "l'ouvrir", c'est-à-dire prévenir le système
qu'elle a besoin de cette bibliothèque et qu'il faudra, le cas échéant, la charger en mémoire (une
seule bibliothèque fait exception à cette règle : c'est l'exec.library, qui sert justement à en ouvrir
d'autres !). Le compteur d'ouverture est alors incrémenté. Lorsque l'application n'a plus besoin
de la bibliothèque, elle la "ferme" : le compteur d'ouverture est décrémenté et, s'il arrive à 0
(c'est-à-dire si plus aucune application ne l'utilise), la bibliothèque est déchargée de la mémoire.
Les champs "RegSise" et "PosSize" indiquent la taille de la mémoire qui a été allouée par le
système lors de la création de la bibliothèque. Ils seront utilisés lors de son déchargement
pour libérer cette mémoire, tâche qui incombe à la bibliothèque elle-même, et non au système !
L'ouverture d'une bibliothèque retourne ce qu'on appelle son adresse de base : c'est un pointeur
sur la structure Library de la bibliothèque, qui est utilisé pour appeler ses fonctions.
L'appel se fait via la registre A6, en sautant avec l'instruction JSR à un décalage situé en dessous
de la base, dans la zone dite "négative". Les paramètres des fonctions sont passés dans les autres
registres, généralement D0, D1, A0 et A1 et la valeur de retour est placée dans D0. La bibliothèque
pourra accéder à des données internes située au-dessus de la base, dans la zone dite "positive",
également via le registre A6, en utilisant le mode d'adressage indirect avec déplacement, par
exemple MOVE.B lib_Flags(A6),d0.
Cette petite description s'arrêtera là. Je vous invite à vous reporter aux articles sur LibTool
de Pascal Amiable pour en savoir un peu plus sur ce sujet (LibTool, le créateur de bibliothèque
et Utilisation de GadToolsBox).
Le RomTag
Pour que le système reconnaisse une bibliothèque comme telle, elle doit présenter en son tout
début un identificateur particulier (il ne suffit heureusement pas de renommer un programme quelconque
en machin.library pour qu'il passe pour une bibliothèque !). Cet identificateur s'appelle un RomTag
résident et est également représenté par une structure :
A la base, le RomTag sert à la routine de réinitialisation, à rechercher dans la ROM tous les
modules composant le système. La recherche est entreprise de la manière suivante :
- Rechercher l'identificateur rt_MatchWord ($4AFC).
- Vérifier que le mot long suivant (rt_MatchTag) pointe bien sur l'identificateur lui-même.
Si c'est le cas, on est sûr de se trouver en présence d'un RomTag, sinon, poursuivre la recherche.
- Initialiser le module trouvé en fonction des données codées dans les champs rt_Flags,
rt_Type, rt_Pri et rt_Init.
- Rechercher le prochain module en ROM à partir de l'adresse pointée par rt_EndSkip (ce
qui évite de perdre du temps à le rechercher à l'intérieur de celui qu'on vient juste de trouver).
Pour les bibliothèques résidant sur disque, le RomTag sert seulement à identifier la bibliothèque
comme telle, et à fournir au système les informations nécessaires à son initialisation.
Toute bibliothèque doit donc contenir une telle structure en son début.
Écrire une bibliothèque
Dans ce qui suit, nous construirons une bibliothèque disposant de quatre fonctions :
- resultat = Addition(op1, op2)
- resultat = Soustraction(op1, op2)
- resultat = Multiplication(op1, op2)
- resultat = Division(op1, op2)
Ces quatre fonctions travaillent de la même manière : elles prennent en paramètre deux opérandes et
retournent le résultat de l'opération dont elles portent le nom.
Nous étudierons le listing source de cette bibliothèque (dénommée operations.library)
au fur et à mesure de notre progression.
;
; operations.library
; Démonstration de l'écriture d'une bibliothèque
;
; Copyright © 1992 Max pour ANT
;
; Inclus les fichiers nécessaires à l'assemblage
INCLUDE "exec/types.i"
INCLUDE "exec/libraries.i"
INCLUDE "exec/resident.i"
INCLUDE "exec/initializers.i"
INCLUDE "exec/memory.i"
INCLUDE "exec/exec_lib.i"
INCLUDE "libraries/operations.i"
|
Les deux instructions suivantes ne sont là qu'au cas où quelqu'un essaierait d'exécuter la bibliothèque
directement depuis le CLI. On pourrait mettre ici une routine plus complète, qui afficherait par
exemple un message d'erreur, ou ce qu'on veut.
Suit le RomTag qu'Exec recherche dans la bibliothèque pour l'identifier comme telle. Le champs
rt_EndSkip pointe sur la fin du code de la bibliothèque, même si cela n'est pas réellement nécessaire
puisqu'il n'est utilisé utilisé que pour les modules en ROM, lors de la réinitialisation.
Le champs rt_Flags est initialisé à RTF_AUTOINIT. Cela signifie qu'Exec appellera la fonction InitStruct()
pour initialiser notre bibliothèque. rt_Init doit pointer sur un tableau composé d'un mot
long et de trois pointeurs, qui sera transmis à cette fonction. Si RTF_AUTOINIT n'était pas spécifié,
rt_Init devrait pointer directement sur une routine d'initialisation propre.
RomTag dc.w RTC_MATCHWORD ; Identificateur
dc.l RomTag ; Pointeur sur l'identificateur
dc.l EndCode ; rt_EndSkip
dc.b RTF_AUTOINIT ; rt_Flags
dc.b OP_VERSION ; rt_Version
dc.b NT_LIBRARY ; rt_Type
dc.b 0 ; rt_Pri
dc.l OpName ; rt_Name
dc.l OpIDStr ; rt_IDString
dc.l Init ; rt_Init
|
Voici la table qui sera passée en paramètre à initstruct. Le premier mot long contient la taille de
la structure à initialiser. Le second mot long pointe sur le tableau des fonctions. Le troisième
pointe sur le tableau des données. Le quatrième, enfin, pointe sur notre routine d'initialisation propre.
Init dc.l ob_SIZEOF ; Taille de la structure
dc.l FuncTable ; Tableau des fonctions
dc.l DataTable ; Tableau des données
dc.l InitRoutine ; Routine d'initialisation
|
Le tableau "FuncTable" n'est ni plus ni moins qu'une table de pointeurs sur toutes les fonctions de
la bibliothèque. InitStruct 0 l'utilise pour fabriquer les décalages de sauts (LVOs) de la bibliothèque.
Les quatre premières fonctions sont obligatoires et communes à toutes les bibliothèques. Les autres
sont propres à la bibliothèque. La fin du tableau est marquée par un pointeur de valeur -1.
FuncTable:
dc.l Open
dc.l Close
dc.l Expunge
dc.l Reserved
dc.l Addition
dc.l Soustraction
dc.l Multiplication
dc.l Division
dc.l -1
|
Le tableau "DataTable" contient des octets de commande qui indiquent à initstruct 0 quelles opérations
effectuer. Sa structure est assez particulière et indéchiffrable, mais on dispose heureusement de macros
(dans exec/initializers.i) pour faciliter sa définition.
Cette table demande à initstruct de placer des valeurs particulières (octet, mot ou mot long) à des décalages
données dans la structure. Par exemple, "INITBYTE LN_TYPE,NT_LIBRARY" commande de mettre la valeur "NT_LIBRARY"
dans le champs "LN_TYPE".
La fin du tableau est marquée par 0. Dans notre cas, c'est la structure Library qu'il faut initialiser.
DataTable:
INITBYTE LN_TYPE,NT_LIBRARY ; On est une bibliothèque
INITLONG LN_NAME,OpName
INITBYTE LIB_FLAGS,LIBF_SUMUSED!LIBF_CHANGED
INITWORD LIB_VERSION,OP_VERSION
INITWORD LIB_REVISION,OP_REVISION
INITLONG LIB_IDSTRING,OpIDStr
dc.l 0
|
Voici enfin la première routine appelée lors de l'initialisation de la bibliothèque. Elle doit ouvrir toutes
les ressources utilisées par la bibliothèque (il peut très bien ne pas y en avoir, mais pour l'exemple, nous
ouvrons la dos.library et la gfx.library, même si elles ne sont pas utilisées). Cette fonction est appelée
avec l'adresse de base de notre propre bibliothèque dans D0, tandis qu'A0 contient la liste de nos segments
(n'oubliez pas qu'on réside normalement sur disque ; Exec nous a chargé en mémoire en appelant Loadseg()).
InitRoutine() doit retourner NULL si une erreur survient, auquel cas la bibliothèque sera immédiatement
déchargée de la mémoire.
InitRoutine:
move.l a5,-(sp) ; Sauve les registres utilisés
movea.l d0,a5 ; a5 = OpBase
move.l d0,ob_SegList(a5) ; Sauve notre SegmentList
lea dosname(pc),a1 ; Ouvre la dos.library
moveq #0,d0
jsr _LVOOpenLibrary(a6) ; (A6 = ExecBase)
move.l d0,ob_DOSBase(a5)
beq .NoDos ; Erreur !!
lea gfxname(pc),a1 ; Ouvre la graphics.library
moveq #0,d0
jsr _LVOOpenLibrary(a6)
move.l d0,ob_GfxBase(a5)
beq .NoGfx ; Erreur !!
move.l a5,d0 ; Retourne OpBase dans D0
bra.s .Done
.NoGfx movea.l ob_DOSBase(a5),a1 ; Referme la dos.library...
jsr _LVOCloseLibrary(a6)
.NoDos moveq #0,d0 ; et retourne 0 dans D0
.Done movea.l (sp)+,a5
rts
|
Les quatre premières fonctions d'une bibliothèque sont standard et obligatoires. Ce sont Open(), Close(),
Expunge() et Reserved().
Open() est appelée à chaque fois que la bibliothèque est ouverte. Elle incrémente le compteur d'ouverture et
efface le drapeau de demande de purge (voir plus loin). Open() est appelée avec l'adresse de base de
la bibliothèque dans A6.
Open addq.w #1,LIB_OPENCNT(a6) ; Incrémente le compteur
bclr #LIBB_DELEXP,LIB_FLAGS(a6) ; Efface le flag de purge
move.l a6,d0 ; Retourne la base dans D0
rts
|
Close() est appelée quand la bibliothèque est refermée. Elle décrémente le compteur d'ouverture et appelle
au besoin Expunge() pour décharger la bibliothèque de la mémoire. Elle est appelée avec le pointeur
de base dans A6 et retourne soit la SegList initiale, soit 0.
Close moveq #0,d0 ; Prépare la valeur de retour
subq.w #1,LIB_OPENCNT(a6) ; Décrémente le compteur
bne.s .ret ; Il reste des utilisateurs !
btst #LIBB_DELEXP,LIB_FLAGS(a6) ; Demande de déchargement ?
bne.s Expunge ; Si oui, on purge
.ret rts
|
Expunge() est le pendant d'InitRoutine() : elle est appelée quand le système a besoin de mémoire
et qu'il veut décharger notre bibliothèque.
Cette fonction doit retirer la bibliothèque de la liste gérée par Exec, libérer la mémoire allouée
par InitStruct(), libérer les ressources allouées pendant l'initialisation, et enfin retourner la
liste des segments dans D0, afin qu'Exec puisse appeler UnLoadSeg(). Si elle retourne NULL, la
bibliothèque n'est pas déchargée.
Expunge movem.l d2/a5/a6,-(sp) ; Sauve les registres utilisés
move.l a6,a5 ; OpBase dans a5
movea.l $4.w,a6 ; ExecBase dans a6
tst.w LIB_OPENCNT(a6) ; Bibliothèque encore utilisée ?
beq.s .Purge ; Non, on purge
bset #LIBB_DELEXP,LIB_FLAGS(a5) ; Place le flag de purge
moveq #0,d0
bra.s .Ret
.Purge move.l ob_SegList(a5),d2 ; Prépare la valeur de retour
movea.l a5,a1 ; Retire la bibliothèque de la
jsr _LVORemove(a6) ; liste gérée par Exec
moveq #0,d0 ; Libère la mémoire allouée par
movea.l a5,a1 ; InitStruct()
move.w LIB_NEGSIZE(a5),d0
suba.l d0,a1
add.w LIB_POSSIZE(a5),d0
jsr _LVOFreeMem(a6)
move.l d2,d0 ; Retourne la SegList dans D0
.Ret movem.l (sp)+,d2/a5/a6
rts
|
Enfin, Reserved() porte bien son nom : elle est réservée pour des extensions futures. Pour le moment,
elle doit juste retourner 0.
Reste le secteur de données. On peut le placer où l'on veut, mais je trouve plus clair de le mettre
entre les fonctions système et les fonctions propres.
OpName OPERATIONSNAME
OpIDStr dc.b "operations.library v1.0 (23 Jun 1992)",13,10,0
dosname dc.b "dos.library",0
gfxname dc.b "graphics.library",0
even
|
A partir d'ici, on trouve les fonctions propres à la bibliothèque.
Addition:
add.l d1,d0
rts
Soustraction:
sub.l d1,d0
rts
Multiplication:
muls d1,d0
rts
Division:
divs d1,d0
rts
|
Dernière ligne du source, celle qui définit le label EndCode utilisé dans le RomTag.
Voilà, il ne reste plus qu'à assembler tout ça en un programme exécutable baptisé libs:operations.library.
Fichiers include
Pour rendre votre bibliothèque utilisable par d'autres programmeurs que vous, il faut leur fournir
les fichiers include nécessaires. On en trouve généralement deux par langage, à savoir l'include
principal, qui définit toutes les constantes et structures propres à la bibliothèque, et l'include
secondaire qui définit sa structure de base. Je ne vous livre ici que celui destiné à l'assembleur
et laisserai à votre sagacité le soin d'écrire son équivalent pour le C, le Pascal, ou
tout autre langage de votre choix.
IFND LIBRARIES_OPERATIONS_I
LIBRARIES_OPERATIONS_I SET 1
IFND EXEC_TYPES_I
INCLUDE "exec/types.i"
ENDC
IFND EXEC_LIBRARIES_I
INCLUDE "exec/libraries.i"
ENDC
;
; operations.library
; Démonstration de l'écriture d'une bibliothèque
;
; Copyright © 1992 Max pour ANT
;
; Fichier include assembleur ('include:libraries/operations.i')
; Version de la bibliothèque
OP_VERSION EQU 1
OP_REVISION EQU 0
; Structure de base
STRUCTURE OpBase,LIB_SIZE ; Structure Library
ULONG ob_SegList ; Liste de nos segments DOS
APTR ob_DOSBase ; Pointeur sur la bibliothèque DOS
APTR ob_GfxBase ; Pointeur sur la bibliothèque Gfx
LABEL ob_SIZEOF
; Nom de la bibliothèque
OPERATIONSNAME MACRO
dc.b "operations.library",0
ds.w 0
ENDM
; décalage de sauts (LVOs)
LIBINIT
LIBDEF _LVOAddition
LIBDEF _LVOSoustraction
LIBDEF _LVOMultiplication
LIBDEF _LVODivision
ENDC ; LIBRARIES_OPERATIONS_I
|
|