Надежное программирование в разрезе языков — нубообзор. Часть 1

habr.png

В очередной раз провозившись два дня на написание и отладку всего четырехсот строк кода системной библиотеки, возникла мысль — «как бы хорошо, если бы программы писались менее болезненным способом».

И в первую очередь, поскольку отладка занимает гораздо больше времени, чем написание кода — нужна защита от дурака (в т.ч.себя) на этапе написания. И это хотелось бы получить от используемого языка программирования (ЯП).

Конечно же, надо изобрести новый, самый лучший ЯП!
Нет, сначала попробуем выразить свои пожелания и посмотреть на то, что уже наизобретали.

Итак, что бы хотелось получить:


  • Устойчивость к ошибкам человека, исключение неоднозначностей при компиляции
  • Устойчивость к входным данным
  • Устойчивость к повреждению программы или данных — сбой носителя, взлом
  • При этом всем — более-менее терпимый синтаксис и функциональность

Область желательного применения — машинерия, транспорт, промышленные системы управления, IoT, эмбеддед включая телефоны.

Вряд ли это нужно для Веб, он построен (пока) на принципе «бросил и перезапустил» (fire and forget).

Достаточно быстро можно прийти к выводу, что язык должен быть компилируемым (как минимум Пи-компилируемым), чтобы все проверки максимально были выполнены на этапе компиляции без VS (Версус, далее по тексту негативное противопоставление) «ой, у этого объекта нет такого свойства» в рантайме. Даже скриптование описаний интерфейса уже приводят к обязательности полного покрытия тестами таких скриптов.

Лично я не хочу платить своими ресурсами (в т.ч. деньгами за более быстрое дорогое железо) за интерпретацию, потому итого желательно иметь минимум JIT-компиляцию.

Итак, требования.


Устойчивость к ошибкам человека

Старательно полистав талмуды от PVS-Studio, я выяснил, что самые распространенные ошибки — это опечатки и недоправленная копипаста.

Еще добавлю чуточку казусов из своего опыта и встреченных в различной литературе, как негативные примеры. Дополнительно обновил в памяти правила MISRA C.

Чуть позже обдумав, пришел к выводу, что линтеры, примененные постфактум страдают от «ошибки выжившего», поскольку в старых проектах серьезные ошибки уже исправлены.

а) Избавляемся от похожих имен


  • должна проводится жесткая проверка видимости переменных и функций. Опечатавшись, можно использовать идентификатор из более общей области видимости, вместо нужного
  • использоваться регистронезависимые имена. (VS) «Давайте функцию назовем как переменную, только в Кэмелкейз» и потом с чем нибудь сравним — в С так сделать можно (получим адрес ф-ции, который вполне себе число)
  • имена с отличием на 1 букву должны вызывать предупреждение (спорно, можно выделять в IDE), но очень распространенная ошибка копипасты .x, .y, .w, .h.
  • не допускаем одинаково именовать разные сущности — если есть константа с таким названием, то не должно быть одноименной переменной или имени типа
  • крайне желательно, именование проверять по всем модулям проекта — легко перепутать, особенно если разные модули пишут разные люди

б) Раз упомянул — должна присутствовать модульность и желательно иерархическая — VS проект из 12000 файлов в одном каталоге — это ад поиска.
Еще модульность обязательна для описаний структур обмена данными между разными частями (модулями, программами) одного проекта. VS Встречал ошибку из-за разного выравнивания чисел в обменной структуре в приемнике и передатчике.


  • Исключить возможность дублей линковки (компоновки).

в) Неоднозначности


  • Должен быть определенный порядок вызовов функций. При записи X = funcA () + fB () или Fn (funcA (), fB (), callC ()) — надо понимать, что человек рассчитывает получить вычисления в записанном порядке, (VS), а не как себе надумал оптимизатор.
  • Исключить похожие операторы. А не как в С: + ++, < <<, | ||, & &&, = ==
  • Желательно иметь немного понятных операторов и с очевидным приоритетом. Привет от тернарного оператора.
  • Переопределение операторов скорее вредно. Вы пишете i:= 2, но (VS) на самом деле это вызывает неявное создание объекта, для которого не хватает памяти, а диск дает сбой при сваппинге и ваш спутник падает на Марс :-(

На самом деле из личного опыта наблюдал вылет на строке ConnectionString = «DSN», это оказалось сеттером, который открывал БД (а сервер не был виден в сети).


  • Нужна инициализация всех переменных дефолтными значениями.
  • Также ООП подход спасает от забывчивости переназначения всех полей в объекте в какой-нибудь новой сотой функции.
  • Система типов должна быть безопасной — нужен контроль за размерностями присваиваемых объектов — защита от затирания памяти, арифметическим переполнением типа 65535+1, потерями точности и значимости при приведении типов, исключение сравнения несравнимого — ведь целое 2 не равно 2.0 в общем случае.

И даже типовое деление на 0 может давать вполне определенный +INF, вместо ошибки — нужно точное определение результата.


Устойчивость к входным данным


  • Программа должна работать на любых входных данных и желательно, примерно одинаковое время. (VS) Привет Андроиду с реакцией на кнопку трубки от 0.2с до 5с; хорошо, что не Андроид управляет автомобильной ABS.

Например, программа должна корректно обрабатывать и 1Кб данных и 1Тб, не исчерпав ресурсы системы.


  • Очень желательно иметь в ЯП RAII и надежную и однозначную обработка ошибок, не приводящую к побочным эффектам (утечкам ресурсов, например). (VS) Очень веселая вещь — утечка хендлов, проявиться может через многие месяцы.
  • Было бы неплохо защититься от переполнения стека — рекурсию запретить.
  • Проблема превышения доступного объема требуемой памятью, неконтролируемый рост потребления из-за фрагментации при динамическом выделении/освобождении. Если же язык имеет рантайм, зависимый от кучи, дело скорее всего плохо — привет STL и Phobos. (VS) Была история со старым C-рантаймом от Микрософт, который неадекватно возвращал память системе, из-за чего msbackup падал на больших объемах (для того времени).
  • Нужна хорошая и безопасная работа со строками — не упирающаяся в ресурсы. Это сильно зависит от реализации (иммутабельные, COW, R/W массивы)
  • Превышение времени реакции системы, не зависящее от программиста. Это типичная проблема сборщиков мусора. Хотя они и спасают от одних ошибок программирования — привносят другие — плохо диагностируемые.
  • В определенном классе задач, оказывается, можно обойтись совсем без динамической памяти, либо однократно выделив ее при старте.
  • Контролировать выход за границы массива, причем вполне допустимо написать предупреждение рантайма и игнорировать. Очень часто это некритичные ошибки.
  • Иметь защиту от обращений к неинициализованному программой участку памяти, в т.ч к null-области, и в чужое адресное пространство.
  • Интерпретаторы, JIT — лишние прослойки снижают надежность, есть проблемы с сборкой мусора (очень сложная подсистема — привнесет свои ошибки), и с гарантированным временем реакции. Исключаем, но есть в принципе Java Micro Edition (где от Явы отрезано так много, что остается только Я, была интересная статья dernasherbrezon (жаль, пристрелили) и .NET Micro Framework с C#.

Впрочем, по рассмотрению, эти варианты отпали:


  • .NET Micro оказался обычным интерпретатором (вычеркиваем по скорости);
  • Java Micro — пригодна только для внедряемых приложений, поскольку слишком сильно кастрирована по API, и придется для разработки переходить хотя бы на SE Embedded, которую уже закрыли или обычную Java«у, слишком монструозную и непредсказуемую по реакции.
    Впрочем, есть еще варианты, и хотя это и не выглядит заготовкой для работоспособного фундамента, но можно сравнить с другими языками, даже устаревшими или обладающими определенными недостатками.


  • Устойчивость к многопоточной работе — защита приватных данных потока, и механизмы гарантированного обмена между потоками. Программа в 200 потоков может работать совсем не так, как в два.
  • Контрактное программирование плюс встроенные юниттесты тоже весьма помогают спать спокойно.


Устойчивость к повреждению программы или данных — сбой носителя, взлом


  • Программа должна целиком загружаться в память — без подгрузки модулей, особенно удаленно.
  • Очищаем память при освобождении (а не только выделении)
  • Контроль переполнения стека, областей переменных, особенно строки.
  • Перезапуск после сбоя.

Кстати, подход, когда рантайм имеет свой логгинг, а не только выдает, что северный песец и стектрейс, мне очень импонирует.


Языки — и таблица соответствия

На первый взгляд, для анализа возьмем специально разработанные безопасные ЯП :


  1. Active Oberon
  2. Ada
  3. BetterC (dlang subset)
  4. IEC 61131–3 ST
  5. Safe-C

И пройдемся по ним с точки зрения вышеприведенных критериев.

Но это уже объем для статьи-продолжения, если карма позволит.

С выделением в таблицу вышеупомянутых факторов, ну и возможно — еще что то толковое почерпнется из комментариев.

Что же касается прочих интересных языков — C++, Crystal, Go, Delphi, Nim, Red, Rust, Zig (добавьте по вкусу) то заполнять таблицу соответствия по ним оставлю желающим.

Дисклеймеры:


  • В принципе, если программа, скажем на Питоне, потребляет 30Мб, а требования к реакции- секунды, а микрокомпьютер имеет 600 Мб свободной памяти и 600 МГц проц — то почему нет? Только надежной такая программа будет с некоторой вероятностью (пусть и 96%), не более.
  • Кроме того, язык должен стараться быть удобным для программиста — иначе никто его использовать не будет. Такие статьи «я придумал идеальный язык программирования, чтобы мне и только мне было удобно писать» — не редкость и на Хабре тоже, но это совсем о другом.

© Habrahabr.ru