
Не будучи профессиональным программистом, я на самом деле редко слышу критику в свой адрес касательно того как и что пишу от компетентных кодеров. От коллег‐физиков, впрочем, мне нередко приходится слышать обвинения в том что то что я делаю оказывается довольно сложно для понимания.
В этой заметке я поделюсь небольшой детективной историей чтобы продемонстрировать, как и откуда берутся некоторые «перегруженные» решения, которые я бы с радостью обошёл или сократил, найдись для них предусмотренное сторонними архитекторами место. Эта история закончилась сравнительно хорошо — я нашёл способ решить задачу без построения внешних костылей или изобретения велосипедов, однако далеко не все экспедиции в код заканчиваются так хорошо. Собственно, и эта не слишком радужна: на то чтобы написать этот пост и исследовать потроха Geant4/xerces-c у меня ушло полдня, и счастливым её окончанием я обязан тому что Geant4 и xerces-c были в общем‐то неплохо запроектированы, что большая редкость в научном софте. Ну, xerces-c — один из старейших и наиболее стабильных XML-парсеров, официально поддерживаемый консорциумом W3C, а вот кого в CERN благодарить за толковую архитектуру Geant4 (в отличие от ROOT) неизвестно.
G4GDMLParserВ пакете для физических MC‐симуляций Geant4 вводится подмножество XML на основе
XSD‐схемы под названием
GDML. Парсер реализован на
основе xerces-c, Geant4 предлагает
основной интерфейс взаимодействия с ним в классе
G4GDMLParser,
ассоциирующий экземпляр реализующий интерфейс чтения XML‐файлов в структуре
G4GDMLRead.
Метод G4GDMLParser::Read() служит точкой входа в процедуру разбора XML
документа и делегирует выполнение методу G4GDMLRead::Read() ассоциированного
экземпляра G4GDMLRead. Класс G4GDMLRead реализует семейство методов
с именами вида <something>Read(), где
<something> <- [Divisionvol, File, Replicavol, ...]. Методы из этого
семейства объявлены как виртуальные.
Светлая идея сулящая множество выгод и великие блага состоит в том чтобы
подготавливать GDML‐документ на удалённом сервере, используя высокоуровневый
шаблонизатор (на Perl, или jinja2 на Python). GDML схож с HTML с точки зрения
шаблонизатора, и такой подход позволил бы динамически собирать нужную геометрию
на основе шаблонов имеющихся на сервере, данных из общей базы и некоторой
информации, сообщённой POST‐запросом протокола HTTP. Основная выгода такого
механизма опирается на выразительную простоту и стабильность такого решения с
большим успехом применяемого в web (вместо XML/GDML тут HTML).
Xerces-c умеет понимать URL в качестве аргумента указывающего на источник, и в
этом смысле название аргумента filename в декларациях Geant4 наталкивает на
подозрения в том что архитекторы Geant4 пошли на редукцию use cases по каким‐то
причинам. Тем не менее, беглый взгляд на код из гугла
(раз,
два)
демонстрирует, что filename передаётся и используется в метод из xerces-c
xercesc::XercesDOMParser::parse() без каких‐либо изменений.
На первый взгляд, проблемы, конечно никакой нет — отдавай URL в
G4GDMLParser::Read(), и не ведай горя. Однако:

На первый взгляд, архитектура Geant4 позволяет попросту определить потомка
G4GDMLRead,
и сообщить экземпляр через конструктор
парсера G4GDMLParser. Это решение вполне легетимно и, судя по всему, даже
предусмотрено архитектурой. В потомке метод G4GDMLRead::Read() можно было
переопределить, снабдив xerces-c-парсер какой‐нибудь реализацией
URLInputSource
с тем чтобы все запросы от парсера были POST, и содержали наши данные.
Однако тут нас ждёт сюрприз.
G4GDMLRead::Read() не объявлен виртуальным.G4GDMLReadStructure,
G4GDMLParser реализует ассоциацию через указатель типа G4GDMLReadStructure.Как ни странно, ни один из его базовых классов а именно сам «концевой» тип,
хотя необходимый интерфейс (с методом Read(), который вызывает G4GDMLParser)
был определён уже в базовом родителе G4GDMLRead.
G4GDMLReadStructure* reader;
С архитектурной точки зрения, такая ситуация напоминает устройства типа изображённого на картинке:

Хотя, конечно, её стоило бы дополнить набором более полезных крутилочек,
соответствующих методам вышеупомянутого семейства <smth>Read(), ведь они-то
как раз и сделаны виртуальными, всё же основная функциональность читалки
вынесенной в G4GDMLRead по отношению к парсеру напоминает именно такой
бесполезный прибор.
Очевидно, это архитектурное решение, однако я затрудняюсь понять, чем оно
было продиктовано. Учитывая, что xerces-c допускает разбор документа из буфера,
для того чтобы реализовать нужное поведение, достаточно было бы переопределить
метод G4GDMLRead::Read().
Простейшее решение на костылях предусматривало бы скачивание функциями cURL'а всех GDML документов в какую‐то временную локальную директорию с последующим запуском нативного парсера Geant4 на них.
Пока гуглил POST-запросы в xerces-c обнаружил упоминание о классе
XMLNetHTTPInfo,
который очевидно позволяет сконфигурировать POST-запрос и задать для него
payload. Очень интригующе.
Класс не имеет родителя, так что можно попробовать отыскать критический участок
(тот в котором xerces-c формирует сетевой запрос) по имени этого класса.
Грепнул по имени класса хидеры xerces-c, и нашёл, что XMLNetAccessor имеет
сравнительно небольшое количество упоминаний. Наиболее интересен тут класс
XMLNetAccessor, но каким образом его присобачить к xerces-c мне пока неясно.
Отложил эту находку и решил сам размотать цепочку вызовов до лексера, чтобы
ничего не упустить.
Метод G4GDMLRead::Read,
как легко видеть использует выделенный на хипе (в строке 289) объект класса
XercesDOMParser
для разбора документа (метод parse в строке 300). Метод parse()
определён
в его родителе AbstractDOMParser,
и в своей реализации
перегруженной по сигнатуре входного аргумента, делегирует дальнейший разбор
документа лексическому сканеру xerces-c: XMLScanner::scanDocument().
По сигнатуре (const char *) резолвится метод scanDocument() который на
основе данного char-URI-идентификатора создаёт
unicode-URI-идентификатор и, наконец, форвардит выполнение методу
scanDocument(),
реализующему вычитывание документа из источника.
Нас интересует строка 347 (по последней ссылке):
srcToUse = new (fMemoryManager) URLInputSource(tmpURL, fMemoryManager);
где URI (уже точно URL, поскольку в строке делается соответствующая проверка)
уходит в конструктор объекта URLInputSource, который, очевидно и работает
с сетевыми запросами (fMemoryManager — аллокатор xerces-c). Проверка на
URL'овость производится
XMLURL::parse() (реализация со строки 932).
Дальнейшая работа с документом происходит на основе интерфейсов абстрактной
базы InputSource, вызовы которой управляются из лексического сканера.
Заинтересуемся пока реализацией
класса URLInputSource
в попытке понять, где именно происходит сборка сетевых запросов. Специфичные
для сетевой работы методы определены, разумеется, в самом классе, и в глаза
сразу бросается виртуальный
метод makeStream(),
который, собственно, и производит вычитку содержимого документа. Там,
в реализации, в строках 605-608 даётся любопытный комментарий:
606 // Need to manually replace any character reference %xx first
607 // HTTP protocol will be done automatically by the netaccessor
из которого следует, что действительная работа с HTTP осуществляется в
аттрибуте fgNetAccessor. Действительно (см. строку 674) — makeStream()
возвращает результат XMLPlatformUtils::fgNetAccessor->makeNew(*this);. И вот
тут, кажется, жизнь пошла совсем другая: из венгерской нотации и лексики C++
ясно, это скорее всего некий глобальный экземпляр, статический атрибут класса
XMLPlatformUtils.
Действительно, Мисс Марпл.
fgNetAccessor — это указатель на экземпляр класса XMLNetAccessor,
упоминания о котором загуглились сходу (в начале этого пункта).
Теперь сделалось понятно, как можно бы прицепить payload к POST‐запросам в
xerces-c:
XMLPlatformUtils::fgNetAccessor экземпляром
кастомного подкласса platform-specific-класса (нам скорее всего подойдёт
CurlNetAccessor,
обратите внимание на сигнатуру makeNew()).makeNew() таким образом чтобы в cURL
уходил нужный нам экземпляр XMLNetHTTPInfo — POST, custom payload.Важно, однако, удостовериться что поддержка сети
была включена при сборке xerces-c: --enable-netaccessor-curl для систем с
cURL.
На этом, собственно поисковая часть заканчивается. Ну… в норме. Практически, оказалось, что Apache Foundation — ленивые жопашники, и понадобился ещё день на локализацию этого бага в Xerces-c. Ну, сделаем скидку парням, они до сих пор держат проект на SVN, там может быть очень непросто добавить патч который висит в открытом тикете уже шесть лет.
Нужно сказать, что из того что сделалось видно, как можно неинвазивно
решить задачу, G4GDMLReadStructure::Read() не перестаёт быть useless device.
Что делать, если нужно более сложное поведение, чем серия POST-запросов с
одинаковым payload — если для импортируемых GDML‐документов payload нужно
изменять?