Swift — le héros inattendu de la programmation système
Quand on entend « programmation système », on pense généralement à du C austère, de l'assembleur ou, à la rigueur, à Rust. Swift ? Ce langage pour les belles applications mobiles ? Il s'avère que oui. Et ce n'est pas une blague.
Apple a intégré dans Swift non seulement la commodité et la sécurité, mais aussi un contrôle total sur la mémoire, les pointeurs et les appels système. Dans cet article, nous verrons comment Swift peut devenir votre outil principal pour écrire des utilitaires, des pilotes, des démons réseau et même des composants de système d'exploitation. Préparez-vous à être surpris — le code sera réel, fonctionnel et très intéressant.
Pourquoi Swift ? Les trois piliers de la programmation système
La programmation système exige trois choses : des performances, un contrôle des ressources et la possibilité de travailler « près du matériel ». Swift offre tout cela, et même plus.
- Performances au niveau du C. Le compilateur LLVM optimise le code aussi bien que pour le C ou le C++. Dans les benchmarks, Swift dépasse souvent Go et Rust dans les tâches d'E/S.
- Accès direct à la mémoire. Via le mécanisme UnsafePointer, UnsafeMutablePointer et le travail avec des octets bruts.
- Bibliothèque standard riche. Pas besoin de traîner des tonnes de dépendances — tout pour travailler avec les sockets, les fichiers, les processus est déjà présent.
Swift reste néanmoins sûr : si vous n'utilisez pas de blocs unsafe, le compilateur vous protège des débordements de tampon et des pointeurs pendants. Et quand c'est nécessaire, il suffit d'écrire withUnsafePointer pour obtenir un contrôle total.
Gestion de la mémoire : des octets aux structures
Commençons par les bases — le travail avec la mémoire brute. Imaginez que vous devez lire un fichier binaire et l'interpréter comme un tableau de structures. En C, vous peineriez avec malloc et sizeof. En Swift — élégant et sûr.
Lecture d'un fichier binaire en Swift
import Foundation
struct Header { let magic: UInt32 let version: UInt16 let count: UInt16}
func readBinaryFile(path: String) -> Header? { let data = try? Data(contentsOf: URL(fileURLWithPath: path)) guard let data = data else { return nil } return data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in guard let baseAddress = ptr.baseAddress else { return nil } let headerPtr = baseAddress.assumingMemoryBound(to: Header.self) return headerPtr.pointee }}Remarquez : nous n'allouons pas la mémoire manuellement. withUnsafeBytes fournit un pointeur temporaire qui ne vit qu'à l'intérieur de la fermeture. C'est sûr, efficace et se lit comme une bonne prose.
Création de structures et travail avec les pointeurs
Et si vous devez créer une structure en mémoire et passer son adresse à un appel système ? Facile !
struct sockaddr_in { var sin_len: UInt8 = 0 var sin_family: UInt8 = UInt8(AF_INET) var sin_port: in_port_t = 0 var sin_addr: in_addr = in_addr(s_addr: 0) var sin_zero: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0,0,0,0,0,0,0,0)}
var address = sockaddr_in()address.sin_port = CFSwapInt16HostToBig(8080)address.sin_addr.s_addr = inet_addr("127.0.0.1")
let ptr = withUnsafePointer(to: &address) { $0 }let rawPtr = UnsafeRawPointer(ptr)Ce code est la base pour travailler avec les sockets à bas niveau. Nous définissons manuellement la structure, remplissons les champs et obtenons un pointeur brut à passer à connect ou bind.
Appels système et API POSIX
Swift s'entend très bien avec POSIX. Vous pouvez appeler open, read, write, fork, exec — comme au bon vieux temps, mais avec la syntaxe du XXIe siècle.
Création de fork + exec en Swift
import Darwin
func spawnProcess(path: String, args: [Str