Кулстори про Geant4, или почему я пишу сложный код

/ / misc :: , , , ,

G4GDMLParser G4GDMLRead relation.

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

В этой заметке я поделюсь небольшой детективной историей чтобы продемонстрировать, как и откуда берутся некоторые «перегруженные» решения, которые я бы с радостью обошёл или сократил, найдись для них предусмотренное сторонними архитекторами место. Эта история закончилась сравнительно хорошо — я нашёл способ решить задачу без построения внешних костылей или изобретения велосипедов, однако далеко не все экспедиции в код заканчиваются так хорошо. Собственно, и эта не слишком радужна: на то чтобы написать этот пост и исследовать потроха Geant4/xerces-c у меня ушло полдня, и счастливым её окончанием я обязан тому что Geant4 и xerces-c были в общем‐то неплохо запроектированы, что большая редкость в научном софте. Ну, xerces-c — один из старейших и наиболее стабильных XML-парсеров, официально поддерживаемый консорциумом W3C, а вот кого в CERN благодарить за толковую архитектуру Geant4 (в отличие от ROOT) неизвестно.

Geant4 GDML Parser и его 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() без каких‐либо изменений.

G4GDMLParser G4GDMLRead relation.

На первый взгляд, проблемы, конечно никакой нет — отдавай URL в G4GDMLParser::Read(), и не ведай горя. Однако:

  • Не предусмотрено (очевидного — в xerces-c и вообще — в Geant4) интерфейса для сообщения CURL (на основе которого в xerces-c реализованы HTTP-запосы) заголовков HTTP request. То есть, достать документ через POST-запрос, сообщив от клиента параметры человеческим образом (не url-encoded) нельзя.
  • GDML имеет тэги для импорта документов, которые чрезвычайно полезны при определении сложных геометрий, состоящих из нескольких детекторов, групп и ансамблей детекторов, включающих составные материалы, etc. Учитывая, что их семантика определяется через коллбэки в Geant4, где рассматриваются только локальные файлы, в пору ожидать трудностей, не говоря уже о том что в таких случаях даже url-encoded‐аргументы протащить в контекст шаблонизатора не получится.

Невозможное решение

G4GDMLRead inheritance graph.

На первый взгляд, архитектура Geant4 позволяет попросту определить потомка G4GDMLRead, и сообщить экземпляр через конструктор парсера G4GDMLParser. Это решение вполне легетимно и, судя по всему, даже предусмотрено архитектурой. В потомке метод G4GDMLRead::Read() можно было переопределить, снабдив xerces-c-парсер какой‐нибудь реализацией URLInputSource с тем чтобы все запросы от парсера были POST, и содержали наши данные.

Однако тут нас ждёт сюрприз.

  • метод G4GDMLRead::Read() не объявлен виртуальным.
  • более того, не взирая на длинную цепочку наследования G4GDMLReadStructure, G4GDMLParser реализует ассоциацию через указатель типа G4GDMLReadStructure.

Как ни странно, ни один из его базовых классов а именно сам «концевой» тип, хотя необходимый интерфейс (с методом Read(), который вызывает G4GDMLParser) был определён уже в базовом родителе G4GDMLRead.

G4GDMLReadStructure* reader;

С архитектурной точки зрения, такая ситуация напоминает устройства типа изображённого на картинке:

Useless device.

Хотя, конечно, её стоило бы дополнить набором более полезных крутилочек, соответствующих методам вышеупомянутого семейства <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 нужно изменять?

  • Проектирование — это хорошо.
  • Тяготение к известным и вычурным решениям (вроде xerces-c) способно исправить почти безнадёжное положение за счёт их собственной гибкости даже когда user code так себе.
  • Когда документация так себе, чтение исходников не так жутко, как может показаться (я потратил бы в три раза меньше времени, если бы не параллельное совещание в лаборатории по случаю возвращения коллеги и написание этого поста).

Comments