Pour cette série en trois parties, nous avons implémenté un GPIO « pédale au métal », clignotant d’une LED, dans le contexte d’un module de noyau Linux pour la carte de développement NVIDIA Jetson Nano (basé sur le noyau v4.9.294, arm64) dans mon langage de programmation préféré… Ada !
Vous pouvez retrouver l’intégralité du projet publié sur https://github.com/ohenley/adacore_jetson. Il est connu pour se construire et fonctionner correctement. Toutes les instructions pour être opérationnel en 5 minutes sont incluses dans le fichier README.md frontal qui l’accompagne. N’hésitez pas à remplir un ticket GitHub si vous rencontrez un problème.
Avis de non-responsabilité : ce texte est destiné à plaire aux codeurs Ada et non-Ada. Par conséquent, j’essaie de trouver un équilibre entre la simplicité de l’histoire du code, la traçabilité didactique et la densité des fonctionnalités. Comme je l’ai dit à un collègue, c’est le texte que j’aurais aimé traverser avant de commencer cette expérience.
Reliure 101
L’épaisseur de la reliure
Notre frontière de code avec les méthodes C du noyau Linux réside dans kernel.ads. Pour une opportunité optionnelle « d’adaptation », noyau.adb existe avant de pénétrer dans la liaison C concrète. Prendre imprimerk (printf équivalent dans l’espace noyau) par exemple. En C, vous appelleriez printk(« bonjour/n »). Les chaînes Ada ne se terminent pas par null, elles sont un tableau de caractères. Pour vous assurer que la chaîne Ada passée reste valide du côté C, vous exposez les signatures de spécification .les publicités qui ont du sens lors de la programmation d’un point de vue Ada et « s’adaptent » dans la mise en œuvre du corps .adb avant d’appeler directement dans la liaison. À proprement parler, notre Ada exposée Imprimerk serait qualifié de reliure « épaisse » même si la couche d’adaptation est minime. Cela s’oppose à une liaison « légère » qui est en fait un mappage un à un sur la signature C telle qu’implémentée par Imprimerk_C.
-- kernel.ads
procedure Printk (S : String); -- only this is visible for clients of kernel
-- kernel.adb
procedure Printk_C (S : String) with -- considered a thin binding
Import => true,
Convention => C,
External_Name => "printk";
procedure Printk (S : String) is -- considered a thick binding
begin
Printk_C (S & Ascii.Lf & Ascii.Nul); -- because we ‘mangle’ for Ada comfort
end;
La fonction wrapper
Liaison à un C enveloppé macro ou statique en ligne est souvent pratique, vous fait potentiellement hériter des correctifs, des mises à niveau se produisant à l’intérieur/sous l’implémentation de la macro et sont, selon le contexte, potentiellement plus portables. create_singlethread_workqueue utilisé dans printk_wq.c comme trouvé dans la partie 1 en est un exemple parfait. Notre chauffeur a une maison C à principal c. Vous créez une fonction d’encapsulation C appelant la macro.
/* main.c */
extern struct workqueue_struct * wrap_create_singlethread_wq (const char* name)
{
return create_singlethread_workqueue(name); /* calling the macro */
}
Vous vous liez ensuite à ce wrapper du côté Ada et l’utilisez. Terminé.
-- kernel.ads
function Create_Singlethread_Wq (Name : String) return Workqueue_Struct_Access with
Import => True,
Convention => C,
External_Name => "wrap_create_singlethread_wq";
-- flash_led.adb
...
Wq := K.Create_Singlethread_Wq ("flash_led_work");
La reconstruction
Parfois, une macro appelée du côté C crée des choses, en place, dont vous finissez par avoir besoin du côté Ada. Vous pouvez probablement toujours vous lier à cette ressource, mais je trouve que cela entrave souvent l’histoire du code. Prendre DECLARE_DELAYED_WORK(dw, delay_work_cb) par exemple. D’un point de vue extérieur, cela crée implicitement structure delay_work dw en place.
/* https://elixir.bootlin.com/linux/v4.9.294/source/include/linux/workqueue.h */
#define DECLARE_DELAYED_WORK(n, f) \
struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
En utilisant cette macro, le seul moyen que j’ai trouvé pour mettre la main sur dw d’Ada sans crash (en retournant dw d’un emballage n’a jamais fonctionné) était d’appeler globalement DECLARE_DELAYED_WORK(n, f) dans main.c puis lier uniquement à dw. Devoir maintenir cela à partir de C, le faire apparaître comme par magie dans Ada me semblait être un « câblage de planche à pain ». Dans le référentiel de code, vous constaterez que nous avons entièrement reconstruit cette macro sous la procédure du même nom Declare_Delayed_Work.
Le raccourci du pointeur
La plupart des liaisons Ada vers C publiées implémentent la parité de définition complète. Il s’agit d’une situation idéale dans la plupart des cas, mais elle est également complexe, peut générer de nombreux fichiers tiers, parfois enfouis profondément, des définitions désynchronisées, etc. Que pouvez-vous faire lorsque des liaisons complètes sont manquantes ou que vous souhaitez simplement déplacer maigre et rapide? Si vous faites un prototype, vous voulez des dépendances minimales, la partie contraignante est périphérique, par exemple. vous n’aurez peut-être besoin que d’une API de fenêtre native rapide. Tu obtiens le point.
Selon le contexte, vous n’avez pas toujours besoin des définitions de type complètes pour démarrer. Chaque fois que vous traitez strictement avec un pointeur de poignée (ne possédant pas la mémoire), vous pouvez prendre un raccourci. attachons-nous à gpio_get_value pour illustrer. Encore une fois, je suis et mets en page toutes les signatures C trouvées dans les sources du noyau menant à des éléments concrets, où nous pouvons nous lier.
/* https://elixir.bootlin.com/linux/v4.9.294/source(-) */
/* (+)include/linux/gpio.h */
static inline int gpio_get_value(unsigned int gpio)
{
return __gpio_get_value(gpio);
}
/* (+)include/asm-generic/gpio.h */
static inline int __gpio_get_value(unsigned gpio)
{
return gpiod_get_raw_value(gpio_to_desc(gpio));
}
/* (+)include/linux/gpio/consumer.h */
struct gpio_desc *gpio_to_desc(unsigned gpio); /* bindable */
int gpiod_get_raw_value(const struct gpio_desc *desc); /* bindable */
/* (+)drivers/gpio/gpiolib.h */
struct gpio_desc {
struct gpio_device *gdev;
unsigned long flags;
...
const char *name;
};
En examinant les définitions C, nous constatons que gpiod_get_raw_value et gpio_to_desc sont nos fonctions disponibles pour la liaison. Nous notons gpio_to_desc utilise un pointeur transitoire de type gpio_desc *. Parce que nous ne touchons pas ou ne possédons pas un plein gpio_desc Par exemple, nous pouvons heureusement ignorer la définition complète (et toutes les pistes dépendantes, par exemple. gpio_device).
En déclarant le type Gpio_Desc_Acc est nouveau System.Address ; nous créons un équivalent à gpio_desc *. Après tout, un pointeur C est une adresse système nommée. Nous avons maintenant tout ce dont nous avons besoin pour construire notre version Ada de gpio_get_value.
-- kernel.ads
package Ic renames Interfaces.C;
function Gpio_Get_Value (Gpio : Ic.Unsigned) return Ic.Int; -- only this is visible for clients of kernel
-- kernel.adb
type Gpio_Desc_Acc is new System.Address; -- shortcut
function Gpio_To_Desc_C (Gpio : Ic.Unsigned) return Gpio_Desc_Acc with
Import => True,
Convention => C,
External_Name => "gpio_to_desc";
function Gpiod_Get_Raw_Value_C (Desc : Gpio_Desc_Acc) return Ic.Int with
Import => True,
Convention => C,
External_Name => "gpiod_get_raw_value";
function Gpio_Get_Value (Gpio : Ic.Unsigned) return Ic.Int is
Desc : Gpio_Desc_Acc := Gpio_To_Desc_C (Gpio);
begin
return Gpiod_Get_Raw_Value_C (Desc);
end;
Les fixations Raw, « 100% Ada »
Dans la plupart des contextes de production, nous ne pouvons pas recommander de reconstruire les appels d’API de noyau non liés dans Ada. L’emballage de la macro C ou de l’inline statique est définitivement plus facile, plus sûr, portable et maintenable. Ce qui suit va à fond dans Ada dans le but d’illustrer quelques écrous et boulons intéressants et de montrer que c’est toujours possible.
Drapeaux, première prise
Compte tenu de la volonté, vous pouvez toujours reconstruire la macro ciblée ou l’inline statique dans Ada. Revenons à create_singlethread_workqueue. Si vous prenez le temps de développer sa macro en utilisant GCC, c’est ce que vous obtenez.
$ gcc -E [~ 80_switches_for_valid_ko] printk_wq.c
...
wq = __alloc_workqueue_key(("%s"),
(WQ_UNBOUND |
__WQ_ORDERED |
__WQ_ORDERED_EXPLICIT |
(__WQ_LEGACY | WQ_MEM_RECLAIM)),
(1),
((void *)0),
((void *)0),
"my_wq");
Tous les arguments sont simples à mapper, à l’exception des drapeaux OU. Cherchons dans les sources du noyau pour ces drapeaux.
/* https://elixir.bootlin.com/linux/v4.9.294/source/include/linux/workqueue.h */
enum {
WQ_UNBOUND = 1 << 1,
...
WQ_POWER_EFFICIENT = 1 << 7,
__WQ_DRAINING = 1 << 16,
...
__WQ_ORDERED_EXPLICIT = 1 << 19,
WQ_MAX_ACTIVE = 512,
WQ_MAX_UNBOUND_PER_CPU = 4,
WQ_DFL_ACTIVE = WQ_MAX_ACTIVE / 2,
};
Voici nos décisions de conception pour la reconstruction
- WQ_MAX_ACTIVE, WQ_MAX_UNBOUND_PER_CPU, WQ_DFL_ACTIVE sont des constantes, pas des drapeaux, nous les gardons donc à l’écart.
- L’énumération est anonyme, donnons-lui un type nommé approprié.
- __WQ pattern est probablement une convention, mais en même temps, l’utilisation est mixte, par exemple. WQ_UNBOUND | __WQ_ORDEREDalors aplatissons tout ça.
Parce que nous n’utilisons pas ces drapeaux ailleurs dans notre base de code, l’occasion est parfaite pour montrer qu’en Ada nous pouvons garder toute cette modélisation locale à notre fonction unique en l’utilisant.
-- kernel.ads
package Ic renames Interfaces.C;
type Wq_Struct_Access is new System.Address; -- shortcut
type Lock_Class_Key_Access is new System.Address; -- shortcut
Null_Lock : Lock_Class_Key_Access :=
Lock_Class_Key_Access (System.Null_Address); -- typed ((void *)0) equiv.
-- kernel.adb
type Bool is (NO, YES) with Size => 1; -- enum holding on 1 bit
for Bool use (NO => 0, YES => 1); -- "represented" by 0, 1 too
function Alloc_Workqueue_Key_C ...
External_Name => "__alloc_workqueue_key"; -- thin binding
function Create_Singlethread_Wq (Name : String) return Wq_Struct_Access is
type Workqueue_Flags is record
...
WQ_POWER_EFFICIENT : Bool;
WQ_DRAINING : Bool;
...
end record with Size => Ic.Unsigned'Size;
for Workqueue_Flags use record
...
WQ_POWER_EFFICIENT at 0 range 7 .. 7;
WQ_DRAINING at 0 range 16 .. 16;
...
end record;
Flags : Workqueue_Flags := (WQ_UNBOUND => YES,
WQ_ORDERED => YES,
WQ_ORDERED_EXPLICIT => YES,
WQ_LEGACY => YES,
WQ_MEM_RECLAIM => YES,
Others => NO);
Wq_Flags : Unsigned with Address => Flags'Address;
begin
return Alloc_Workqueue_Key_C ("%s", Wq_Flags, 1, Null_Lock, "", Name);
end;
- En C, chaque indicateur est implicitement codé comme un littéral entier, bit échangé par une quantité. Parce que __alloc_workqueue_key la signature attend des drapeaux encodés en tant que entier non signé Il devrait être raisonnable d’utiliser Ic.Unsigned’Sizepour tenir un Workqueue_Flags.
- Nous construisons la représentation de Workqueue_Flags type similaire à ce que nous avons appris dans la partie 2 pour modéliser les registres. Par rapport à la version C, nous avons maintenant NON => 0, OUI => 1 sémantique et pas besoin d’opérations au niveau du bit.
- Rappelez-vous, dans Ada, nous roulons avec des types définis par l’utilisateur forts pour les biens les plus importants. Donc quelque chose comme Workqueue_Flags ne correspond pas à l’attendu Drapeaux : Ic.Unsigned paramètre de notre __alloc_workqueue_key reliure fine. Que devrions nous faire? Vous créez une variable Wq_Flags : Ic.Unsigned et superposez-y l’adresse de Indicateurs : Workqueue_Flags que vous pouvez maintenant transmettre à __alloc_workqueue_key.
Wq_Flags : Ic.Unsigned with Address => Flags'Address; -- voila!
Ioremap et iowrite32
Le travail de base du version raw_io se passe dans Set_Gpio. En utilisant Ioremapnous récupérons l’emplacement de mémoire IO mappé du noyau pour le GPIO_OUT enregistrer l’adresse physique. Nous écrivons ensuite le contenu de notre Gpio_Control à cet emplacement de mémoire IO via Io_Write_32.
-- kernel.ads
type Iomem_Access is new System.Address;
-- led.adb
package K renames Kernel;
package C renames Controllers;
procedure Set_Gpio (Pin : C.Pin; S : Led.State) is
function Bit (S : Led.State) return C.Bit renames Led.State'Enum_Rep;
Base_Addr : K.Iomem_Access;
Control : C.Gpio_Control := (Bits => (others => 0),
Locks => (others => 0));
Control_C : K.U32 with Address => Control'Address;
begin
...
Control.Bits (Pin.Reg_Bit) := Bit (S); -- set the GPIO flags
...
Base_Addr := Ioremap (C.Get_Register_Phys_Address (Pin.Port, C.GPIO_OUT),
Control_C'Size); -- get kernel mapped register addr.
K.Io_Write_32 (Control_C, Base_Addr); -- write our GPIO flags to this addr.
...
end;
Prenons les chemins difficiles de la reconstruction complète pour illustrer des choses intéressantes. Nous implémentons d’abord ioremap. Du côté C, on trouve
/* https://elixir.bootlin.com/linux/v4.9.294/source(-) */
/* (+)arch/arm64/include/asm/io.h */
#define ioremap(addr, size) \
__ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE))
extern void __iomem *__ioremap(phys_addr_t phys_addr, size_t size, pgprot_t prot);
Drapeaux, deuxième prise
Ici, nous sommes à la fois chanceux et malchanceux. __ioremap est suspendu tandis que __pgprot(PROT_DEVICE_nGnRE) s’avère être un terrier de lapin. Je saute l’expansion intermédiaire en rapportant le résultat final
$ gcc -E [~ 80_switches_for_valid_ko] test_using_ioremap.c
…
void* membase = __ioremap(
(phys_addr + offset),
(4),
((pgprot_t) {
(((((((pteval_t)(3)) << 0) |
(((pteval_t)(1)) << 10) |
(((pteval_t)(3)) << 8)) |
(arm64_kernel_unmapped_at_el0() ? (((pteval_t)(1)) << 11) : 0)) |
(((pteval_t)(1)) << 53) |
(((pteval_t)(1)) << 54) |
(((pteval_t)(1)) << 55) |
((((pteval_t)(1)) << 51)) |
(((pteval_t)((1))) << 2)))
}))
Recherche de définitions dans les sources du noyau : (échantillonnage significatif uniquement)
/* https://elixir.bootlin.com/linux/v4.9.294/source(-) */
/* (+)arch/arm64/include/asm/pgtable-hwdef.h */
#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
...
#define PTE_NG (_AT(pteval_t, 1) << 11)
...
#define PTE_ATTRINDX
/* (+)arch/arm64/include/asm/mmu.h */
static inline bool arm64_kernel_unmapped_at_el0(void)
{
return IS_ENABLED(CONFIG_UNMAP_KERNEL_AT_EL0) &&
cpus_have_const_cap(ARM64_UNMAP_KERNEL_AT_EL0);
}
/* (+)arch/arm64/include/asm/pgtable-prot.h */
#define PTE_DIRTY (_AT(pteval_t, 1) << 55)
/* (+)arch/arm64/include/asm/memory.h */
#define MT_DEVICE_nGnRE 1
La macro motif _AT(pteval_t, x) peut être effacé tout de suite. IIUC, il sert à gérer les appels à la fois depuis l’assembly et le C. Lorsque vous êtes concerné par le cas C, comme nous le faisons, cela se résume à Xpar exemple. ((pteval_t)(1)) devient 1 .
arm64_kernel_unmapped_at_el0 est en partie ‘dépendant de la configuration du noyau’, par défaut à ‘oui’, alors simplifions notre travail et intégrons-le, PTE_NG quel est le choix ? (((pteval_t)(1)) pour tous les cas.
(((pteval_t)((1))) se révèle être PTE_ATTRINDX
type Pgprot_T is mod 2**64; -- type will hold on 64 bits
type Memory_T is range 0 .. 5;
MT_DEVICE_NGnRnE : constant Memory_T := 0;
MT_DEVICE_NGnRE : constant Memory_T := 1;
...
MT_NORMAL_WT : constant Memory_T := 5;
function PTE_ATTRINDX (Mt : Memory_T) return Pgprot_T is
(Pgprot_T(Mt * 2#1#e+2)); -- base # based_integer # exponent
Ici, je veux montrer une autre façon de reproduire le comportement C, cette fois en utilisant des opérations au niveau du bit. Quelque chose comme PTE_TYPE_MASK valeur ((pteval_t)(3)) ne peut pas être abordé comme nous l’avons fait avant. 3 prend deux bits et est en quelque sorte un nombre magique. Ce que nous pouvons faire, c’est améliorer la représentation. Nous faisons des masques de bits, alors pourquoi ne pas exprimer directement en utilisant des nombres binaires. Cela a même un sens graphiquement.
PTE_VALID : Pgprot_T := 2#1#e+0;
...
PTE_TYPE_MASK : Pgprot_T := 2#1#e+0 + 2#1#e+1; -- our famous 3
...
PTE_HYP_XN : Pgprot_T := 2#1#e+54;
-- kernel.ads
type Phys_Addr_T is new System.Address;
type Iomem_Access is new System.Address;
-- kernel.adb
function Ioremap (Phys_Addr : Phys_Addr_T;
Size : Ic.Size_T) return Iomem_Access is
...
Pgprot : Pgprot_T := (PTE_TYPE_MASK or
PTE_AF or
PTE_SHARED or
PTE_NG or
PTE_PXN or
PTE_UXN or
PTE_DIRTY or
PTE_DBM or
PTE_ATTRINDX (MT_DEVICE_NGnRE));
begin
return Ioremap_C (Phys_Addr, Size, Pgprot);
end;
Alors qu’est-ce qui est intéressant ici ?
- Ada est flexible. L’original Pgprot_T l’arrangement des valeurs ne permettait pas le mappage des enregistrements comme nous le faisions précédemment pour tapez Workqueue_Flags. Nous nous sommes adaptés en reproduisant l’implémentation C, OU ALORS‘ing toutes les valeurs pour créer un masque final.
- Tout a été rangé par une frappe forte. Nous sommes maintenant coincés avec des trucs disciplinés.
- La représentation est explicite, exprimée dans la base voulue.
- Une fois de plus, cette machinerie de frappe vit à l’échelle la plus restrictive, à l’intérieur du Ioremap une fonction. Étant donné que la « portée » d’Ada a peu de règles spéciales, la refactorisation/hors portée se résume généralement à un simple jeu d’échange de blocs.
Ensemble émetteur
Maintenant, nous donnons un coup d’oeil à ioread32 et iowrite32. Il s’avère qu’il s’agit, encore une fois, d’une cascade de macros et de macros statiques finissant par émettre directement des directives d’assemblage GCC (détaillant uniquement iowrite32).
/* https://elixir.bootlin.com/linux/v4.9.294/source(-) */
/* (+)include/asm-generic/io.h */
static inline void iowrite32(u32 value, volatile void __iomem *addr)
{
writel(value, addr);
}
/* (+)include/asm/io.h */
#define writel(v,c) ({ __iowmb(); writel_relaxed((v),(c)); })
#define __iowmb() wmb()
/* (+)include/asm/barrier.h */
#define wmb() dsb(st)
#define dsb(opt) asm volatile("dsb " #opt : : : "memory")
/* (+)arch/arm64/include/asm/io.h */
#define writel_relaxed(v,c) \
((void)__raw_writel((__force u32)cpu_to_le32(v),(c)))
static inline void __raw_writel(u32 val, volatile void __iomem *addr)
{
asm volatile("str %w0, [%1]" : : "rZ" (val), "r" (addr));
}
Dans Ada, cela devient
with System.Machine_Code
...
procedure Io_Write_32 (Val : U32; Addr : Iomem_Access) is
use System.Machine_Code;
begin
Asm (Template => "dsb st",
Clobber => "memory",
Volatile => True);
Asm (Template => "str %w0, [%1]",
Inputs => (U32'Asm_Input ("rZ", Val),
Iomem_Access'Asm_Input ("r", Addr)),
Volatile => True);
end;
Ce Io_Write_32 l’implémentation n’est pas portable car nous avons reconstruit la macro suite à l’extension adaptée à arm64. L’emballage AC poserait moins de problèmes tout en garantissant la portabilité. Néanmoins, nous avons pensé que cette expérience était une bonne occasion de montrer des directives d’assemblage en Ada.
C’est ça!
J’espère que vous avez apprécié cet aperçu modérément dense d’Ada dans le contexte du développement de modules du noyau Linux. Je pense que nous pouvons convenir qu’Ada est une concurrente vraiment disciplinée et puissante en matière de système, de pédale au métal, de programmation. Je vous remercie pour votre temps et votre sollicitude. N’hésitez pas à tendre la main et, bon codage Ada !
Je tiens à remercier Quentin Ochem, Nicolas Setton, Fabien Chouteau, Jérôme Lambourg, Michael Frank, Derek Schacht, Arnaud Charlet, Pat Bernardi, Leo Germond et Artium Nihamkin pour leurs différentes idées et commentaires pour réussir cette expérience.
L’auteur, Olivier Henleyest ingénieur UX chez AdaCoreComment. Son rôle consiste à explorer de nouveaux marchés à travers des histoires techniques. Avant de rejoindre AdaCore, Olivier était ingénieur logiciel consultant pour Autodesk. Avant cela, Olivier a travaillé sur des titres de jeux AAA tels que For Honor et Rainbow Six Siege en plus de nombreux efforts de R&D en matière de jeux chez Ubisoft Montréal. Olivier est diplômé du programme de génie électrique de Polytechnique Montréal. Il est co-auteur du brevet US8884949B1, décrivant l’invention d’un nouveau filtre temporel impliquant la technologie NI. Défenseur d’Ada, Olivier organise activement Liste Awesome-Ada de GitHub.