Swift — неожиданный герой системного программирования
Когда слышишь «системное программирование», обычно на ум приходят суровые C, ассемблер или на худой конец Rust. Swift? Тот самый язык для красивых мобильных приложений? Оказывается, да. И это не шутка.
Apple вложила в Swift не только удобство и безопасность, но и полный контроль над памятью, указателями и системными вызовами. В этой статье мы разберем, как Swift может стать вашим основным инструментом для написания утилит, драйверов, сетевых демонов и даже компонентов операционной системы. Приготовьтесь удивляться — код будет реальным, рабочим и очень интересным.
Почему Swift? Три кита системного программирования
Системное программирование требует трех вещей: производительности, контроля над ресурсами и возможности работать «близко к железу». Swift дает всё это, плюс кое-что сверху.
- Производительность на уровне C. Компилятор LLVM оптимизирует код не хуже, чем для C или C++. В бенчмарках Swift часто обгоняет Go и Rust в IO-задачах.
- Прямой доступ к памяти. Через механизм UnsafePointer, UnsafeMutablePointer и работу с сырыми байтами.
- Богатая стандартная библиотека. Не нужно тащить за собой кучу зависимостей — всё для работы с сокетами, файлами, процессами уже есть.
При этом Swift остается безопасным: если не использовать unsafe блоки, компилятор защитит вас от переполнения буфера и висячих указателей. А когда нужно — просто пишете withUnsafePointer и получаете полный контроль.
Работа с памятью: от байтов до структур
Начнем с основ — работы с сырой памятью. Представьте, что вам нужно прочитать бинарный файл и интерпретировать его как массив структур. На C вы бы мучились с malloc и sizeof. На Swift — элегантно и безопасно.
Чтение бинарного файла в 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
}
}
Обратите внимание: мы не выделяем память вручную. withUnsafeBytes предоставляет временный указатель, который живет только внутри замыкания. Это безопасно, эффективно и читается как хорошая проза.
Создание структур и работа с указателями
А что если нужно создать структуру в памяти и передать ее адрес системному вызову? Легко!
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)
Этот код — основа для работы с сокетами на низком уровне. Мы вручную определяем структуру, заполняем поля и получаем сырой указатель для передачи в connect или bind.
Системные вызовы и POSIX API
Swift отлично дружит с POSIX. Вы можете вызывать open, read, write, fork, exec — всё как в старые добрые времена, но с синтаксисом XXI века.
Создание fork + exec в Swift
import Darwin
func spawnProcess(path: String, args: [String]) -> pid_t? {
let pid = fork()
if pid == 0 {
// Дочерний процесс
var cargs = args.map { strdup($0) }
cargs.append(nil)
execvp(path, &cargs)
// Если exec вернулся — ошибка
exit(1)
} else if pid > 0 {
return pid
}
return nil
}
// Использование
if let childPid = spawnProcess(path: "/bin/ls", args: ["ls", "-la"]) {
print("Дочерний процесс запущен, PID: \(childPid)")
waitpid(childPid, nil, 0)
}
Важно: fork — это системный вызов, который возвращает 0 в дочернем процессе и PID в родительском. Swift не пытается «улучшить» этот механизм — он просто дает вам доступ к нему. Чистая мощь.
Работа с сигналами
Системное программирование невозможно без обработки сигналов. Убить процесс, перезагрузить конфиг, поймать SIGINT — всё это делается в несколько строк.
import Darwin
signal(SIGINT) { _ in
print("Получен SIGINT, завершаем работу...")
exit(0)
}
signal(SIGTERM) { _ in
print("Получен SIGTERM, сохраняем состояние...")
// Здесь можно сохранить данные
exit(0)
}
print("Демон запущен. Нажмите Ctrl+C для остановки.")
while true {
sleep(1)
}
Обратите внимание: сигнальные обработчики в Swift — это обычные замыкания. Никаких макросов, никакой магии. Просто код.
Сетевые сокеты на низком уровне
Конечно, можно использовать Network.framework от Apple, но для системного программирования иногда нужен чистый BSD-сокет. Swift справляется и с этим.
Создание TCP-сервера без внешних зависимостей
import Darwin
func startServer(port: UInt16) {
let serverFd = socket(AF_INET, SOCK_STREAM, 0)
guard serverFd >= 0 else {
perror("socket"); return
}
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = CFSwapInt16HostToBig(port)
addr.sin_addr.s_addr = INADDR_ANY
let bindResult = withUnsafePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
bind(serverFd, $0, socklen_t(MemoryLayout.size))
}
}
guard bindResult == 0 else {
perror("bind"); close(serverFd); return
}
listen(serverFd, 5)
print("Сервер слушает порт \(port)")
while true {
var clientAddr = sockaddr_in()
var clientLen = socklen_t(MemoryLayout.size)
let clientFd = withUnsafeMutablePointer(to: &clientAddr) {