BD103 описал странности языка C для программистов на Rust
Автор блога BD103 пишет, что первым системным языком программирования для него стал Rust, и это нетипично: чаще люди сначала учат C или C++ и лишь потом приходят к Rust. Статей «Rust для программистов на C» в интернете много, а «C для программистов на Rust» почти нет. Недавно автор изучает C и C++ и собрал список удивительных деталей C (C++ в этот раз не затрагивается). Текст прямо назван не заменой учебнику, а перечнем того, что стоит держать в голове при работе с языком.
Булевы типы. В исходном C примитивного булева типа не было, использовали целые 0 и 1. В C99 булевы значения добавили в необязательном заголовке <stdbool.h>, причём true и false, не литералы и не ключевые слова, а определения, которые раскрываются в 1 и 0. В C23 булевы стали настоящими примитивами языка, но при компиляции под более ранние стандарты заголовок по-прежнему нужен. В сноске автор отмечает, что Clang, судя по всему, поддерживает C23 лишь частично, так что убирать include пока рано.
Строки с нулевым байтом в конце. Rust-ссылка &str занимает 16 байт: 8 байт на адрес и 8 на длину строки, тогда как обычная ссылка, 8 байт. C длину отдельно не хранит, а завершает каждую строку нулевым байтом; автор называет это осознанным компромиссом. Следствия: не нужна отдельная переменная с размером; каждой строке нужно место под завершающий байт, поэтому даже пустая строка занимает один байт; нулевые байты нельзя просто так использовать внутри строки, не ломая функции из <string.h>; забытый терминатор приводит к чтению за границами массива. На примере функции разворота строки автор показывает, что приходится выделять len + 1 байт через malloc и вручную дописывать нулевой байт, а Rust-аналог этого не требует.
Целые разной ширины. Ширина целых типов в C не гарантирована и зависит от платформы; автора особенно раздражает, что long занимает 64 бита в Unix и 32 бита в Windows. Для кроссплатформенности автор следует совету друга: использовать только целые фиксированной ширины из <stdint.h>.
Обработка ошибок. Автор любит Result в Rust, который заставляет разбирать ошибки. В C, по его словам, всё сводится к «магическим» значениям вроде -1 или нулевого указателя, плюс errno, потокозависимое целое для уточнения вида ошибки; получить внятное сообщение и трассировку стека гораздо сложнее. Язык никогда не заставит обработать ошибку: например, malloc() может вернуть нулевой указатель, и без проверки при нехватке памяти программа упадёт с segfault, поэтому нужно проверять указатель и аккуратно завершаться с сообщением. Автор пробовал воссоздать Result на помеченных объединениях, но пользоваться этим «полный бардак», и ничто не мешает сразу обратиться к значению, не проверив ошибку. Отсутствие модификаторов private и public, которые могли бы это предотвратить, по мнению автора, похоже на осознанное решение: C целиком доверяет программисту. Лично автор с таким подходом не согласен и предпочёл бы выражать требования в системе типов, чтобы их проверял компилятор.
Доступ к полям. В C два оператора: точка для значений и -> для указателей, тогда как в Rust точка работает везде.
Массивы становятся указателями. Внутри функции, где массив объявлен, sizeof возвращает его размер (в примере, 3 байта для обоих массивов). Но стоит передать массивы в другую функцию, как sizeof сообщает 8 байт: массив неявно превращается в указатель на первый элемент, а 8 байт, размер указателя. Clang в таком случае выдаёт предупреждение -Wsizeof-array-argument.
Ссылок нет. В C нет проверки заимствований и понятия ссылок, только сырые указатели, с которыми можно делать арифметику. Автор сомневается, что это хорошая идея, и напоминает: ошибки с памятью дают переполнение буфера и запись за границами, а это серьёзные угрозы безопасности.
Итог. Автору было интересно изучать C; он сомневается, что возьмёт его для личных проектов, но считает язык обязательным для системного программиста. Все примеры лежат на GitHub.
Ключевые факты
- BD103 учил Rust первым системным языком и теперь собрал список странностей C для таких же программистов; текст, не замена учебнику.
- Булевы типы в C появились только в C99 через <stdbool.h> (true и false, определения 1 и 0), а в C23 стали примитивами языка.
- Строка в C завершается нулевым байтом и не хранит длину: пустая строка занимает один байт, а забытый терминатор ведёт к чтению за границами; Rust-ссылка &str для сравнения занимает 16 байт.
- Ширина целых зависит от платформы (long, 64 бита в Unix и 32 в Windows); автор советует фиксированные типы из <stdint.h>.
- Массив, переданный в функцию, неявно становится указателем: sizeof вместо 3 байт показывает 8; ссылок в C нет, только сырые указатели.
Почему это важно
Материал закрывает конкретный пробел, который автор сам отмечает: «Rust для программистов на C» написано много, а обратного направления почти нет. Он показывает, где привычки из Rust (длина строки внутри ссылки, обязательная обработка Result, ссылки с проверкой заимствований) не переносятся на C. Это личный разбор новичка в C, а не новое исследование или новость индустрии.
Кому это важно
В первую очередь программистам, которые пришли в системное программирование через Rust и теперь знакомятся с C или читают кодовую базу на C. Полезен и тем, кто сравнивает подходы двух языков к строкам, ошибкам и памяти.
Как это применить
Из текста можно вынести несколько практических правил: подключать <stdbool.h> при компиляции под стандарты до C23; для кроссплатформенного кода брать целые из <stdint.h>; всегда проверять результат malloc() на нулевой указатель; закладывать место под завершающий нулевой байт в строках; не применять sizeof к массиву, пришедшему в функцию аргументом (Clang предупредит об этом). Все примеры автора выложены на GitHub, их можно запускать самостоятельно. Как отмечено в самом тексте, это не замена учебнику по C.
Можно ли доверять
Это личный блог человека, который сам говорит, что изучает C и C++ недавно. Оценки вроде «обработка ошибок, трагедия» и «мне не нравится такой подход» поданы как личные предпочтения, а не как установленные факты. Технические утверждения подкреплены короткими примерами кода и выводами программ, в сносках автор честно оговаривает слабые места. Бенчмарков и измерений производительности в тексте нет.
Риски и подводные камни
Автор сам предупреждает, что Rust-пример разворота строки не идиоматичен и корректно работает только с ASCII. В сноске он признаёт, что не до конца понимает разницу между usize/isize и size_t/ptrdiff_t, и советует разобраться самостоятельно. Поддержка C23 в Clang, по его словам, пока лишь частичная. Совет про <stdint.h> передан со слов друга. Работа с указателями и памятью, напоминает автор, чревата переполнением буфера и записью за границами, это угрозы безопасности кода.
«C полностью доверяет программисту делать всё правильно™ и почти не даёт средств для контрактов и безопасных абстракций.»
— BD103, блог «C for Rust Programmers»