Какие части C наиболее переносимы?
Недавно я прочитал интервью с соавторами Луи Луисом Х. де Фигеродо и Роберто Иерусульским, где они обсудили дизайн и реализацию Lua. Это было очень интригующе, если не сказать больше. Однако одна часть обсуждения вызвала у меня что-то. Роберто говорил о Lua как о "автономном приложении" (т.е. Чистом ANSI C, который ничего не использует для ОС). Он сказал, что ядро Lua полностью переносимо и благодаря своей чистоте удалось значительно портировать более легко и на платформах, которые даже не рассматриваются (например, роботы и встроенные устройства).
Теперь это заставляет меня задуматься. C в целом - очень портативный язык. Итак, какие части C (а именно те, что содержатся в стандартной библиотеке) являются самыми неуязвимыми? и каковы те, которые можно ожидать для работы на большинстве платформ? Следует использовать только ограниченный набор типов данных (например, избегая short
и, возможно, float
)? Как насчет системы FILE
и stdio
? malloc
и free
? Кажется, что Луа избегает всех этих. Это что-то до крайности? Или они являются корнем проблем переносимости? Помимо этого, что еще можно сделать, чтобы сделать код чрезвычайно портативным?
Причина, по которой я все это прошу, заключается в том, что я пишу приложение в чистом C89, и это оптимально, насколько это возможно. Я готов пойти на средний путь в его реализации (достаточно переносимый, но не так много, что я должен писать все с нуля.) В любом случае, я просто хотел узнать, что вообще является ключом к написанию лучшего кода на языке C.
Как последнее замечание, все это обсуждение связано только с C89.
Ответы
Ответ 1
В случае с Lua нам нечего жаловаться на сам язык C, но мы обнаружили, что стандартная библиотека C содержит множество функций, которые кажутся безвредными и прямолинейными для использования, пока вы не подумаете, что они делают не проверяйте их ввод на достоверность (это нормально, если это неудобно). В стандарте C говорится, что обработка неправильного ввода - это undefined поведение, позволяющее этим функциям делать то, что они хотят, даже сбой программы-хозяина. Рассмотрим, например, strftime. Некоторые libc просто игнорируют недопустимые спецификаторы форматирования, но другие libc (например, в Windows) разбиваются! Теперь strftime не является важной функцией. Почему крушение вместо того, чтобы делать что-то разумное? Таким образом, Lua должен выполнить свою собственную проверку ввода перед вызовом strftime и экспортировать strftime в Lua-программы, что становится основной задачей. Следовательно, мы попытались остаться в стороне от этих проблем в ядре Lua, нацелившись на автономную основу. Но стандартные библиотеки Lua не могут этого сделать, потому что их целью является экспорт средств в программы Lua, в том числе то, что доступно в стандартной библиотеке C.
Ответ 2
"Freestanding" имеет особое значение в контексте C. Примерно, автономные хосты не обязаны предоставлять какую-либо из стандартных библиотек, включая библиотечные функции malloc
/free
, printf
и т.д. Определенные стандартные заголовки по-прежнему необходимы, но они определяют только типы и макросы (например, stddef.h
).
Ответ 3
C89 позволяет использовать два типа компиляторов: хостинг и автономный. Основное отличие заключается в том, что размещенный компилятор предоставляет всю библиотеку C89, в то время как автономный компилятор должен предоставлять только <float.h>
, <limits.h>
, <stdarg.h>
и <stddef.h>
. Если вы ограничиваетесь этими заголовками, ваш код будет переносимым для любого компилятора C89.
Ответ 4
Это очень широкий вопрос. Я не буду давать определенный ответ, вместо этого я буду поднимать некоторые проблемы.
Обратите внимание, что стандарт C указывает определенные вещи как "определенные реализацией"; соответствующая программа всегда будет компилироваться и запускаться на любой соответствующей платформе, но она может вести себя по-разному в зависимости от платформы. В частности, есть
- Размер слова.
sizeof(long)
может быть четыре байта на одной платформе, восемь на другой. Размеры short
, int
, long
и т.д. Имеют некоторый минимум (часто относительно друг друга), но в противном случае гарантий нет.
- Порядок байтов.
int a = 0xff00; int b = ((char *)&a)[0];
может назначать 0
на b
на одной платформе, -1
на другой.
- Кодировка символов.
\0
всегда является нулевым байтом, но как появляются другие символы, зависит от ОС и других факторов.
- Текстовый режим ввода/вывода.
putchar('\n')
может выдавать символ линии на одной платформе, возврат каретки на следующий и комбинацию каждого из них на другой.
- Подпись char. Возможно, что
char
может принимать отрицательные значения.
- Размер байта. Хотя в настоящее время байт составляет восемь бит практически везде, C обслуживает даже несколько экзотических платформ, где это не так.
Разнообразные размеры и суждения разных слов являются общими. Проблемы с кодировкой символов, вероятно, появятся в любом приложении для обработки текста. Машины с 9-битным байтом, скорее всего, будут найдены в музеях. Это далеко не полный список.
(И, пожалуйста, не пишите C89, это устаревший стандарт. C99 добавил некоторые довольно полезные материалы для переносимости, такие как целые числа фиксированной ширины int32_t
и т.д.)
Ответ 5
Все, что входит в стандарт C89, должно быть портативным для любого компилятора, соответствующего этому стандарту. Если вы придерживаетесь чистого C89, вы можете легко его переносить. Тогда любые проблемы с переносимостью будут связаны с ошибками компилятора или местами, где код вызывает поведение, специфичное для реализации.
Ответ 6
C был сконструирован таким образом, что компилятор может быть написан для генерации кода для любой платформы и вызова языка, который он компилирует, "C". Такая свобода действует в противоположность тому, что С является языком для написания кода, который можно использовать на любой платформе.
Любой код для записи C должен решить (намеренно или по умолчанию), какие размеры int
они будут поддерживать; в то время как можно написать код C, который будет работать с любым юридическим размером int
, он требует значительных усилий, и полученный код будет часто менее читабельным, чем код, предназначенный для определенного целочисленного размера. Например, если у вас есть переменная x
типа uint32_t
, и один хочет ее умножить на другой y
, вычисляя результат mod 4294967296, оператор x*=y;
будет работать на платформах, где int
равно 32 бит или меньше или где int
составляет 65 бит или больше, но будет вызывать Undefined Behavior
в случаях, когда int
составляет от 33 до 64 бит, а продукт, если операнды считаются целыми числами, а не членами алгебраическое кольцо, которое обертывает мод 4294967296, превысит INT_MAX
. Можно заставить оператор работать независимо от размера int
, переписав его как x*=1u*y;
, но это делает код менее ясным, и случайное исключение 1u*
из одного из умножений может быть катастрофическим.
В соответствии с настоящими правилами C достаточно переносима, если код используется только для машин, целочисленный размер которых соответствует ожиданиям. На машинах, где размер int
не соответствует ожиданиям, код вряд ли будет переносимым, если он не содержит достаточного количества принуждений типов, чтобы сделать большинство правил ввода текста несущественными.