*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 26 февраля 2009 г.

Чарльз Уэзерелл, "Этюды для программистов"

Лет пятнадцать назад мне в руки попала книга Чарльза Уэзерелла "Этюды для программистов".

Книга содержит 27 “этюдов”. Каждый этюд – это законченная задача для обучающихся программированию. Удивительно, книге более 30 лет, но любой из этюдов может быть до сих пор использован по назначению. Я сам с удовольствием давал этюды из этой книги студентам.

Тематика задач совершенно разная: ретроспективная задача по программе, которая печатает свой текст, игра Джона Конвея “Жизнь”, игровое и имитационное моделирование, интерпретаторы форматов и форматирование текста, статистический анализ карточных игр, символьные алгебраические вычисления, плавающая арифметика и работа с числами большой разрядности, математические методы взлома шифров, искусственный интеллект, машина Тьюринга, рекурсивные алгоритмы поиска, компрессия данных, эмуляция виртуальной компьютерной архитектуры, связывающий загрузчик, компилятор, интерпретатор символьного а-ля функционального языка и т.д. И все эти задачи времен, когда программировали на фортране с помощью перфокарт!

Удивительно, как человек смог написать столь долгоживущую книгу, легкого и неперегруженного чрезмерной научностью содержания. Я пытался найти какие-то еще книги Уэзерелла, но безуспешно. Может это его единственная книга.

И я бережно храню переведенный на русский экземпляр до сих пор. Не исключено, что эта книга пришлась мне в нужное время, и поэтому она стала моей главной книгой.

С различным успехом, но с огромным азартом, я возился в разные времена со всеми этюдами.

Был интересный эпизод с одним из этюдов. В главе 24, под названием “Секрет фирмы, или Математический подход к раскрытию шифров” описывается задача по взлому шифра Виженера (точнее, одной из его модификаций). В книге давался зашифрованный текст, описывался метод взлома и предлагалось расшифровать секретное послание.

И так меня эта задача торкнула, что я даже списался с одним из технических переводчиков этой книги. Тогда еще мне пришлось делать это по обычной бумажной почте России. У меня была масса вопросов, так как расшифровать предлагаемый в книге пример не получалось (математический анализ я тогда еще не изучал). Мне ответили, и среди всего прочего рассказали, как они сами (те, кто переводил книгу) расшифровывали. Ведь им, чтобы опубликовать это на русском языке, надо было иметь исходное сообщение, но в книге не приводился ответ, так что пришлось засучить рукава и заняться английской шифровкой:

Решить задачу предложенным автором способом не получилось, и наши решили все по-своему (советская математическая школа показала себя), выявив, что метод автора не совсем правильный. В русском варианте в главе 24 есть “партия переводчика”, где описывается “советский” способ решения. Попутно наши переводчики выяснили, что в английском оригинале есть опечатка! Одна из строк шифровки просто пропущена. Уже после перевода они связывались с господином Уэзереллом, и тот подтвердил факт наличия досадной опечатки.

Кстати, в интернете ходит много решений “этюдов” Уэзерелла. Например, программа, играющая в Калах, или интерпретатор символьного интерактивного языка TRAC.

Но теперь к самому главному.

Пару дней назад королевская почта доставила мне то, что я так давно мечтал полистать лично — это английский оригинал этой книги.

Книга была издана аж в 1978 (я тогда даже не ходил пешком под стол, а просто лежал) и более не переиздавалась. Я купил списанный библиотечный экземпляр. Немного потертый, переклеенный скотчем по корешку, с библиотечным кармашком для карточки на обратной стороне обложки. Экстаз, один словом.

Если в мире программирования могут существовать иконы, то “Этюды для программистов” Чальза Уэзеррела одна из них.

Unit-тест для Coredump

Как мне подсказали в комментариях к посту про создание coredump файлов в Windows, если изменить код возврата из функции обработки исключения, то можно подавить появление стандартного окна об ошибке. Посему родился unit-тест для этого модуля.

Модифицированный текст файла coredump.cpp, в котором с помощью макроса UNIT_TESTING встроена поддержка для тестирования. Если этот макрос определен, то, как я уже сказал, подавляется появление окна с ошибкой, и coredump файл создается с постоянным именем.

Файл coredump.cpp:
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

#ifdef UNIT_TESTING
lstrcpy(fname, "___coredump.dmp");
#else
// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);
#endif

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

#ifdef UNIT_TESTING
return EXCEPTION_EXECUTE_HANDLER;
#else
return EXCEPTION_CONTINUE_SEARCH;
#endif
}
Теперь, собственно, тест (файл coredump_unittest.cpp):
#include "gtest/gtest.h"

#include <fstream>
#include <windows.h>
#include <stdlib.h>

TEST(Coredump, CoredumpCreation) {
const char* coredump = "___coredump.dmp";

// На всякий случай заведомо стираем старые файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));

// Создаем файл с тестовой программой.
std::string program = "int main() { *(char *)0 = 0; return 0; }";
std::ofstream os("___coredump_main.cpp");
os << program << std::endl;
os.close();

// Компилируем тестовую программу с опцией UNIT_TESTING.
// С этой опцией coredump файл будет создаваться с постоянным
// именем "___coredump.dmp", и будет подавляется окно с сообщением
// об ошибке.
EXPECT_EQ(
0, std::system(
"cl /Zi /DUNIT_TESTING /Fe___coredump_main.exe"
" ___coredump_main.cpp coredump.cpp dbghelp.lib"
" 1>nul 2>&1"
)
);

// На всякий случая удаляем старый coredump файл.
std::remove(coredump);

// Убеждаемся, что файл действительно удалился.
std::ifstream isdel(coredump);
EXPECT_FALSE(isdel.good());
isdel.close();

// Запускаем тестовую программу.
ASSERT_EQ(0xC0000005, std::system("___coredump_main.exe"));

// Проверяем, создался ли файл coredump.dmp.
std::ifstream is(coredump);
EXPECT_TRUE(is.good());
is.close();

// Удаляем за собой временные файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));
std::remove(coredump);
}
Данный тест имеет ряд существенных недостатков. Во-первых, он использует файловую систему, и во-вторых, он вызывает компилятор, что занимает небольшое, но все же время. Недостатки неприятные, но в целом приемлемые.
Кстати, Google Test Framework умеет делать так называемые "смертельные" (death) тесты. То есть можно протестировать именно аварийное "падение" фрагмента кода, например, из-за нарушения защиты памяти, и для проведения такого теста не надо вручную компилировать что-либо, как мы делали тут. К сожалению, эта возможность основана на использования юниксового системного вызова fork() и поэтому доступна только на UNIX платформах.
Дежурный файл для запуска тестов runner.cpp:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Компилируем, например в Visual Studio 2008:
cl /EHsc /I. /Fecoredump_unittest_vs2008.exe /DWIN32 runner.cpp coredump_unittest.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Coredump
[ RUN ] Coredump.CoredumpCreation
[ OK ] Coredump.CoredumpCreation
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает.

Сразу скажу, я проверял все это только под Windows XP SP2 и Server 2003. Пожалуйста, сообщайте, если есть какие-то проблемы или тонкости под другими виндами.

Как это часто бывает в unit-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.


Другие посты по теме:

среда, 25 февраля 2009 г.

Coredump для Windows

Тривиальная задачка: что будет, если запустить вот такую программу?

Файл main.cpp:

int main() {
*(char *)0 = 0;
return 0;
}
В UNIX'е самым вероятным исходом будет сообщение:
Segmentation fault
Это означает безусловное падение из-за нарушения защиты памяти. Если пользователь разрешил создание coredump файлов (например, для командного интерпретатора bash это делается командой):
ulimit -c unlimited
то после запуска будет создан файл coredump. Этот файл фактически содержит образ памяти вашей программы в момент возникновения ошибки, и его можно открыть в отладчике, например в gdb:
gdb -c coredump
После чего командой bt (back trace) можно точно установить место в программе, в котором произошла ошибка. Естественно, для удобства, программа должна быть скомпилирована с включенной отладочной информацией.
Когда ошибка происходит в процессе отладки у вас на глазах, то проще запустить программу сразу под отладчиком. Но когда, например, ошибка происходит у заказчика, и вас нет рядом, то тогда можно попросить прислать вам coredump файл для анализа. Во многих случаях этого хватает для локализации проблемы.
А что делать под Windows? Запуск приведенной программы под виндами обычно приведет вот к такому сообщению:


Неискушенный пользователь обычно жмет "Don't send", и затем программа благополучно падает, и не останется никакой информации произошедшей ошибке. Конечно, программа может вести подробное журналирование, но часто ошибки подобного рода редко удается локализовать по журналам.

Я слышал, что может так случиться, что после нажатия "Send Error Report" Майкрософт с вами свяжется и поможет решить проблему. Со мной такого ни разу не случалось, увы.
В Windows тоже есть схожий механизм создания “на лету” образов памяти работающего процесса, но для его использования надо немного потрудиться.

Windows предоставляет механизм исключений, чем-то схожий с исключениями в С++ и системными сигналами в UNIX. На С++ эти исключения похожи try-catch синтаксисом, а на UNIX — тем, что можно перехватывать исключительные ситуации (например, ошибки) в программе типа нашей (для UNIX можно было бы перехватить сигнал SIGSEGV и получить управление при возникновении подобной ошибки).

Естественно, сигналы в UNIX используются не только для этого.
Итак, ниже я приведу исходный текст небольшого модуля, который будет создавать аналогичный по смыслу юниксовому coredump'у файл, по которому можно будет установить причину аварийного завершения программы. Все, что нужно — это прилинковать этот модуль в ваш проект. Ничего вызывать специально не надо. Модуль инициализируется автоматически.

Файл coredump.cpp:

#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

return EXCEPTION_CONTINUE_SEARCH;
}
Данный модуль создает статический объект coredumpInitializer, конструктор которого вызывается до функции main(). Конструктор устанавливает специальный обработчик для системных исключений, в котором в файл и записывается образ памяти текущего процесса при возникновении системных ошибок. Имя файла содержит идентификатор процесса, идентификатор потока и текущее время. Этот файл можно открыть в Visual Studio просто запустив его, либо в самой студии в меню File->Open->Project/Solution выбрать этот файл, указав его тип как "Dump files". Далее надо запустить программу через отладчик, нажав F5, и отладчик остановится на месте возникновения ошибки. Естественно, для работы на уровне исходного текста необходимо наличие файла с расширением .PDB, содержащего символьную информацию, в том каталоге, где расположен ваш coredump файл. Файл с раширением .PDB обычно создается при компиляции с включенной отладочной информацией, например, при использования ключа "/Zi".
Надо отметить, что если ошибка в вашей программе произойдет до функции main(), например, при инициализации каких-то статических объектов, то данный модуль может и не сработать, так как вызов конструктора объекта coredumpInitializer может быть запланирован уже после проблемного места. Вообще, статические переменные и объекты — это источник многих проблем, в основном из-за неопределенного порядка их инициализации, и их использования стоит избегать по возможности.
Итак, компилируем:
cl /Zi main.cpp coredump.cpp dbghelp.lib
Для компиляции необходима библиотека DbgHelp.lib. Она обычно входит в состав Windows SDK в составе студии. Также получившийся исполняемый файл main.exe будет использовать динамическую библиотеку DbgHelp.dll. DbgHelp.dll входит в состав так называемых redistributable файлов, то есть которые вы имеете право распространять вместе с программой на случай, если у клиента нет этой dll'ки. Эта dll'ка с большой вероятностью находится у вас в каталоге C:\WINDOWS\system32.
Теперь при запуске программы main.exe и после закрытия пользователем окна с сообщением об ошибке (см. картинку выше), будет создан .DMP файл с именем, начинающийся со слова coredump, например coredump-4584-3240-20090226022327093.dmp. Этот файл уже можно открыть в Visual Studio.

Теперь, когда у заказчика программа падает, можно попросить его прислать такой файл для анализа. Единственное, надо хранить .PDB файлы, соответствующие отданным заказчику исполняемым модулям.

Пока я не придумал, как сделать для модуля coredump.cpp автоматизированный unit-тест. Проблема в том, что тут надо как-то подавить вывод окна с ошибкой (см. картинку выше). Если это сделать, то тест может быть вполне себе автоматизированным.
Более подробная информация вопросу создания образов памяти процесса есть в хорошей статье “Effective minidumps”.


Другие посты по теме:

понедельник, 23 февраля 2009 г.

Статический анализ кода

Все программисты ошибаются, даже самые продвинутые. Хорошим подспорьем в отлове ошибок, которые просмотрел программист, являются анализаторы кода.
Конечно, последней инстанцией будет непосредственно клиент, который непременно сообщит, что программа падает, но всегда хочется минимизировать такие случаи. Хорошо, когда процедура внедрения у заказчика вообще существует. Это позволяет без особых проблем сделать “критическое обновление” уже после релиза. Но в некоторых областях цена допущенных до клиента ошибок крайне высока. Например, для области встраиваемых систем любая минимальная утечка памяти будет фатальной, или, например, для разработчиков игр под игровые консоли будет трудновато “донести” до клиентов критические обновления, необходимость в которых выяснилась сразу после релиза и начала продаж (как это обычно бывает).
Перейдем от слов к делу и рассмотрим конкретный пример работы статического анализатора.

Вот пример "не очень хорошей программы":

01 class A {
02 public:
04 A() {
05 char* __p = new char[10];
06 __p = new char[10];
07
08 char* a = (char *)0;
09 *a = 0;
10
11 char c[10];
12 c[10] = 0;
13 }
14 ~A() {
15 delete __p;
16
17 char* a = new char[100];
18 return;
19
20 delete[] a;
21 }
22 private:
23 char* __p;
24 };
25
26 int main() {
27 A a;
28 return 0;
29 }
Тут без микроскопа видно, что проблем полно:

  • Утечка памяти в строке 05. Указатель __p явно имеет неправильное объявление в виде лишнего char*, которое перекрывает декларацию этого указателя в классе в строке 23. Оператор delete в строке 15 скорее всего закончится аварийно, так как значение __p для него будут неопределенно.
  • Строка 06 присваивает указателю __p адрес вновь распределенной памяти, тем самым затирая старое значение, присвоенное в строке 05, которое будет потеряно.
  • Строки 08 и 09 — это обращение по нулевому указателю, приводящее к нарушению защиты памяти.
  • Строки 12 и 12 — это типичное переполнение буфера (buffer overrun)
  • Память под указателем в строке 17 никогда не будет освобождена. Это утечка памяти.
Достаточно для начала. Не спорю, пример очень вычурный, но ошибки то весьма типичные, а когда они перемешаны с “правильным” кодом, их обнаружение становится серьезной проблемой.

Теперь возьмем "микроскоп".

Посмотрим, что сможет сделать для нас Visual Studio. Начиная с версии 2005 у компилятора cl.exe появился ключ /analyze, который включает дополнительный анализ и вывод предупреждений о потенциальных проблемах. К сожалению, этот ключ есть только в версии студии Team (в Professional его нет).

Компилируем в Visual Studio 2008 Team:

cl /W3 /O2 /analyze /EHsc bad.cpp
Вот, что дает анализ:

c:\sandbox\analyze\bad.cpp(12) : warning C6201: Index '10' is out of valid index range '0' to '9' for possibly stack allocated buffer 'c'

c:\sandbox\analyze\bad.cpp(5) : warning C6211: Leaking memory '__p' due to an exception. Consider using a local catch block to clean up memory: Lines: 5, 6
c:\sandbox\analyze\bad.cpp(9) : warning C6011: Dereferencing NULL pointer 'a': Lines: 5, 6, 8,9

c:\sandbox\analyze\bad.cpp(12) : warning C6386: Buffer overrun: accessing 'c', the writable size is '10' bytes, but '11' bytes might be written: Lines: 5, 6, 8, 9, 11, 12

Не так много, как хотелось бы, но хоть что-то. Переполнение буфера в строке 12 обнаружено. Запись по нулевому указателю в строке 09 тоже найдена. Давайте разберемся с сообщением об утечке памяти. Нам сообщается, что возможна утечка, если в строке 06 произойдет исключение (std::bad_alloc, например), тогда память, распределенная в строке 05 будет потеряна. Это, конечно, проблема, но все-таки суть ошибки передана неверно. Как мне показалось, анализатор в cl.exe работает последовательно, то есть он следует ходу компиляции, отсюда и “последовательный” характер смысла выведенных предупреждений.

Мы в компании для статического анализа используем Coverity Prevent for C/C++. Есть еще похожий продукт — Klocwork. Эти два продукта делают примерно одну и ту же работу примерно с одинаковым результатом. Мы выбрали первый из-за более подходящей нам ценовой политики и более простого встраивания в систему сборки.

Суть анализа, проводимого данными продуктами, это подобие псевдо-компиляции, когда строится синтаксическое дерево разбора и на основе его проводится анализ всех возможных ветвлений программы. Проходя все ветки, анализатор и проводит свои многочисленные проверки. Прикол в том, что анализ может найти проблему в таком закоулке кода, который может выполняется то раз в год (и раз в год программа падает), и может вы сами никогда не видели, как этот кусок программы работает. Анализ на уровне синтаксиса языка позволяет находить парные проблемы, которые могут быть расположены в разных частях исходного текста (например, поиск несоответствий в конструкторе и деструкторе). Также понимание синтаксиса дает возможность анализировать вложенные вызовы, когда, например, неверный указатель "проявляет" себя только двух или тремя уровнями выше.

Программы-анализаторы типа lint (или тот же ключ “/analyze”), которые просто ищут шаблоны "плохого" кода на уровне лексем, обычно выдаются миллиарды предупреждений, из которых только единицы ценны. При таком подходе разработчику быстро надоедает заниматься выуживанием “жемчужин” из общего потока мусора, и он перестает это делать. Анализаторы же в Coverity и Klocwork выдаются крайне точные сообщения, и процент ложных срабатываний крайне мал (по крайне мере на моем опыте). Также, в каждом из этих продуктов можно самостоятельно настраивать анализатор, фокусируя его на специфичных конкретно для вас потенциальных проблемах, отключая ненужные проверки для уменьшения “шума”.

Идея, лежащая в этих продуктах, это дать не просто нечто, генерирующее тонны текстовых файлов, в которых надо копаться вручную. Тут дается целая среда для автоматизации анализа: групповая работа, система интеграции с контролем версий, позволяющая отдельно проверять каждый внесенный кусок кода и моментально локализовывать время, место и автора “проблем”, система рецензирования когда по исправлению ошибок, общая база данных по ошибкам, которая исключает повторный анализ уже исправленных ошибок, так как положение ошибки характеризуется не просто именем файла и номером строки, на контекстом, и поэтому даже когда ошибка “переехала” в другое место, то он не будет заявлена как новая. Обычно время псевдо-компиляции равно времени вашей обычной сборки, а время самого анализа может занимать в среднем в 3-4 раза дольше. Анализатор прекрасно может использовать многоядерные системы для радикального ускорения процесса. Например, мы с интегрировали статический анализ с системой автоматических “ночных” сборок.

Естественно, никакой анализатор — это не панацея, и все 100% ошибок он не найдет, но изрядную долю выловит, позволив вам потратить освободившееся время на внесение новых ошибок.

Кстати, обе эти конторы всегда организуют бесплатный тест-драйв. Можно попробовать, чего такого интересного сможет найти их анализатор в конкретно вашем коде. Честно могу сказать, это производит впечатление даже на самых заядлых зануд и скептиков среди разработчиков и менеджеров. Когда на ваших глазах открывается такое в коде, что волосы дыбом встают, то к этому невозможно остаться равнодушным. Например, мы сопровождаем большое количество так называемого legacy кода, и тут, конечно, статический анализ проявляет себя во всей красе, хотя и новом, объектно-ориентированном и unit-оттестированном коде тоже бывают ошибки. Это человеческий фактор и от него никуда не деться.

Так вот, анализатор Coverity нашел все проблемы в данной маленькой, но очень плохой программе, включая несоответствие распределения памяти в конструкторе, и ее “неправильном” освобождении в деструкторе.
У нас в отделе есть даже специальная копилка, если в твоем коде статический анализатор находит серьезную проблему, типа утечки или какой-нибудь “неприятности” с указателями или памятью, то принято внести в кассу посильную сумму, чтобы ее можно было потратить коллективно при очередном походе в паб. А пабе как-то особенно продуктивно обсуждаются темы типа кто, куда и какую ошибку внес.
Сейчас мы рассмотрели статический анализ кода. Также существует также динамический анализ, когда уже в процессе работы программы специальными средствами производится автоматизированный поиск ошибок. Лично я постоянно использую совершенно волшебный динамический анализатор Valgrind. Valgrind не так удобен, как мне кажется, для полностью автоматизированной проверки и больше подходит, когда надо поймать какой-то конкретный глюк, например, явную утечку памяти, обнаруженную функциональными тестами, но не выявленную статическим анализом.

Отдельной строкой хочу отметить Borland/CodeGear Codeguard, входящий в состав одноименной студии. Данная библиотека может опционально встраиваться борландовым компилятором в код, шпигуя его сотнями проверок на различные утечки, неправильную работу с указателями и прочими неприятностями. Код при этом замедляется в разы и порой делает невозможным отладку вычислительно тяжелых алгоритмов, но вот находимые с помощью Codeguard’а ошибки порой дорогого стоят.

Анализаторы кода (статические или динамические) являются крайне необходимым инструментом. А конкретно, статические, позволяют автоматизировано находится “плохие” места кода, которые проглядели программисты.

Голубая (Борланд) палитра для Visual Studio

В процессе перехода Visual Studio 2008 с Professional на Team System в очередной раз слетели настройки палитры. Так сложилось, годы работы на борланде приучили меня к голубому фону и желтым буквам, и ничего уже поделать нельзя. Как-то давно, покопав в интернете на тему разных палитр для Visual Studio, я пришел вот к такой трехшаговой комбинации, которая за минуту превращает стандартную микрософтовскую белую палитру в 90%-e подобие борландовой.

Menu -> Tools -> Options -> Environment -> Fonts and Colors:
Font -> Fixedsys
TextEditor:
Plain text -> Yellow/Navy
KeyWord -> Lime/(фон оставить по умолчанию)

Конечно, тут есть еще что доработать по мелочам, но лично мне и этого хватает.

А вот для чего я перешел с Professional на Team я расскажу буквально скоро.

суббота, 21 февраля 2009 г.

Спрятать кнопку "Пуск" и помигать клавиатурой

Я с удовольствием коллекционирую всякие программные штучки-дрючки в виде миниатюрных исходников.

Приведу парочку из загашника. Ни разу не претендую на авторство, так что ценителей авторского права просьба сообщать о возможных нарушениях незамедлительно.

Убирание кнопки Пуск в Windows

Данная программа убирает кнопку Пуск (Start) на 5 секунд, а потом возвращает ее назад. Проверял на Windows 2000 и XP. Если программу прервать в отведенные 5 секунд, кнопку Пуск придется восстанавливать повторным запуском программы.

#include <windows.h>
int main(void) {
// Ищем кнопку.
HWND hWnd;
hWnd = FindWindow("Shell_TrayWnd", NULL);
hWnd = FindWindowEx(hWnd, NULL, "BUTTON", NULL);

// Прячем её.
ShowWindow(hWnd, SW_HIDE);

// Ждём.
Sleep(5000);

// Показываем обратно.
ShowWindow(hWnd, SW_SHOW);

return 0;
}
Мигание индикаторами на клавиатуре

Данная программа устраивает бегущий огонек по индикаторам  NUM LOCK, CAPS LOCK и SCROLL LOCK на более менее обычных клавиатурах.

#include <windows.h>

void kbdLight(WORD code) {
INPUT input;

input.type = INPUT_KEYBOARD;
input.ki.wVk = code;
input.ki.wScan = 0;
input.ki.dwFlags = 0;
input.ki.time = 0;
input.ki.dwExtraInfo = 0;

SendInput(1, &input, sizeof(input));

input.type = INPUT_KEYBOARD;
input.ki.wVk = code;
input.ki.wScan = 0;
input.ki.dwFlags = KEYEVENTF_KEYUP;
input.ki.time = 0;
input.ki.dwExtraInfo = 0;

SendInput(1, &input, sizeof(input));
}

int main(void) {
while (true) {
kbdLight(VK_NUMLOCK);
Sleep(100);
kbdLight(VK_CAPITAL);
Sleep(100);
kbdLight(VK_SCROLL);
Sleep(100);
}
return 0;
}
Если у вас есть интересные исходнички подобного рода — прикладывайте в комментарии.

Не все же нам в каких-то конструкторах копаться.

Разница между T() и T

Как совершенно справедливо было замечено в комментариях в посте про разницу между new T() и new T — при объявлении автоматической переменной, а не динамической через new, нельзя использовать скобки, если подразумевается вызвать конструктор по умолчанию. То есть нельзя писать:
T a();
а надо писать:
T a;
так как в первом случае такая запись будет означать декларацию функции a, которая возвращает тип T, а далеко не декларацию переменной класса T с вызовом конструктора по умолчанию.

Не спорю, это очевидно для профессионалов. Для новичков же порой подобная "неочевидная" разница вызывает затруднения, поэтому приведу простейший пример, которые расставит все на свои места.

#include <iostream>
class T {
public:
T() { std::cout << "constructor T()"; }
};

int main() {
std::cout << "T a: ";
// Это синтаксис создания экземпляра класса T с вызовом
// конструктора по умолчанию.
T a;
std::cout << std::endl;

std::cout << "T b(): ";
// А вот это декларация функции "b" без аргументов,
// которая возвращает тип T.
T b();
std::cout << std::endl;
return 0;
}
Данная программа напечатает:
T a: constructor T()
T b():
Видно, что для T b(); никакой конструктор не был вызван. Что в целом и ожидалось.

Использование круглых скобок может быть весьма тонким вопросом в С++.


Другие посты по теме:

пятница, 20 февраля 2009 г.

Разница между new T() и new T

Начнем с new T().

Стандарт говорит нам, что если Т является POD-классом (не объектно-ориентированной сущностью), то объект будет инициализирован значением по умолчанию (обычно, например, для арифметических типов это 0), а если это не POD-класс (явная объектно-ориентированная сущность), то для него вызовется конструктор по умолчанию (либо явный, либо созданный компилятором). Если конструктор по умолчанию задан явно, то будет вызван только он, и вся ответственность за инициализацию ляжет на него. Никой инициализации по умолчанию больше не будет. Если же конструктор по умолчанию не задан явно, и компилятор создал его сам, и в этом случае все члены класса будут проинициализированы неявно: POD-объекты будут проинициализированы нулем, а для не-POD объектов будет проведена инициализация по умолчанию (включая всех его дочерних составляющих — рекурсивный обход всех подобъектов и их инициализация по такому же принципу).

Теперь new T.

В этом случае для POD-объектов вообще не будет никакой инициализации (что было в памяти на момент распределения, то и будет). Для не POD-объекта просто будет вызван конструктор по умолчанию (либо явный, ли заданный компилятором по умолчанию), и не будет проводиться никакой инициализации POD-составляющих этого объекта.

Для простоты, POD-типами (Plain Old Data) является все наследие языка С в С++. Везде, где есть объектно-ориентированная примесь — это уже не POD-класс. Для не POD-классов нельзя делать никаких предположений о внутренней структуре, расположению в памяти и т.д.
Забавно, структура:
struct A {
int b;
};
является POD-типом, а вот если добавить в нее, например, слово public:
struct A {
public:
int b;
};
то по стандарту это не POD-объект, и его нельзя уже трогать на уровне внутреннего представления, например обнулить через memset. Хотя многие компиляторы разрешают такие "игры" с не POD-объектами и, программа может в принципе работать, но это против стандарта, и, конечно, против переносимости программы.

Итак, описание различий весьма путанное, поэтому лучше рассмотреть пример.

Для чистоты эксперимента я буду использовать так называемое распределение памяти с размещением. То есть я вручную указываю, в каком месте памяти должен будет создаваться объект. Это позволит контролировать "непредсказуемые" значения неинициализированной памяти.

Итак, первый пример:

#include <iostream>
#include <cstdlib>

class T {
public:
// Для простоты экспериментируем на однобайтовом типе.
unsigned char n;
};

int main() {
// "Случайная" память для создания объекта.
// Берем с запасом, чтобы уж точно вместить объект класса T.
char p[10240];

// Заполняем память числом 170 (0xAA)
std::memset(p, 170 /* 0xAA */, sizeof(p));
// Создаем объект явно в памяти, заполненной числом 170.
T* a = new (p) T;
std::cout << "new T: T.n = " << (int)a->n << std::endl;

// Заполняем память числом 171 (0xAB)
std::memset(p, 171 /* 0xAB */, sizeof(p));

// Создаем объект явно в памяти, заполненной числом 171.
T* b = new (p) T();
std::cout << "new T(): T.n = " << (int)b->n << std::endl;

return 0;
}
Данный пример выведет:
new T: T.n = 170
new T(): T.n = 0
Видно, что для new T элемент T.n так остался неинициализированным и равным числу 170, которые заполнили память заранее. Для new T() же в свою очередь элемент T.n стал равны нулю, то есть он был проинициализирован. Все, как сказано в стандарте.

Теперь изменим одну маленькую деталь — добавим в класс Т явный конструктор:

class T {
public:
// Явный конструктор.
T() {}
// Для простоты экспериментируем на однобайтовом типе.
unsigned char n;
};
Теперь нас ждет сюрприз. Теперь программа будет выводить следующее:
new T: T.n = 170
new T(): T.n = 171
Получается, что даже при new T() элемент T.n не был более инициализирован. Почему? А потому, что стандарт гласит: если задан явный конструктор класса, то никакие инициализации по умолчанию для POD-объектов не производятся. Раз программист задал конструктор явно, значит он знает что делает, и вся ответственность за инициализацию теперь на его плечах.
Лично для себя я всегда предпочитаю писать new T() хотя бы для единообразия вызова конструкторов. Также я всегда явно инициализирую все POD-объекты вручную в конструкторе или в его списке инициализации. Отсутствие предположений о значении POD-типов по умолчанию и инициализация их принудительно позволяет избежать сюрпризов при смене компилятора.


Другие посты по теме:

четверг, 19 февраля 2009 г.

Исключения в списке инициализации конструктора

Правилом хорошего тона в С++ является использование списка инициализации для вызова конструкторов членов класса, например:
class A { ... };

class B {
public:
B(int n);
private:
A __a;
};

B::B(int n)
: __a(n) // вызов конструктора А() в списке инициализации.
{}
А что произойдет, если в одном из вызовов в списке инициализации произойдет исключение? Например:
class A {
public:
A(int n) {
throw 0; // Конструктор класса А бросает исключение int
}
};

class B {
public:
B(int n);
private:
A __a;
};

B::B(int n)
: __a(n) // Данный вызов бросает исключение
{}
Хотелось бы иметь возможность поймать это исключение и провести "чистку" уже распределенной на тот момент памяти, например:
class P { ... };

class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};

class B {
public:
B();
private:
P* __p;
A __a;
};

B::B()
: __p(new P), // Память для P распределяется до вызова конструктора класса А
__a(0) // Данный вызов бросает исключение
{}
На момент, когда конструктор А бросит исключение, мы уже будем иметь распределенную память под указателем __p и, не обработав исключение, эту память можно потерять.

В С++ есть форма задания try-catch блока на уровне функции. Используя ее, можно переписать пример так:

#include <iostream>

class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};

class P {
public:
P() { std::cout << "P(), constructor" << std::endl; }
~P() { std::cout << "~P(), destructor" << std::endl; }
};

class B {
public:
B();
private:
P* __p;
A __a;
};

B::B()
try
: __p(new P), __a(0) {
} catch (int& e) {
std::cout << "B(), exception " << e << std::endl;
delete __p;
};

int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), exception " << e << std::endl;
}
}
Видно (см. тело конструктора B::B()), что лист инициализации ушел между словом try и началом try-блока, а тело конструктора теперь внутри try-блока (в данном примере оно пустое), а обработчик исключения находится в catch-блоке после тела конструктора. Данный пример сумеет обработать исключение класса А и освободит память из под указателя __p. Данный пример выведет следующее:
P(), constructor
B(), exception 0
~P(), destructor
main(), exception 0
Видно, что деструктор класса P был вызван.

Внимательный читатель заметит, что в функции main() тоже есть try-блок, а последней строкой программа печатает "main(), exception 0", что значит, что исключение было обработано дважды: в теле try-блока конструктора и затем в функции main(). Почему?

Правило гласит: исключение, пойманное в обрамляющем функцию виде try-catch блоке конструктора, будет переброшено еще раз при выходе из конструктора, если конструктор принудительно не сделал это сам, поймав это исключение. Сейчас очень важный момент: если хоть один из членов класса бросил исключение в процессе конструирования, то весь объект принудительно завершает конструирование аварийно с исключением вне зависимости от того, обработано это исключение в конструкторе или нет.
Единственное, что мы тут можем сделать, это "на лету" подправить исключение, брошенное членом класса (например, добавить туда дополнительную информацию). Следующий пример меняет код брошенного классом А исключения:
#include <iostream>

class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};

class B {
public:
B();
private:
A __a;
};

B::B()
try
: __a(0) {
} catch (int& e) {
std::cout << "B(), exception " << e << std::endl;
e = 1; // Меняем код исключения с 0 на 1.
};

int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), exception " << e << std::endl;
}
}
Эта программы выведет следующее:
B(), exception 0
main(), exception 1
Видно, что когда исключение было поймано второй раз, код у него уже не 0 как в оригинальном исключении, а 1.

С конструкторами вроде разобрались. Перейдем к деструкторам.

Деструктор — это тоже функция. К нему тоже применим синтаксис ловли исключения на уровне тела функции, например:

#include <iostream>

class B {
public:
~B();
};

B::~B()
try {
throw 2;
} catch (int& e) {
std::cout << "~B(), exception " << e << std::endl;
}
Поведение ловли исключения в деструкторе на уровне функции схоже с конструктором, то есть исключение, пойманное в catch-блоке на уровне функции будет переброшено автоматически снова при завершении деструктора, если он это не сделал сам, обработав исключение. Например:
#include <iostream>

class B {
public:
~B();
};

B::~B()
try {
throw 2;
} catch (int& e) {
std::cout << "~B(), exception " << e << std::endl;
}

int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), B(), exception " << e << std::endl;
}
}
выведет:
~B(), exception 2
main(), B(), exception 2
то есть исключение, после его обработки в деструкторе было переброшено снова. Конечно, не пойманные исключения в деструкторе являются большим "no-no!" в С++. Принято считать, что не пойманное в деструкторе исключение — это прямой путь к аварийному завершению программы, так как нарушается принцип целостности системы исключений. Если хотите, чтобы ваши программы на С++ работали стабильно, то не допускайте, чтобы исключения “вылетали” из деструктора. Например так:
#include <iostream>

class B {
public:
~B();
};

B::~B() {
try {
throw 2; // Бросаем исключение.
} catch (int& e) { // И тут же ловим его, не пропуская него “на волю”.
std::cout << "~B(), exception " << e << std::endl;
}
}

int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), B(), exception " << e << std::endl;
}
}
Эта программа выведет:
~B(), exception 2
Видно, что исключение не дошло до функции main().

С деструкторами тоже вроде разобрались. Теперь перейдем к обычным функциям.

Технику обработки исключений на уровне функции можно применять для любой функции, а не только для конструктора или деструктора, например:

void f() 
try {
throw 1;
} catch (int& e) {
std::cout << "f(), exception " << e << std::endl;
}
Но целесообразность такого синтаксиса сомнительна, так как пойманное исключение не перебрасывается автоматически снова после окончания функции, как это было в случае с конструктором и деструктором. Программа:
#include <iostream>

void f()
try {
throw 1;
} catch (int& e) {
std::cout << "f(), B(), exception " << e << std::endl;
}

int main(int argc, char* argv[]) {
try {
f();
} catch (int& e) {
std::cout << "main(), f(), B(0), exception " << e << std::endl;
}
}
напечатает только:
f(), B(), exception 1
то есть исключение не было передано дальше, поэтому разумнее было бы просто оформить функцию традиционным образом с помощью try-блока, обрамляющего всё тело функции:
void f() {
try {
throw 1;
} catch (int& e) {
std::cout << "f(), B(), exception " << e << std::endl;
}
}
не внося в форматирование текста лишней каши непривычным положением слов try и catch.
Лично мне кажется, из всего выше написанного, реально для применения только try-catch блок на уровне функции для конструктора. Там это действительно актуально, чтобы не допустить объектов, сконструированных только наполовину и убитых в процессе создания исключением от собственного члена (простите за каламбур).
Выводы

Исключения, брошенные при обработке списка инициализации класса можно поймать в теле конструктора через синтаксис try-catch блока на уровне функции.

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

вторник, 17 февраля 2009 г.

Автоматический мьютекс

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

Часто бывают случаи, когда несколько функций реализуют какую-то единую функциональность, построенную на общем разделяемом ресурсе, защищенном блокировкой. В этом случае каждая функция в начале работы занимает эту блокировку, а на выходе — освобождает ее. Например, методы класса-регистратора системных событий все работают с выходным буфером и используют единую блокировку для синхронизации доступа к нему. Например:

class Logger {
public:
...
void put(const char* str) {
__lock.Lock();
__buffer.push_back(str);
__lock.Unlock();
}
void flush() {
__lock.Lock();
...
__buffer.clear();
__lock.Unlock();
}
...
private:
Mutex __lock;
std::vector<std::string> __buffer;
}
В целом, такой подход является не совсем правильным, так как данные методы могут быть весьма сложными, иметь многочисленные условные операторы, могут генерировать исключения. В этом случае программисту необходимо позаботиться о всех возможных вариантах завершения каждой функции и везде вставить оператор освобождения блокировки:
__lock.Unlock();
Если этого не сделать, то неосвобожденная по какой-то редко возникающей причине блокировка может просто "подвесить" всю программу, так как все остальные функции, работающие с этой блокировкой, более никогда не получат управления.

К счастью, в С++ есть механизм, дающий возможность очень просто избежать подобных проблем, вывозом кода освобождения блокировки при любом варианте завершения функции. Механизм называется RAII (Resource Acquisition Is Initialization). В С++ деструкторы созданных в контексте функции объектов обязательно вызываются перед завершением контекста (попросту говоря, когда функция завершается любым способом). Если возникло непойманное в функции исключение, то в процессе раскрутки стека деструкторы созданных локальных объектов тоже будут вызваны. Отсюда и идея: занимать блокировку в конструкторе созданного в функции локального объекта и затем освобождать ее в деструкторе. Использование такого метода позволило бы изменить приведенный пример так:

class Logger {
public:
...
void put(const char* str) {
AutoLock(__lock);
__buffer.push_back(str);
}
void flush() {
AutoLock(__lock);
...
__buffer.clear();
}
...
private:
Mutex __lock;
std::vector<std::string> __buffer;
}
Объект AutoLock, создаваемый первым в контексте каждой функции, будет занимать блокировку и освобождать ее при закрытии этого контекста.

Идея проста и понятна, а класс, реализующий эту логику еще проще.

Пространство имен ext можно заменить по вкусу на подходящее вам.
Файл autolock.h:
#ifndef _EXT_AUTOLOCK_H
#define _EXT_AUTOLOCK_H

#include "mutex.h"

namespace ext {

class AutoLock {
public:
// Запираем блокировку в конструторе
AutoLock(Mutex& lock) : __lock(lock) {
__lock.Lock();
}

// Освобождаем блокировку в деструкторе
~AutoLock() {
__lock.Unlock();
}
private:
// Защита от случайного копирования
AutoLock(const AutoLock&);
void operator=(const AutoLock&);

Mutex& __lock;
};

} // ext

#endif
Данный класс использует реализацию блокировки (мьютекса) Mutex.

Посмотрим, как оно будет в деле (конечно с помощью unit-тестирования).

Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл autolock_unittest.cpp:
#include "gtest/gtest.h"

#include "autolock.h"
#include "mutex.h"
#include "thread.h"

// Универсальная задержка в миллисекундах для Windows и UNIX
#ifdef WIN32
#include <windows.h>
#define msleep(x) Sleep(x)
#else
#include <unistd.h>
#define msleep(x) usleep((x)*1000)
#endif

// Тестовый поток
class T: public ext::Thread {
public:
// Параметры потока:
// flag - флаг для сигнализации о выполненном действии
// mutex - рабочий объект-блокировка
// timeout - время, которое необходимо подождать после
// установки флага
// val - значение, в которое надо установить флаг
T(volatile int& flag, ext::Mutex& mutex, int timeout, int val) :
__flag(flag), __mutex(mutex), __timeout(timeout), __val(val)
{}

// Функция потока: занять автоматическую блокировку, установить
// флаг, подождать указанное время, освободить автоматическую
// блокировку.
virtual void Execute() {
ext::AutoLock locker(__mutex);
__flag = __val;
msleep(__timeout);
}
private:
volatile int& __flag;
ext::Mutex& __mutex;
int __timeout;
int __val;
};

// Данный тест выполняет параллельно две функции, которые конкурируют
// за одну блокировку. Функция-поток 'a' занимает блокировку, устанавливает
// флаг в 1, ждет 100мс и затем освобождает блокировку. Функция-поток 'b'
// стартует, когда поток 'a' уже занял блокировку, поэтому после старта
// потока 'b' флаг еще некоторое время будет равен 1, пока поток 'a' не
// отпустит блокировку, и затем поток 'b' изменит флаг в 0, получив
// управление ожидания на блокировке.
TEST(AutoLock, ConcurrentCalls) {
volatile int flag = 0;

ext::Mutex mutex;

T a(flag, mutex, 100, 1);
T b(flag, mutex, 0, 0);

// Запускаем поток 'a'.
a.Start();
// Ждем, пока поток 'a' займет блокировку.
// Это случится, когда флаг станет равен 1.
while (!flag);

// Запускаем поток 'b'.
b.Start();
// Ждем немного, чтобы убедиться, что поток запустился
// и дошел до попытки занять блокировку.
msleep(50);

// Так как время задержки в потоке 'a' больше 50мс,
// то флаг все еще равен 1, так как поток 'a' пока не отпустил
// блокировку, не давая потоку 'b' получить управление
// и изменить флаг в 0.
EXPECT_EQ(1, flag);

// Ждем завершения потока 'a' (блокировка должна быть
// отпущена при его завершении.
a.Join();

// Ждем завершения потока 'b', который к своему завершению
// должен обнулить флаг.
b.Join();
EXPECT_EQ(0, flag);
}
Для компиляции нам также понадобятся файлы mutex.h (класс Mutex), thread.cpp и thread.h (класс Thread).

Файл для запуска тестов runner.cpp:

#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем:

Visual Studio:

cl /EHsc /I. /Feautolock_unittest_vs2008.exe /DWIN32 runner.cpp autolock_unittest.cpp thread.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o autolock_unittest_cygwin.exe runner.cpp autolock_unittest.cpp thread.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from AutoLock
[ RUN ] AutoLock.ConcurrentCalls
[ OK ] AutoLock.ConcurrentCalls
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает, что приятно. Тест работает как ожидалось. 
Не забудьте включить файл autolock_unittest.cpp в тестовый набор вашего проекта. Не тратьте время на вылавливание неожиданных глюков тогда, когда вы уже порядком подзабыли, как тут все работает. Пусть ловлей глюков занимается тест, автоматизировано.


Другие посты по теме:

понедельник, 16 февраля 2009 г.

Олег Цилюрик, Егор Горошко, "QNX/UNIX. Анатомия параллелизма"

Олег Цилюрик, Егор Горошко

QNX/UNIX. Анатомия параллелизма

Хоть я в многопотоковом программировании далеко не новичок, я прочитал эту книгу от корки до корки, дважды. С удовольствием цеплялся на каждый абзац, перечитывал, обдумывал примеры, чтобы ничего не пропустить, компилировал их, крутил туда-сюда, сравнивал результаты с книгой, пробовал на разных системах и т.д.

Несмотря на присутствие слова QNX в названии книги, она является прекрасным руководством по программированию потоков в UNIX в целом. Начиная прямо от печки, то есть от определения потока и его порой неверного отделения от термина "процесс", далее, следуя по базовым способам синхронизации потоков (блокировки, семафоры, условные переменные), детально разжеваны все тонкости поточного программирования с точки зрения идей и концепций, и в конкретном применении их в потоках POSIX (pthreads), включая нестандартные расширения различных версий.

Отдельной строкой укажу на отличную главу про взаимодействие механизма сигналов UNIX'а и, собственно, потоков. Это весьма сложная тема, но тут она понятно расписана. Даны рекомендации “как надо” и “как не надо”. 

Полно показательных примеров на С++. Причем примеры не просто призваны показать типа "о! работает в потоке!". Примеры демонстрируют особенности, проблемы потоков, позволяют оценить ресурсоемкость различных приемов. Везде авторы дают рекомендации типа как ускорить данный кусок кода, как его упростить, как сделать его надежнее, как его тестировать.

Даются сравнительные оценки работы потоков в различных UNIX'ах, например, QNX против Linux. Все плюсы и минусы обстоятельно, без эмоций разобраны. В конце книги рассматриваются некоторые чисто QNX'овые возможности: пулы потоков, и как QNX избавляет программиста от огромного количества головной боли при их использовании, методика программирования сервисов на основе сообщений (для QNX это вообще родная тема благодаря микроядру).

Авторы совершенно точно смогли сконцентрироваться именно на теме многопоточности, не тратя место в книге (всего ~280 страниц) на смежные вопросы, предоставив для “открытых” вопросов отличную библиографию.  

Есть книги, которые, как говориться, прочитал, передай другому, а некоторые уже не хочется никому отдавать. Это одна из таких немногих книг.

Редактор переменных окружения для Windows

Поднимите руки те, кому удобно пользоваться стандартным способом редактирования переменных окужения в Windows XP? Особенно когда, например, редактируешь длиннющую строку PATH в поле ввода длиной десять сантиметров. Я руку не поднимал, так как мне не нравится, радикально не нравится.

Есть замечательная программа Rapid Environment Editor (RapidEE). Она позволяет очень удобно редактировать переменные окружения Windows в виде двух панелей: слева системные переменные, справа пользователькие. Переменная PATH автоматически представляется в виде списка путей. И даже то, что всю картину переменных видно как на ладони, и то, что программа понимает вставку из буфера обмена — все это меркнет перед гениальной функцией подсветки "мертвых" путей в переменной PATH. "Мертвым" путь может быть в основном по двум причинам: либо путь остался от программы, которая давно снесена, либо путь просто задан неверно, а вы битый час пытаетесь понять, почему что-то там не запускается. RapidEE моментально решает подобные проблемы.

Я не сторонник графического интерфейса, и чего греха таить, люблю командную строку. В свое время я написал небольшой скрипт, который в Windows распечатывает пути из переменной PATH по отдельности:

Файл splitpath.cmd:
@echo off 
set line=%path%
setlocal
:parse_line
for /F "delims=; tokens=1,*" %%a in ("%line%") do (
echo %%a
set line=%%b
)
if "%line%" NEQ "" goto parse_line
endlocal
Примерный результат его работы выглядит так:
C:\WINDOWS\system32
C:\WINDOWS
C:\WINDOWS\System32\Wbem
c:\Python25
C:\Program Files\CodeGear\RAD Studio\5.0\bin
C:\Program Files\Java\jdk1.6.0_04
C:\Program Files\Java\jdk1.6.0_04\jre\bin
C:\Program Files\PC Connectivity Solution
c:\oracle\9.2.0.1\bin
C:\Program Files\Oracle\jre\1.3.1\bin
C:\Program Files\Oracle\jre\1.1.8\bin
C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322
C:\PROGRA~1\Borland\CBUILD~1\Bin
Но после перехода на RapidEE нужда в скрипте отпала совсем.

Вывод: RapidEE очень удобная программа для редактирования переменных окружения в Windows. Программа бесплатная. Может работать в portable режиме и не требовать установки.

Скриншот (если на него кликнуть, то будет картинка побольше):


Скриншот Rapid Environment Editor

воскресенье, 15 февраля 2009 г.

std::min() и std::max() в Visual Studio

Простейший кусок кода (файл minmax.cpp):
#include <algorithm>
int main() {
int a = std::min(10, 20);
return 0;
}
Все тривиально и отлично компилируется и в Visual Studio, и в CodeGear/Borland Studio, и Cygwin. Но допустим потребовались какие-то функции из Windows API, и вы подключили файл windows.h:
#include <algorithm>
#include <windows.h>
int main() {
int a = std::min(10, 20);
return 0;
}
Теперь компиляция в Visual Studio (я проверял в 2005 и 2008) будет падать со следующей ошибкой:
minmax.cpp
minmax.cpp(4) : error C2589: '(' : illegal token on right side of '::'
minmax.cpp(4) : error C2059: syntax error : '::'
Постановка #include <windows.h> до #include <algorithm> проблемы не решает.
Очевидно, проблема в том, что кто-то переопределил значение слова min. Запустим препроцессор и проверим догадку:
cl /P minmax.cpp
И что мы видим? А видим мы следующее (фрагмент файла minmap.i):
#line 7 "minmax.cpp"
int main() {
int a = std::(((10) < (20)) ? (10) : (20));
return 0;
}
Естественно, это каша с точки зрения синтаксиса, и компилятор ругается совершенно законно.

Покопавшись в заголовочных файлах Windows SDK, в файле WinDef.h, который косвенно подключается через windows.h, я нашел корень зла:

#ifndef NOMINMAX

#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif

#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif

#endif /* NOMINMAX */
Вот теперь ясно, что делать — надо определить макрос NOMINMAX, тем самым заблокировать определение min и max:
#define NOMINMAX
#include <algorithm>
#include <windows.h>
int main() {
int a = std::min(10, 20);
return 0;
}
Вот теперь в Visual Studio все нормально компилируется.

Забавно, что в Cygwin и CodeGear/Borland исходный пример компилируется без проблем. В борландовой версии windows.h я нашел вот такой фрагмент:

#if defined(__BORLANDC__)
...
# if defined(__cplusplus)
# define NOMINMAX /* for WINDEF.H */
...
# endif
...
#endif /* __BORLANDC__ */
Эдак они заранее оградились от проблемы, принудительно запретив проблемные макросы.

Вывод: Порой промежуточные результаты работы препроцессора являются крайне полезной информацией.

На всякий случай напомню, как его запускать для перечисленных мной компиляторов:

Visual Studio:

cl /P имя_исходника.cpp
Borland/CodeGear Studio:
cpp32 имя_исходника.cpp
Cygwin:
cpp имя_исходника.cpp
Прочие флаги командной строки должны повторять флаги при обычной компиляции. Для препроцессора важны определения макросов (обычно это флаги -D и -U) и пути для поиска включаемых файлов (обычно это флаг -I).


Другие посты по теме:

суббота, 14 февраля 2009 г.

Павел Агуров, "Интерфейс USB. Практика использования и программирования"

Павел Агуров

Интерфейс USB. Практика использования и программирования

Очень целостная книга про USB от электрических основ и применяемых микросхем до написания драйверов под Windows. Я прочитал книгу на одном дыхании, и не пожалел ни капли о потраченном времени. Правда, если быть честным, то последние главы про написание драйверов под Windows я уже просматривал по диагонали, ехидно хихикая про себя на тему “как же можно было усложнить написание драйверов под винды…” и почему в libusb так просто и понятно даже с нуля, а в Windows DDK проще использовать всякие конструкторы драйверов для радикального сокращения времени “начального вхождения” в тему. Но это мои личные тараканы.

Прочитав книгу вы как минимум точно будете знать почему конкретно нельзя два компьютера просто взять и соединить обычным USB кабелем. Я, например, со своим программистским сознанием недоумевал раньше, мол почему если принтер можно подсоединить к компьютеру по USB, то почему же нельзя вместо принтера поставить другой компьютер, написав для него программу по аналогии с принтерной прошивкой, и организовать тем самым мини сеть? Это же просто вопрос драйверов (я так думал)… А тут меня заставляют покупать какой-то хитрый кабель с логикой внутри…

В общем, за себя могу сказать — я на капельку поумнел, что приятно.

А если серьезно, то прочитав эту книгу, можно спокойно самостоятельно “набросать” USB-устройство и написать для него драйвера по Windows.

Жалко, что в книге рассмотрено написание USB драйверов только под Windows. Было бы интересно написать один и то же драйвер под Windows и Linux, например, и оценить трудозатраты.

Шестнадцатеричная печать в STL поток

Когда-то очень давно я написал элементарный манипулятор для шестнадцатеричной печати в стандарный поток. Все просто и тривиально. Но тем не менее я заметил, что таскаю этот микрокласс почти в кажный проект, где нужна отладочная печать. Обычно для шестнадцатеричной печати надо указывать сразу несколько итераторов, типа:
std::cout << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << 0xAA;
Причем std::setw() надо повторять для каждого нового выводимого элемента. Я свел все это в один итератор, чтобы можно было просто написать (указав итератору ширину выводимого поля):
std::cout << ext::Hex(2) << 0xAA;
Итак, класс Hex (название пространства имен можно подкрутить по вкусу), файл hex.h:
#ifndef _EXT_HEX_H
#define _EXT_HEX_H

#include <iostream>
#include <iomanip>

namespace ext {

class Hex {
public:
Hex(int width) : __width(width) {}
friend std::ostream& operator<< (std::ostream& os, const Hex& hex);
private:
int __width;
};

inline std::ostream& operator<< (std::ostream& os, const Hex& hex) {
std::hex(os);
std::uppercase(os);
os.width(hex.__width);
os.fill('0');
return os;
}

} // ext

#endif // _EXT_HEX_H
Теперь можно писать так:
std::cout << ext::Hex(0)  << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0xaa << std::endl;
std::cout << ext::Hex(2) << 0xaa << std::endl;
std::cout << ext::Hex(4) << 0xaa << std::endl;
std::cout << ext::Hex(8) << 0x0a << std::endl;
std::cout << ext::Hex(16) << 0x0a << std::endl;
std::cout << ext::Hex(32) << 0x0a << std::endl;
И результатом будет:
A
A
AA
AA
00AA
0000000A
000000000000000A
0000000000000000000000000000000A
На всякий случай, unit-тест. Чтобы не было сюрпризов при обновлении компилятора, STLport или чего-то еще. Тест всегда проверит, работает ли класс так, как вы от него ждете. Вы можете возразить — ну класс-то выеденного яйца не стоит, а тут для него тесты... Соглашусь. А еще я соглашусь, что сотни раз самые казалось бы ненужные на первый взгляд тесты для "очевидных" классов помогали обнаружить глюки на новой версии системных библиотек, новой версии компилятора, использовании "более мощных" параметров оптимизации и т.д. Время на написание тестов всегда окупается сполна, всегда.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл hex_unittest.cpp:
#include "gtest/gtest.h"
#include "hex.h"
#include <sstream>

void testHex(int n, int w, const std::string& etalon) {
std::stringstream fmt;
fmt << ext::Hex(w) << n;
EXPECT_EQ(etalon, fmt.str());
}

TEST(HexManip, Generic) {
testHex(0x0A, 0, "A");
testHex(0x0A, 1, "A");
testHex(0xAA, 1, "AA");
testHex(0xAA, 2, "AA");
testHex(0xAA, 4, "00AA");
testHex(0xAA, 8, "000000AA");
testHex(0xAA, 16, "00000000000000AA");
testHex(0xAA, 32, "000000000000000000000000000000AA");
}
Ну и головная программа:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем.

Visual Studio:

cl /EHsc /I. /Fehex_unittest_vs2008.exe runner.cpp hex_unittest.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o hex_unittest_cygwin.exe runner.cpp hex_unittest.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from HexManip
[ RUN ] HexManip.Generic
[ OK ] HexManip.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает как положено.
При использовании Hex у себя в проекте не забудьте включить файл hex_unittest.cpp в ваш набор unit-тестов. Оберегите себя от ненужной траты времени в будущем.
Под занавес пара слов о производительности. Очевидно, что если вы выводите в поток десятки тысяч шестнадцатеричных чисел подряд, то разумнее будет использовать стандартные итераторы — настроить поток с помощью std::hex, std::uppercase и std::setfill(), а потом вызывать только std::setw() для каждого нового элемента. Но если вы печатаете разнородные данные, что часто требуется при отладке, то тогда итератор Hex будет в самый раз.


Другие посты по теме:
Тема про параметризированные параметры шаблонов получила продолждение. Один из читателей объяснил, почему пример не компилировался в Visual Studio. Обновленный вариант кода теперь работает во все трех опробованных мной компиляторах: g++, bcc32 и cl.

пятница, 13 февраля 2009 г.

Шаблоны как параметры шаблона

Есть в шаблонах С++ интересная возможность параметризировать сами параметры шаблонов. Чтобы это могло значить?

Бывают случаи, когда параметр шаблона сам является шаблонным классом и для его инстанцирования тоже нужно указать параметр. Например, универсальная шаблонная функция для печати стандартного контейнера любого типа в поток:

template< typename C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
И все бы ничего, но с только зрения синтаксиса С++ это неверно. Нельзя просто написать C<E>, если E сам является не определенным типом, а параметром шаблона. Правильный способ использования параметра шаблона, который в свою очередь зависит от другого параметра, должен выглядеть так:
template< template<typename> class C, typename E >
void print(const C<E>& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
Теперь полный пример (файл template_parameter.cpp):
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
#include <list>
#include <deque>

// Я обычно не использую пространства имен "по умолчанию", но тут
// это сделано для компактности примера.
using namespace std;

// Вся изюминка тут: template<typename> или template<class>.
// Без этого параметр шаблона "С" нельзя будет параметризировать.
// в конструкции C<E>&.
template< template<typename> class C, typename E >
// Тут происходит параметризация параметра "С" параметром "E".
// Без этого класс "С" не может быть использован, так как "E"
// является не просто типом, а тоже параметром шаблона.
void print(const C<E>& v) {
// Так как класс элемента контейнера "Е" нам тут нужен как отдельный
// тип, то для этого и затеяна вся тема с параметризированными
// параметрами шаблона.
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}

// Тестовая программа демонстрирует, как одна функция print()
// может использоваться для печати любого контейнера
// (если, конечно, он удовлетворяет требованиям алгоритма
// copy() по наличию должных итераторов), содержащего элементы
// любого типа.
int main(int argc, char* argv[]) {
// Массив целых.
int i[5] = { 1, 2, 3, 4, 5 };
// Создаем вектор, состоящий из целых, и печатаем его.
print< vector, int >( vector<int>(i, i + 5) );

// Массив вещественных.
float f[5] = { .1, .2, .3, .4, .5 };
// Создаем вектор, состоящий из вещественных, и печатаем его.
print< vector, float >( vector<float>(f, f + 5) );

// Массив символов.
char c[5] = { 'a', 'b', 'c', 'd', 'e' };
// Создаем деку, состоящую их символов, и печатаем ее.
print< deque, char >( deque<char>(c, c + 5) );

// Массив строк в стиле С.
char* s[5] = { "a1", "b2", "c3", "d4", "e5" };
// Создаем список, состоящий из строк, и печатаем его.
print< list, string >( list<string>(s, s + 5) );

return 0;
}
Компилируем.

Cygwin:

g++ -o template_parameter_cygwin.exe template_parameter.cpp
или в Borland/Codegear Studio 2007:
bcc32 /etemplate_parameter_cg2007.exe template_parameter.cpp
И запускаем скомпилированный файл:
1  2  3  4  5
0.1 0.2 0.3 0.4 0.5
a b c d e
a1 b2 c3 d4 e5
Отчетливо видно, что на первой строке распечатаны целые, на второй вещественные, на третьей символы, и на четвертой строки.
Вы спросите, где компиляция в Visual Studio? А вот с ней вышел облом. Я пробовал скомпилировать этот пример в Visual Studio 2005 и 2008, и в обоих случаях я получал ошибки типа:
template_as_parameter.cpp(38) : error C3208: 'print' : template parameter list for class template 'std::vector' does not match template parameter list for template template parameter 'C'

Из чего я сделал вывод, что микрософтовский компилятор не поддерживает подобный синтаксис.

Я был очень расстроен подобным фактом, так как в целом очень положительно отношусь к cl.exe. А тут выходит, что даже борландовый компилятор это понимает, а cl.exe нет. Если кто знает, может есть ключик какой секретный для включения поддержки "хитрых и редких" возможностей С++ в компиляторе микрософта — научите, пожалуйста. Буду очень признателен.
Предвосхищу вопросы типа "зачем так сложно, да еще и плохо переносимо" — все верно. Лично я бы отнес все выше описанное к "темным углам" С++, но уж больно интересно по ним полазать.

Обновление
Комментарий Александра прояснил ситуацию с проблемой при компиляции в Visual Studio. Окончательный вариант кода, чтобы работало в cl.exe, таков:
template< template<typename, typename> class C, typename E >
void print(const C<E, allocator<E> >& v) {
copy(v.begin(), v.end(), ostream_iterator<E>(cout, " "));
cout << endl;
}
У шаблонов стандартных контейнеров есть второй параметр, так называемый allocator. Этот параметр часто используется со значением по умолчанию, поэтому редко приходится вспоминать о нем. И как уточнил Александр, моя проблема была в том, что cl.exe требует явного указания наличия этого параметра при параметризации параметра C.

Исправленный код компилируется во всех опробованных компиляторах, теперь включая и cl.exe.


Другие посты по теме:

четверг, 12 февраля 2009 г.

Универсальная информация об ошибках в Windows и UNIX

Достоверная информация об ошибках во время исполнения программы является залогом простой ее эксплуатации и поддержки. Вместо выслушивания от клиента стенаний на тему "я тут что-то нажал... а тут все не работает..." можно просто попросить его прислать файл журнала программы (log), и с большой вероятностью этого будет достаточно для локализации проблемы. С логическими ошибками бизнес логики программы все понятно — тут все зависит от вас, и вы точно знаете, какая ошибка произошла. Хуже обстоит дело с ошибками системными. Тут надо максималько точно опросить систему, что произошло, и по возможности, получить расшифровку когда ошибки.

Как я уже писал, я работаю одновременно на совершенно разных платформах — от виндов до встраеваемого QNX'а. Поэтому мне всегда хочется иметь максимально простые и переносимые исходники (и желательно с минимумом нестандартных зависимостей), чтобы модули можно было просто кидать из проекта в проект, с платформы на платформу, не допиливая каждый раз что-то напильником.

Итак, привожу ниже класс, который я использую для получения информации об ошибке, произошедшей в операционной системе. Можно узнать код ошибки и его текстовое объяснение, если оно предоставляется. Это не бог весть какой сложный и оригинальный класс, но у меня он работает без каких-либо "допиливаний" на Windows 32- и 64-бит, Linux 2.6 32- 64-бит SPARC и Intel, Sun OS 5.10 SPARC и Intel, AIX, HP-UX и HP-UX IA64. К тому же, этот класс безопасен для мультипотокового использования (что лично для меня, например, очень важно).

Итак, класс SystemMessage. Все члены статические, так что можно работать с ними без создания экземпляра класса.
Пространство имен, как обычно, ext, так что измените, если необходимо.
Файл systemmessage.h:
#ifndef _EXT_SYSTEM_MESSAGE_H
#define _EXT_SYSTEM_MESSAGE_H

#include <string>

namespace ext {

class SystemMessage {
public:
// Эта функция возращает код ошибки.
static int code();
// Эта функция по коду ошибки возвращает ее текстовое описание, если
// таковое предоставляется операционной системой. Если нет, то
// возвращается строка "?".
static std::string message(int code);
};

} // namespace ext

#endif // _EXT_SYSTEM_MESSAGE_H
Файл systemmessage.cpp
#include "SystemMessage.h"

#ifdef WIN32
#include <windows.h>
#else
#include <string.h>
#include <unistd.h>
#include <errno.h>
#endif

namespace ext {

int SystemMessage::code() {
#ifdef WIN32
return GetLastError();
#else
return errno;
#endif
}

// Если система по какой-то причине не имеет функции strerror_r,
// то придется лазить напрямую в таблицу сообщений об ошибках.
// Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R.
// Пока я видел такое только на HP-UX IA64 v2.
#ifndef WIN32
#ifndef LIBC_NO_STRERROR_R
extern "C" int sys_nerr;
extern "C" char* sys_errlist[];
#endif
#endif

std::string SystemMessage::message(int code) {
char msg[1024];

#ifdef WIN32

// Версия для Windows
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
msg,
sizeof(msg) - 1,
NULL
);

char* p = msg + strlen(msg);

// Обрезаем c конца '\r', '\n' и '.'
for(p = msg + strlen(msg) - 1;
p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p)
*p = 0;

#elif LIBC_NO_STRERROR_R

// Если UNIX-платформа не имеет функции strerror_r, то делаем ее
// работу вручную. Пока я встретил такое только на HP-UX IA64 v2.
if (code < 0 || code >= sys_nerr)
return "?";

strncpy(msg, sys_errlist[code], sizeof(msg) - 1);

// Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0'
// не будет скопирован, поэтому добавляем его вручну.
msg[sizeof(msg) - 1] = 0;

#else

// Для нормальной UNIX-системы просто вызываем strerror_r.
if (strerror_r(code, msg, sizeof(msg) - 1) < 0)
return "?";

#endif

// Поможем компилятору по возможности оптимизировать
// возвращаемое значение как rvalue.
return std::string(msg);
}

} // namespace ext
Теперь посмострим это в работе.
Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах — поделитесь, пожалуйста.
Тестовая программа systemmessage_test.cpp:
#include <iostream>
#include <fstream>

#include "systemmessage.h"

int main(int argc, char* argv[]) {
// Пытаемся открыть заведомо несуществующий файл.
std::ifstream is("__non_existing_file__");

// Печатаем ошибку.
int error = ext::SystemMessage::code();
std::cout
<< error << ", "
<< ext::SystemMessage::message(error)
<< std::endl;

return 0;
}
Компилируем в Visual Studio:
cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_vs2008.exe:
2, The system cannot find the file specified
Получили примерно ожидаемое виндовое сообщение об ошибке.

Теперь компилируем в Cygwin:
g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_cygwin.exe:
2, No such file or directory
Получили сообщение об ошибке в стиле UNIX.
Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.
И небольшая ремарка. В мире UNIX существует два диалекта функции strerror_r: XSI-версия (когда определен макрос _XOPEN_SOURCE, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int'а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE в значение 600 (-D_XOPEN_SOURCE=600 для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.

среда, 11 февраля 2009 г.

Двухпанельные файловые менеджеры

Пока в персонально-компьютером мире имя Питера Нортона, прочно ассоциированное с программой Norton Commander, еще не забыто, особенно на территории бывшего СССР.
Интересно, что настоящим автором Нортон Коммандера является далеко не сам Питер Нортон, а John Socha. Именно он создал изначальную версию, которая называлась VDOS (Virtual DOS), еще будучи студентом. Следуя именно идее “виртуального ДОСа”, первая версия коммандера имела файловые панели только на половину экрана — вторая нижняя часть была "виртуальным окном" в ДОС. Сейчас это окно обычно сокращают до одной командной строки. Затем Джон Сόха присоединился к Peter Norton Computing, и программа начала свое триумфальное шествие по планете под именем Norton Commander. Последней версией коммандера, вышедшей из под рук Джона, была версия 3.0. Именно она является "классической" и именно её знало большинство российских пользователей IBM PC. Но эта версия была лебединой песней коммандера. Питер Нортон продал компанию Symantec'у, а Джон Сόха уволился, прекратив работу над коммандером. После версии 3.0 в Symantec выпустили еще пару версий, но время уходило. Оригинальный интерфейс версии 3.0 был "улучшен", программа заметно потолстела и замедлилась, а на пятки уже наступали Дос Навигатор и Волков Коммандер, а в новом 32-битном мире — первые версии FAR и Total (Windows) Commander.

Вообще, история создания эпохального файлового менеджера весьма интересна.

На мой взгляд, именно представление файловой структуры в виде двух панелей с возможностью адресовать файлы с одной панели на другую, дополненная мгновенным редактором и просмотрщиком, является наиболее удобной для программистской работы, когда необходимо прыгать между десятком файлов одновременно, чего-то временно скопировать/переименовать/удалить, тут что-то быстренько отредактировать, в другом месте поискать и т.д. С трудом могу представить все эти действия через однопанельный файловый а-ля explorer, где только для копирования файла надо сначала мышкой на него указать, взять "на копирование", открыть второй explorer с местом назначения и сделать туда "Paste". Например, замечаю на собой постоянно, если мне надо подправить какие-либо настройки проекта в Visual Studio (например, пути), то мне гораздо быстрее переключиться в FAR, там через F4 открыть файл проекта, найти нужное место и исправить прямого в тексте конфигурации, чем ползать через окошки и менюшки самой IDE. Чего уж говорить об удобстве встроенного редактора, особенно если установлена подсветка синтаксиса. Я много раз пытался заставить себя по F4 вызывать внешний редактор, типа Notepad++, но все равно скатывался до встроенного, ибо он вызывается мгновенно. Notepad++, конечно, отличная программа, но в ней я только делаю “сложную” поиск-замену, когда нужны регулярные выражения.
Мир UNIX'а тоже не обделен хорошими двухпанельными файловыми менеджерами, реально ускоряющими процесс, когда надо сотни раз повторять cd/ls/cp/mv/cd/ls/cp/mv..., разруливая какой-нибудь завал на файловой системе. Midnight Commander знают все. Радостно, что за последние несколько месяцев снова началась активная работа над проектом, знамя которого лежало без движения уже несколько лет. Но есть еще один заслуживающий внимания проект. Неутомимый Сергей Вакуленко создал и развивает Bash Commander. Сейчас это патч к официальной версии Bash, но Сергей бьется за включения его в общее дерево. Этот патч ненавязчиво добавляет в Bash то, что там так не хватает: при нажатии на Ctrl-O (^O) появляются заветные две панели. То есть пока идет неспешная программерская работа в стиле make/vi, то все как обычно в командной строке шелла, но как только надо муторно повозиться с множеством файлов, то нажимаем Ctrl-O, и далее привычные панели с клавишам F1-F10. Интерфейс, конечно, крайне минималистический и не имеет всех наворотов MC, но есть один плюс - крайне простая сборка. Везде, где собирается Bash, там можно собрать и Bash Commander, чего нельзя сказать MC с длинным списков зависимостей, который просто собирается разве что под линуксом, а вот на динозаврах типа AIX'а или HP-UX'а становится грустно. Функциональность Bash Commander’а легко расширяется при помощи самих же скриптов на Bash. Надеюсь, что работу Сергея таки включат в официальную версию Bash.
Мне кажется, что наш постсоветский IBM-PC'шный мир привил некоторым из нас не самую плохую привычку к двухпанельным текстовым файловым менеджерам, не так ли?

вторник, 10 февраля 2009 г.

Игры в числа

Наверняка многие знают, что в эту пятницу, а именно 13.02.2009 в 23:31:30 по Гринвичу так называемое UNIX-время, по которому работает большинство правильных операционных систем, достигнет отметки в 1234567890 секунд. Также, это будет пятница 13-е!
К сожалению для Москвы этот день будет не столь особым, так как там это произойдет уже 14-го в 02:31:30.
Это, наверное, как наблюдать затмение, в некотором цифровом виде.

Для отслеживания обратного отсчета сего события есть забавная страничка с говорящим адресом: http://coolepochcountdown.com/

Всегда любил игры в числа.