Как только вы переступаете через болевой порог Борроу-Чекера и осознаёте, что Rust позволяет вытворять невообразимые (и порой опасные) в других языках вещи, вас может постигнуть настолько же непреодолимое желание Переписать Всё на Rust. Хоть и в лучшем случае это банально непродуктивно (бессмысленное разбазаривание усилий на несколько проектов), а в худшем — приводит к уменьшению качества кода (ведь с чего вы считаете себя более опытным в области применения библиотеки, чем её изначальный автор?) Гораздо полезнее будет предоставить безопасный интерфейс для оригинальной библиотеки, повторно используя её код. • Первые шаги
• Собираем chmlib-sys
• Пишем безопасную обёртку на Rust
  • Поиск элементов по имени
  • Обход элементов по фильтру
  • Чтение содержимого файлов
• Добавляем примеры
  • Оглавление CHM-файла
  • Распаковка CHM-файла на диск
• Что дальше? Для начала стоит разобраться в том, как изначально задумывалась работа с библиотекой. Мы будем работать с CHMLib, библиотекой на Си для чтения файлов Microsoft Compiled HTML Help (.chm). Начнём с создания нового проекта и подключения CHMLib в виде git-подмодуля: После этого глянем, что там внутри, с помощью tree: Похоже, библиотека использует GNU Autotools для сборки. Это нехорошо, потому что всем пользователям крейта chmlib (и их пользователям) потребуется устанавливать Autotools. Файлы lzx.h и lzx.c содержат реализацию алгоритма сжатия LZX. Вообще лучше было бы использовать какую-нибудь библиотеку liblzx, чтобы получать обновления бесплатно и всё такое, но, пожалуй, проще будет тупо скомпилировать эти файлы. enum_chmLib.c, enumdir_chmLib.c, extract_chmLib.c, похоже, являются примерами использования функций chm_enumerate(), chm_enumerate_dir(), chm_retrieve_object(). Это пригодится... В файле test_chmLib.c находится ещё один пример, на это раз извлекающий одну страницу из CHM-файла на диск. chm_http.c реализует простой HTTP-сервер, показывающий CHM-файл в браузере. Вот это, наверное, уже не пригодится. Вот мы и разобрали всё, что находится в vendor/CHMLib/src. Будем собирать библиотеку? Честно говоря, она достаточно маленькая, чтобы применить метод научного тыка. Ладненько, может этот LZX всё же нужен... Э-э-э… и всё? Чтобы убедиться в работоспособности кода, я скачал пример из Интернета: Посмотрим, как с ним справится enum_chmLib: Господи, даже здесь jQuery ¯\_(ツ)_/¯ Теперь мы знаем достаточно, чтобы использовать CHMLib в крейте chmlib-sys, который отвечает за сборку нативной библиотеки, линковку её компилятором Раста, и интерфейс к функциям на Си. Для сборки библиотеки нужно написать файл build.rs. С помощью крейта cc он вызовёт компилятор Си и сделает прочую дружбомагию, чтобы всё работало вместе как надо. Сперва добавим cc как зависимость для chmlib-sys: Затем напишем build.rs: Ещё надо рассказать Cargo о том, что chmlib-sys линкуется с библиотекой chmlib. Тогда Cargo сможет гарантировать, что во всём графе зависимостей присутствует только один крейт, зависящий от конкретной нативной библиотеки. Это позволяет избежать непонятных сообщений об ошибках про повторяющиеся символы или случайного использования несовместимых библиотек. Дальше нам остаётся объявить все функции, экспортируемые библиотекой chmlib, чтобы их можно было использовать из Раста. Именно для этого и существует замечательный проект bindgen. На вход ему отдаётся заголовочный файл на Си, а на выходе получается файл с FFI-привязками для Раста. На этом этапе полезно будет написать smoke-тест, который проверит, что всё работает как положено и мы действительно можем вызывать функции оригинальной библиотеки на Си. cargo test говорит, что вроде всё в порядке: Техни-и-ически мы теперь можем вызывать CHMLib из Раста, но это требует кучи unsafe. Для наколенной поделки может и сойдёт, но для публикации на crates.io стоит написать безопасную обёртку для всего небезопасного кода. Если посмотреть на API chmlib-sys с помощью cargo doc --open, то в нём видно много функций, которые принимают *mut ChmFile как первый аргумент. Это похоже на объекты и методы. Начнём с типа данных, который в конструкторе вызывает chm_open(), а в деструкторе — chm_close(). Для упрощения обработки ошибок мы используем крейт thiserror, которые автоматически реализует std::error::Error. Теперь надо придумать, как превратить std::path::Path в *const c_char. К сожалению, это не так-то просто сделать из-за разных приколов с совместимостью. Теперь определим структуру ChmFile. Она хранит ненулевой указатель на chmlib_sys::chmFile. Если chm_open() возвращает нулевой указатель, то значит у неё не получилось открыть файл из-за какой-то ошибки. Чтобы убедиться в отсутствии утечек памяти, запустим простой тест под Valgrind. Он создаст ChmFile и сразу же его освободит. Valgrind говорит, что неучтённой памяти не осталось: На очереди функция chm_resolve_object(): Поиск может завершиться ошибкой, поэтому chm_resolve_object() возвращает код ошибки, сообщающий об успехе или неудаче, а информация о найденном объекте будет записана по переданному указателю на chmUnitInfo. Тип std::mem::MaybeUninit создан как раз для нашего случая с out-параметром ui. Пока что оставим структуру UnitInfo пустой — это Rust-эквивалент C-структуры chmUnitInfo. Поля мы добавим, когда начнём что-то читать из ChmFile. Проверим ChmFile::find() на подопытном файле, который мы ранее скачали: CHMLib предоставляет API для просмотра содержимого CHM-файла через фильтр по битовой маске. Возьмём удобный крейт bitflags для работы с масками и флажками: И определим флажки Filter на основе констант из chm_lib.h: Ещё нам понадобится extern "C"-адаптер для Растовых замыканий, который можно передать в Си в виде указателя на функцию: Вот как function_wrapper используется для реализации ChmFile::for_each(): Последняя функция, которая нам нужна, отвечает за собственно чтение файла с помощью chm_retrieve_object(). Её реализация довольно тривиальная. Это похоже на типичный трейт std::io::Read, за исключением явного смещения в файле. Конечно, было бы неплохо иметь более детализированное сообщение об ошибке, чем «не вышло прочитать», но судя по исходному коду, chm_retrieve_object() не особо различает ошибки: Протестировать ChmFile::read() можно с помощью файлов с известным содержимым: Мы покрыли большую часть API библиотеки CHMLib и многие на этом бы закончили работу, считая портирование успешно завершённым. Однако, было бы неплохо сделать наш крейт ещё более удобным для пользователей. Этой цели служат примеры кода и документация — я заметил, что в сообществе Rust и Go этим моментам уделяется довольно много внимания (наверное потому, что rustdoc и godoc хорошо интегрированы в языковую среду). К счастью, в CHMLib уже есть примеры кода, так что нам будет достаточно просто портировать их. Также эти примеры послужат своеобразным интеграционным тестом, проверяющим, что исходная библиотека и наша обёртка ведут себя одинаково. Этот пример открывает CHM-файл и печатает таблицу с информаций обо всех его элементах. Функцию _print_ui() легко переписать на Rust. Она всего лишь генерирует текстовое описание из флажков UnitInfo и строк, а потом немного шаманит с выравниванием, чтобы таблица выглядела как таблица. Функция main() на скорую руку разбирает аргументы командной строки, открывает файл, и вызывает describe_item() через ChmFile::for_each(). Для проверки давайте сравним вывод примера на Расте с оригиналом: diff говорит, что всё совпадает, но давайте убедимся, что он бы увидел разницу, если бы она была. Добавим в вывод какой-нибудь мусор и посмотрим, что на это скажет diff. Запускаем тест с новым кодом: Победа! Другой пример, идущий в комплекте с CHMLib, извлекает все «обычные» файлы на диск. Реализация на Си довольно многословна ввиду отсутствия в языке высокоуровневых абстракций и слабой стандартной библиотеки. Надеюсь, наш пример будет более читабельным. Интерес здесь представляет функция extract(). Код в принципе самоочевиден, так что я не буду особо утруждать себя его пересказом своими словами на русском. Функция main() существенно проще, чем extract(), и от предыдущего примера отличается только обработкой ошибок распаковки. Натравив собранный пример на подопытный CHM-файл мы получаем пачку HTML-файлов, которые можно открыть в обычном веб-браузере. Часть JavaScript не работает (подозреваю какие-то особенности родного браузера Microsoft Help), поиска тоже нет, но в целом жить можно. Крейт chmlib теперь полностью функционален и, за исключением пары мелочей, готов к публикации на crates.io. Несколько вопросов остаются читателю в качестве домашнего задания: