
Эта статья с одинаковым успехом может быть отнесена к блогам ".NET", «Open source» и «Я пиарюсь». После того. как я написал материал, стало ясно, что больше всего в ней «Open source»… Просьба сильно не бить, если я ошибся.
Итак, ниже пойдет рассказ о моем полуторагодичном опыте разработки библиотеки SevenZipSharp с открытыми исходниками, выложенной на CodePlex в феврале 2009. Эта библиотека — обертка над 7-Zip, позволяющая с легкостью его использовать в .NET.
Использование SevenZipSharp
В библиотеке два главных класса — SevenZipExtractor и SevenZipCompressor. Шаблон использования первого:
// Синхронная распаковка
using (var extr = new SevenZipExtractor(@"путь\к\архиву"))
{
extr.Extracting += DoExtractingEvent();
extr.ExtractArchive(@"куда\распаковывать");
DoFinishEvent();
}
// Асинхронная распаковка
var extr = new SevenZipExtractor(@"путь\к\архиву");
extr.Extracting += DoExtractingEvent();
extr.ExtractionFinished += (s, e) => { DoFinishEvent(); extr.Dispose(); extr = null; };
extr.BeginExtractArchive(@"куда\распаковывать");
* This source code was highlighted with Source Code Highlighter.
Шаблон использования второго:// Синхронная упаковка
var cmpr = new SevenZipCompressor();
cmpr.CompressDirectory(@"путь\к\пакуемой\папке", @"имя\архива");
DoFinishEvent();
cmpr = null;
// Асинхронная упаковка
var cmpr = new SevenZipCompressor();
cmpr.CompressionFinished += (s, e) => { DoFinishEvent(); cmpr = null; }
cmpr.BeginCompressDirectory(@"путь\к\пакуемой\папке", @"имя\архива");
* This source code was highlighted with Source Code Highlighter.
Не хочу превращать статью в документацию по SevenZipSharp, так что просто перечислю ее некоторые возможности:
- Полная поддержка многопоточности
- Оборачиваются все форматы архивов, поддерживаемые 7-Zip-ом
- Неприхотливость к оборачиваемой библиотеке — никаких жестких привязок
- Поддержка многотомных архивов при распаковке и упаковке
- Распаковка большинства SFX архивов
- Весь код тщательно снабжен комментариями
Как все началось
В феврале 2009 года мне понадобилось работать с 7-zip архивами в одном из платных проектов, над которым я трудился. Несколько дней я безуспешно искал готовое удобоваримое решение, но не нашел ничего лучше этой статьи на CodeProject. Зато прочитал много сообщений, где люди жаловались на его отсутствие. И тогда, собрав волю в кулак и отталкиваясь от найденной статьи, я
Рассматривался вариант с компиляцией 7-Zip в смешанную сборку (mixed assembly) с флагом /clr. Этот вариант был отвергнут, т.к. во-первых, интерфейс получился бы низкоуровневый и не пригодный для быстрого использования и все равно пришлось писать «добавку», а во-вторых, чтобы собрать код с флагом /clr:pure, требовалось бы переписывать много кода, и unmanaged части все равно остались.
Когда SevenZipSharp только появился, мне хотелось рассказать о нем потенциально заинтересованным пользователям-разработчикам. Я оставлял краткое описание библиотеки везде, где было можно: в ответах на вопросы StackOverflow, на программистских форумах (в т.ч. MSDN, Channel 9), в комментариях к той самой статье на CodeProject и даже на английской Wikipedia. Это все принесло результаты, и вскоре поисковые выдачи Google вышли по траффику на первое место. Думаю, рекламировать свои проекты должны все, иначе большинство попросту не узнает об их существовании. Эффективность рекламы оценивается по статистике загрузок и посещений, которая публично доступна.
SevenZipSharp и 7-Zip
Как многие знают, 7-Zip написан на C++ с небольшим количеством C и пресловутой ассемблерной функцией, вычисляющей CRC32, которую Игорь Павлов реализовал для x86, x86-64 и ARM. Какая либо документация по коду отсутствует, что в стиле русских программистов, участвующих в open source движении. Кода много, он совсем не прост и требуется некоторое время, чтобы разобраться в изобилии define-ов, интерфейсов и классов. Реализации алгоритмов сжатия называются кодеками (Codecs). Кодеки стандартным способом встраиваются в библиотеку, как подключаемые модули протоколов в мессенджерах ala Miranda/Pidgin. Архитектура 7-Zip неотделима от COM; именно это препятствует развитию p7zip — 7-Zip для POSIX систем, которым также занимается Игорь Павлов. В p7zip COM заменен костылем, который симулирует его работу, попутно объявляя половину типов windows.h. Сами алгоритмы написаны безупречно и очень стабильны, однако верхние уровни, как вы уже догадываетесь, оставляют желать лучшего. Если бы автор начал писать 7-Zip сейчас, думаю, он придумал архитектуру ядра более понятную, универсальную и портируемую, в идеале — на языке вроде C# или Java, хоть даже на Python (ну не годятся для этих целей плюсы, чего уж там).
Кстати, 7-Zip для конечных пользовтелей (инсталляция, которая качается с 7-zip.org) собирается Visual Studio 6 образца начала века. Файлы решений успешно преобразовываются в форматы VS2008/2010, и после замены компилятора C/C++ на более новый и активации всех флагов оптимизации (да-да, моя основная профессия — компиляторщик), а также использования профиля достигается ускорение около 15% (LZMA/LZMA2). На заметку…
Вот как SevenZipSharp оборачивает 7-Zip. Через COM-овский CreateObject создается объект, поддерживающий указанный интерфейс (IInArchive, IOutArchive). Из этого объекта дергаются нужные функции, и достигается желаемый результат (например, IInArchive.Extract(...)). Во время длительных операций из unmanaged кода вызываются managed callback-и, и это приводит к проблеме, которую я не сразу осознал — обработка ошибок. Например, из-за ошибки в callback-е или исключении в вызываемом callback-ом пользовательском событии исполнение операции падает без предупреждений и какой-либо вразумительной информации, кроме странного 32-битного кода ошибки. Я решил обернуть все callback-и try/catch-ами и заносить все возникшие исключения в стек ошибок, который в случае неудачи показывается пользователю. Если есть более изящное решение, прошу о нем рассказать.
Попытки
В один прекрасный момент мне захотелось заставить работать SevenZipSharp под Mono (GNU/Linux). И тогда проблема привязанности 7-Zip к COM проявила себя во всем великолепии. Необходимо было заново написать низкоуровневую часть библиотеки почти с нуля. Т.к. код 7-Zip, как я уже писал, специфичный, инструменты для автоматической обертки вроде SWIG оказались бесполезными, а чтобы они вообще заработали, мне пришлось сначала пройтись по всему коду препроцессором и убрать 10-ти этажные define-ы. В настоящее время я потихоньку пишу независимую от COM обертку.
Разработка
Наверное, многие начинающие C# разработчики повторяют одни и те же ошибки, и я не являюсь исключением. Когда мне стало известно о FxCop и StyleCop, я сразу попробовал их использовать. Казалось бы, логично поддерживать код библиотеки в хорошем состоянии. Однако, StyleCop по умолчанию выдает слишком много предупреждений, а настраивать его мне не хотелось, и он был сразу отброшен. FxCop использовался некоторое время, но в один прекрасный момент я поймал себя на мысли, что на элементарные изменения кода я трачу слишком много времени, чтобы соответствовать правилам, которые, по сути ни на что не влияют. Вывод, который я для себя сделал — эти инструменты важны для выработки индивидуального стиля программирования, но разработчикам-одиночкам увлекаться ими не стоит.
Изначально SevenZipSharp писался на Visual Studio 2008 и работал под 2-ым фреймворком. Даже тогда я понимал, что чем меньше версия .NET, тем меньше проблем будет использующим библиотеку. Жаль, что многие разработчики на CodePlex этого не понимают, начинают писать код, используя ультрасвоременные возможности .NET 4, и потом удивляются, почему у них так мало загрузок. Потом я узнал, что в Windows Mobile < 7 есть полноценный COM, и за несколько дней портировал SevenZipSharp на эти мобильные системы. Если кто-то не знает, фреймворки обычные и compact имеют ряд различий, и код без небольших изменений не будет собираться, а тем более работать. Поддерживать две почти идентичные ветки я считал неразумным, и решил эту проблему множественными #if/#else/#endif (стандартный подход в исходниках на C++).
Когда вышел Visual Studio 2010/C# 4, я обнаружил, что фичи новой версии языка можно эффективно применить к коду (например, появившиеся опциональные параметры устраняют 10+ перегрузок единственного логического метода). Для сохранения обратной совместимости я снова применил #if/#else/#endif. Код понемногу стал превращаться из изящных классов и ветвистого монстра. А когда пришла идея портировать SevenZipSharp на Mono, некоторые файлы с кодом я все-таки раздвоил, т.к. иначе сам бы через пару недель не смог в нем разобраться. В итоге, передо мной во всей красе встала проблема поддержки разных платформ и фреймворков в одном единственном файле. Пример:
#if !DOTNET20
/// <summary>
/// Unpacks the whole archive asynchronously to the specified directory name at the specified priority.
/// </summary>
/// <param name="directory">The directory where the files are to be unpacked.</param>
/// <param name="eventPriority">The priority of events, relative to the other pending operations in the System.Windows.Threading.Dispatcher event queue, the specified method is invoked.</param>
#else
/// <summary>
/// Unpacks the whole archive asynchronously to the specified directory name at the specified priority.
/// </summary>
/// <param name="directory">The directory where the files are to be unpacked.</param>
#endif
public void BeginExtractArchive(string directory
#if !DOTNET20
, DispatcherPriority eventPriority
#if CS4
= DispatcherPriority.Normal
#endif
#endif
)
{
SaveContext(
#if !DOTNET20
eventPriority
#endif
);
(new ExtractArchiveDelegate(ExtractArchive)).BeginInvoke(directory, AsyncCallbackImplementation, this);
}
* This source code was highlighted with Source Code Highlighter.
Отмечу, что разрабатывался SevenZipSharp стихийно. Если что-то хотелось добавить к функциональности, я брал и добавлял — и не советовался с начальниками, не согласовывал решения с руководством и т.д. В этом есть свои минусы, но зато баги исправлялись мгновенно и просьбы о новых функциях удовлетворялись в течение нескольких дней. Полная свобода действий — и настоящая учеба на своих ошибках.
Бонусы
От участия в open source проекте на CodePlex появились неожиданные приятные сюрпризы. Во-первых, я заметил, что компания JetBrains, создатель ReSharper, выдает бесплатные лицензии разработчикам программного обеспечения с открытым исходным кодом. Я попытал счастья и не пожалел — мне действительно выдали лицензию. ReSharper оказался незаменимым помощником в написании кода, и я всем его советую. Во-вторых, с недавнего времени CodePlex показывает рекламу на страницах проектов (по желанию их владельцев). Доход от рекламы можно либо жертвовать на благие цели, либо присваивать себе. Я выбрал второй вариант, и получаю около 10$ в месяц. В-третьих, когда SevenZipSharp обрел популярность, мне стали предлагать спонсорство компании, разрабатывающие полезные инструменты для C#/.NET программистов, такие как NDepend и SciTech .NET Memory Profiler. В-четвертых, небольшие деньги приносит кнопка «Donate». Никто пока больше 10$ не жертвовал, но даже это приятно и стимулирует.
Воодушевляет, когда в письмах благодарят за библиотеку, сообщают, что используют ее в реальных и весьма известных проектах (например, Stardock). Иногда мне приходят письма от людей, с предложениями начать работать над библиотекой вместе. Человек обычно полон энтузиазма, уверяет, что вместе будет здорово и т.п. После ответного в письма, в котором я пишу, что сразу никому пароль от SVN не дам, спрашиваю, а что человек вообще умеет, и описываю примерные планы развития проекта на будущее, никто со мной пока не связывался. Мне это кажется странным, возможно, психологию таких людей мне объяснят в комментариях.

Типичное письмо без продолжения
Поскольку речь зашла о людях, расскажу о публике на CodePlex. Несколько раз мне жертвовали код, причем всего один раз — по правилам, через патч. Порой давали дельные советы, подсказывали, что можно сделать лучше. Очень приятно, когда ошибки исправляешь не ты, а другие пользователи, и делятся потом багфиксом. Однако часто о баге заявляют не в Issue Tracker, а в обсуждения (Discussions), даже если баг очевидный. Приходится регулярно вчитываться в вопросы и решать, чей это косяк, библиотеки или криворукого пользователя. Впрочем, иногда появлялись «участники», заводящие сразу с десяток багов, из которых в лучшем случае пара действительно стоящие, а остальные являются просьбами добавить лишнюю функциональность, которая кроме самих «участников» никому не нужна. Бывает, что завел человек баг вроде «не работает распаковка», спрашиваешь у него в комментариях, какая версия библиотеки, как воспроизвести ошибку, а тот уже давно забыл про SevenZipSharp и не отвечает. Совсем.
Отдельно веселят люди, оставляющие оценки (звездочки на CodePlex, от 1 до 5). Бесит, когда ставят 2 и не объясняют почему. Впрочем, также бесит когда ставят 2 и пишут, что мол вообще ничего не работает и библиотека ваша говно. К счастью, с SevenZipSharp это происходит редко, в отличие от других популярных проектов, которые, я уверен, не заслуживают таких оценок.
Итоги
Оглядываясь назад, я вижу, что связался с SevenZipSharp не зря. Получены и бесценный опыт, и некоторые выгоды. Если вы спросите, а стоит ли разрабатывать свой open source проект «для души», я без раздумий отвечу — конечно!
Спасибо за внимание.