Не будучи профессиональным программистом, я на самом деле редко слышу критику в свой адрес касательно того как и что пишу от компетентных кодеров. От коллег‐физиков, впрочем, мне нередко приходится слышать обвинения в том что то что я делаю оказывается довольно сложно для понимания.
В этой заметке я поделюсь небольшой детективной историей чтобы продемонстрировать, как и откуда берутся некоторые «перегруженные» решения, которые я бы с радостью обошёл или сократил, найдись для них предусмотренное сторонними архитекторами место. Эта история закончилась сравнительно хорошо — я нашёл способ решить задачу без построения внешних костылей или изобретения велосипедов, однако далеко не все экспедиции в код заканчиваются так хорошо. Собственно, и эта не слишком радужна: на то чтобы написать этот пост и исследовать потроха 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 нужно
изменять?