понедельник, 30 августа 2010 г.

SafeD – безопасный D

Bartosz Milewski, член команды дизайна языка D.

Я видел, как множество программистов бросают C++ в пользу таких языков, как Java или C#. Будучи опытным разработчиком на C++, я удивлялся, почему кому-то захочется перейти на язык с меньшими возможностями и эффективностью. Я понимаю, когда новички выбирают что-то попроще, чтобы можно было быстро осваивать, но когда это делает тот, кто уже вложил столько сил и энергии на приобретение опыта на C++, почему кому-то захочется бросить писать на С++? Универсальная причина, которую мне приходилось слышать от таких людей – это «продуктивность». В общем, их мнения сводятся к тому, что программисты показывают большую продуктивность, когда пишут на Java, C#, Ruby, или Python вместо C++. Что является препятствием к продуктивному программированию на C++? Ужасный синтаксис. На самом деле это большая проблема, чем кажется. Хороший программист, скорее всего, сможет сделает что-нибудь привлекательное и с таким синтаксисом, если дать ему побольше времени. Однако проблема в том, что синтаксис трудно понять не только программисту, но и парсерам. Тот факт, что рынок Java переполнен всевозможными утилитами и инструментами для повышения продуктивности является следствием легкого для разбора синтаксиса. Я еще не видел инструментов с такими возможностями для рефакторинга в C++, которые имеются для Java. Безопасность языка – другой важный фактор. C++ печально известен тем, что предоставляет нескончаемую галерею возможностей выстрелить себе в ногу. На самом деле C++ не только дает такие возможности, но и поощряет их. В какой-то момент один из основных поставщиков C++ компилятора пометил часть алгоритмов из STL как "deprecated" из-за проблем безопасности. В частности, стандартная библиотека C++, согласно атмосфере языка C++, дает кучу путей занести в вашу программу переполнение буфера. Печально известен алгоритм std::swap_range, который принимает три итератора. Два первых должны определять границы одной последовательности, третий определяет начало третьей последовательности. При этом не проводится никакой проверки выхода за границы контейнера. А когда это случается, вирусописатели бурно радуются. Заветной мечтой программиста является иметь такой язык, при котором если программа скомпилировалась, то она обязательно и работать будет. Вы должны определиться с понятием «рабочая» программа. Например, вы можете потребовать от программы, чтобы она никогда не зависала. For instance, you might require that the program will never get "stuck"—a term which has a precise meaning in computer science, but loosely means that the program will not GP-fault on you (it is stuck in the sense that there is no well-defined system-independent next step). Languages that have such a property are called "sound". Guess what, there is a well-defined (and meaningful) subset of Java that is sound. Java программы на практике, такие моменты встречаются редко, и их легче обнаружить, чем в программе C++. На практике компилятор Java обнаружит больше ошибок в вашей программе, чем C++ компилятор, что тут же сказывается на меньшем времени, потраченном на отладку и повышении продуктивности. А какие у C++ плюсы? Производительность. Действительно трудно побить C++ по скорости. Если ваша программа должна быть быстрой и хорошо реагировать, то выбор не велик - используйте C++ (или, иногда, C или ассемблера). Еще в C++ можно писать программы, напрямую взаимодействующие с оборудованием. Например, C и C++ до сих пор остаются главными языками в программировании встроенных систем. C++ предлагает средства построения мощных абстракций, писать обобщенный код. В Java и C# есть свои генерики, но их трудно сравнить с тем, что может предложить C++. Все эти качества деляют C++ идеальным языком для написания операционных систем. Операционные системы - это огромные программы, которые должны быть быстрыми и непосредственно взаимодействовать с оборудованием. Однако и в других областях требуется написание больших и быстрых приложений. Таким образом, складывается впечатление, что мир программирования должен быть четко разделен между C++, Java, C# и другими. И это верно, пока вы верите в неминуемость компромиссов. Но нет такого закона в природе, который бы заставлял выбирать между продуктивностью и мощностью. Что насчет языка, который устроен как лук. У него есть простое и безопасное ядро, как в Java или C#. Программист может быстро работать с безопасной частью и быть продуктивным, как программисты Java (если не больше). И что если безопасная часть предлагает производительность, сравнимую с C++? И в то же время, в языке будет часть, к которой будут обращаться не так часто, а по мере возникновения необходимости. Она будет предоставлять возможности работы с оборудованием, высокоуровневые средства для генерирования кода по мере необходимости. Она будет предлагать модульность и сокрытие реализации. И у него будут непревзойденные возможности при компиляции, что будет обеспечивать высокую скорость работы программы. Я открою вам один секрет, этот язык D.

Программные ловушки
Вы знали что программа "Hello World!", которая обычно бывает первой написанной человеком на С программой, выявляет самые опасные моменты языка? Программа содержит следующее выражение: printf ("Hello World!\n"); Рассмотрим объявление функции printf: int printf (const char * restrict format, ...); (restrict - новое ключевое слово C.) Данная функция принимает переменное число аргументов. Количество и тип переменных указываются в строке форматирования. Когда производится проверка соответствия между форматом и списком аргументов? Не во время компиляции, т.к. компилятор понятия не имеет о строке форматирования (хотя некоторые компиляторы способны выдать предупреждение если строка статически известна). Тогда во время компиляции? Если программист сделал ошибку при вызове printf с меньшим числом аргументов, то он получит замечательный код ошибки или исключение. Вот что говорится об этом в стандарте языка С: если не достаточно аргументов для форматирования, то поведение не определено. Неопределенное поведение - это худшее, что может случится с программой. Если вам повезло, программа выкинет ошибку и остановится. Если вам не совсем повезло, то программа продолжит работу в неопределенном состоянии, и в худшем случае, он выполнит поврежденный код, который возмет полный контроль над компьютером. Другая опасность в функции printf заключается в использовании указателя. Программист сам должен проследить за тем, чтобы указатель адресовал правильное место в памяти. В примере "Hello World!" указатель адресует статическую заканчивающуюся завершающим нулем строку, так что проблем не возникает. Но вот эта программа так же успешно скомпилируется: char * format = 0; printf (format); Угадайте, что случится. Внутри printf указатель разыменовывается и все рушится. Снова, цитируя стандарт С: если указателю было присвоено неверное значение, то поведение унарного оператора * не определено. Каждый раз при выделении памяти возвращается верный адрес (пока у программы не закончится память). Вы можете подумать, что разыменование такого указателя может быть безопасным. Это верно лишь до того, пока ваша программа не будет эту память освобождать при окончании времени жизни объектов. После этого у вас окажется указатель с неверным адресом и пиши пропало. И снова стандарт языка не обнадеживает. Из тех значений указателя, которые не следует разыменовывать оператором * он указывает на null указатель, неправильно выравненный с началом объекта адрес, и адрес объекта, после того как последний был удален. Ясно, что C был создан не для слабонервных. Он является низкоуровневым и программисту нужно знать, что и зачем он делает, иначе будут проблемы. Но C++ же не такой? С момента создания C++ старался избавиться наследие C. В него было добавлено множество конструкций, которые закрывали небезопасные возможности языка С. Например, программа "Hello World!" может быть трансформирована в более безопасный вариант. std:cout << "Hello World!" <<> Здесь уже не требуется считать количество передаваемых аргументов, и std::cout достаточно умен, чтобы понять типы передаваемых ей аргументов (хотя множество программистов продолжают использовать printf в C++, из-за более простого написания). В отличие он С, выделение памяти в C++ типизировано и производится при выполнении конструктора объекта (если вы конечно не используете malloc и free). Это хорошая сторона. Однако объекты по прежнему должны быть явно удалены. И после удаления все еще остаются указатели, использование которых, как вы уже можете догадаться, приведет к неопределенному поведению. Когда как в C указатели были очень важны, то в C++ они являются основным механизмом Стандартной библиотеки. Алгоритмы STL используют итераторы, объекты, которые являются указателями сами или имитируют их поведение (и все опасные моменты в том числе). Как и в случае указателей, неправильное использование итераторов приведет к неопределенному поведению (см. пример с swap_range). В то время как в C/C++ имеются проблемы с безопасностью, в языках наподобие Java и C# пошли другим путем. Они либо вообще запретили использование указателей, либо выделили их в специальные блоки с меткой "небезопасный". Управление памятью, которое порождает риск обращения по неверным указателям, скрыт от программиста и производится автоматическая сборка мусора. Кроме этого в них есть еще множество упрощения и средств, повышающих безопасность. К несчатью, все они либо снижают мощность языка или его производительность.

Подмножество безопасного D (SafeD Subset)
В языке D, основная работа выполняется в безопасной области языка, которую мы называем SafeD. Безопасность и простота использования языка D в этом случае сравнима с Java. На самом деле программы на Java можно автоматически транслировать в безопасную часть D. SafeD легок в освоении и избавляет программиста от неопределенного поведения. И он очень эффективен в плане производительности. В SafeD вы не используете указатели, непроверенное приведение типа и union'ы. Управление памятью ложится на сборщик мусора. Объекты передаются с помощью идентификаторов. Выполняется проверка выхода за границы массивов и строк (можно отключить эту возможность с помощью ключа компиляции, но тогда вы уже не в SafeD). Вы можете писать код, который будет выкидывать исключения времени выполнения (т.е. ошибка выхода за границы массива, неинициализированный объект), но вы не сможете перезаписать память, которую выделили или уже освободили. Посмотрим на программу "Hello World!" на языке D. На первый взгляд, различий с C не много: writeln ("Hello Safe World!"); Функция writeln является эквивалентом printf в С (более точно, это представитель семейства функций для вывода, которое включает write и ее версии для форматированного вывода - writef и writefln). Так же как и printf, writeln принимает переменное число аргументов любого типа. Однако на этом сходство заканчивается. Пока вы будете передавать ей SafeD-аргументы, вы можете быть уверены, что не возникнет никакого неопределенного поведения. Здесь writeln вызван одним аргументом типа string. В отличие от C, D строки не являются указателями. Это массив char, и массивы находятся в безопасной части D. Вам может быть интересно, как осуществлена безопасность функции writeln. Один из возможных способов, это сделать функцию обрабатываемую компилятором, чтобы генерировался правильный код. Красота D в том, что он дает современные инструменты для подобных случаев. Из таких инструментов, которые использовались при написании этой функции нужно отметить: Генерирование кода во время компиляции с помощью шаблонов, и Безопасный механизм работы с переменным числом аргументов с помощью кортежей.

SafeD Библиотеки
Основное отличие между Java и D в том, что в D есть достаточно возможностей, чтобы создать бибиотеки для использования в SafeD. Большое количество возможностей D совместимы с SafeD. Например, в библиотеке может быть реализован типизированный список (generic list). Этот список может быть создан для любого типа, в том числе указателя. Список указателей не может быть безопасным по определению. Причем список целых чисел или объектов может и должен быть безопасным, поэтому в SafeD можно использовать списки таким образом, а использование вне безопасного Ди может быть небезопасным. Более того, может оказаться в плане эффективности лучше реализовать список через указатели. И если такая внутренняя реализация хорошо скрыта от пользователя, такая реализация может быть названа совместимой с безопасным Ди.

Опыт одного пользователя
Даже до того, как у меня в голове возникла идея о SafeD2, в своих проектах я пытался ограничивать себя безопасным в использовании подмножеством Ди. Я был удивлен, насколько многого можно при этом сделать и насколько возрастает продуктивность. Я также показал Ди знакомому по работе, программисту С++, и он смог освоить его за очень короткое время. Так по моему опыту можно было сказать, что если написанная на SafeD программа компилируется без ошибок, она будет и работать без ошибок и делать то, чего я от нее хочу. Это определенно не то, с чем я сталкивался при использовании C++. Что еще больше меня удивляет, как я умудрился все это сделать, не имея в наличии подходящего инструментария и получая непонятные сообщения от компилятора. Ди все еще не хватает инфраструктуры, но я могу представить, насколько легко будет на нем программировать, когда появится критическая масса инструментария для разработчика. В отличие от C++, D можно легко парсить и у него открытый фронт енд, что упрощает работу людям, пишущим инструментарий.

Заметки
Сейчас нет какого-либо органа, кто-бы занимался подобной сертификацией, каждый кто создает библиотеки устанавливает свой уровень доверия с клиентами. В частности, вам следует ожидать, что разработчик компилятора обеспечивает совместимость стандартной библиотеки с SafeD. Название SafeD было предложено David B. Held.

Благодарности
Огромное спасибо членам команды разработчиков языка D за ценный отклик и исправления в данной статье.

Оригинал: http://d-programming-language.org/safed.html
Это черновой перевод статьи. Буду еще дорабатывать. Если есть замечания и комментарии, прошу в комменты.