пятница, 8 апреля 2016 г.

Cache': перенос классов без исходников

При распространении коммерческих приложений иногда возникает вопрос о сокрытии исходного кода приложения. При написании приложения для Cache' перенос приложения выполняется путем переноса объектного кода. На машине разработчика производится экспорт рутин в объектном формате, на целевой машине они импортируются. Поскольку для работы интерпретатора требуются только объектники, то приложение работает нормально.

В современных условиях программисты все чаще прибегают к применению объектных технологий, в том числе при разработке приложений для Cache'. Но перенос классов с сокрытием исходного кода уже не такая простая задача. Формально предоставляемые средства предполагают экспорт класса в файл в формате cdl либо в формате xml в более современных версиях. При переносе же в этом формате предполагается перенос и исходного кода классов, его импорт на целевой машине и компиляция. Какие могут быть варианты сокрытия исходного кода при работе с классами?
Не возьмусь говорить обо всех возможных вариантах, но по крайней мере три варианта предложить можно. Первый и самый простой состоит в том, что в теле функций производится вызов рутин, которые уже, в свою очередь, переносятся в компилированном виде. Таким образом часть приложения переносится в исходном коде, часть в компилированном. Не уверен, что мне хотелось бы получить приложение, построенное таким образом, поскольку либо недоверие следует высказывать полностью, либо не высказывать его вообще. Хотя, с другой стороны, при таком распространении приложения используются достаточно стандартные средства.

Второй вариант состоит в подготовке файла данных cache.dat и его перенос на целевую машину с монтированием области (namespace) и / или включением маппинга глобалов на него. Переносится достаточно большой файл, но при использовании CD-ROM и при их распространенности в настоящее время это не составляет особой проблемы.

Третий вариант состоит в переносе класса несколько необычным образом. На машине разработчика создается класс, компилируется. После этого его можно использовать. Почему его можно использовать и как? Класс может быть использован двумя способами - либо в целях программирования, то есть наследования от него, либо в целях создания экземпляра объекта этого класса. На целевой машине предполагается второй вариант.

Разберем данные, имеющие отношение к нашему классу. Чтобы не вести совсем уж отвлеченный разговор, создадим какой-нибудь класс, например NewClass1 (хранимый) с двумя целочисленными свойствами NewProperty1 и NewProperty2, и с одним методом, например MewMethod1. Определение метода и тело его кода совершенно неважно. Для определенности укажем, что при написании статьи использовалась версия Cache' 3.2.2. В дальнейшем при воспроизведении действий могут быть различия, вызванные отдельными различиями версий.

Скомпилируем класс в обласи по умолчанию USER и разберем, что от него где хранится. Во-первых, в глобали ^oddDEF("NewClass1") хранится описание этого класса в том виде, как его дал программист. Экспорт и редактирование класса производится именно из этой глобали. Во-вторых, в глобалах ^oddCOM("NewClass1") и ^oddMAC("NewClass1") хранится код, скомпилированный компилятором классов (он не дает объектного кода). Данные из ^oddMAC("NewClass1") служат исходным материалом для генерации рутин mac, которые впоследствии компилируются в int с развертыванием директив препроцессора, и которые в свою очередь компилируются в obj. Исполняется же только obj - код.

Это, вообще говоря, основы работы с классами, и про них может и не стоило бы отдельно говорить, но дело в том, что в этой последовательности, описанной в документации, не совсем четко упоминается создание дескриптора класса. Дескриптор класса - это и есть те самые данные, которыми руководствуется объектный движок, когда ему требуется вызвать метод класса, объекта или обратиться к свойству объекта. В дескрипторе класса в компилированном виде хранится отображение методов на точки входа в объектном коде сгенерированных рутин. Дескриптор класса хранится в глобали ^rOBJ, то есть там же, где и скомпилированный код сгенерированных рутин поддержки класса. За исключением того, что он не является исполняемым. В нашем случае это узел ^rOBJ("ooNewClass1R0"). Приписываемые к имени класса символы спереди и сзади зависят от версии Cache' и могут содержать точки. При попытке обратиться к этому узлу получим ошибку, которой не было в необъектных версиях Cache':
USER>d ^ooNewClass1R0

<CLASS DESCRIPTOR>+1^ooNewClass1R0
USER 2d0>q
В нашем случае (в версии 3.2.2) к объектной поддержке класса относятся узлы
^rOBJ("ooNewClass1G1")
^rOBJ("ooNewClass1G2")
^rOBJ("ooNewClass1R0")
^rOBJ("ooNewClass1R1")
^rOBJ("ooNewClass1T1")
При компиляции класса в режиме сохранения промежуточных сгенерированных рутин можно посмотреть, что генерируется в остальных рутинах.

Проведем перенос объектного кода в другую область. Используем создаваемую по умолчанию область SAMPLES:
USER>m ^|"SAMPLES"|rOBJ("ooNewClass1G1")=^rOBJ("ooNewClass1G1")
USER>m ^|"SAMPLES"|rOBJ("ooNewClass1G2")=^rOBJ("ooNewClass1G2")
USER>m ^|"SAMPLES"|rOBJ("ooNewClass1R0")=^rOBJ("ooNewClass1R0")
USER>m ^|"SAMPLES"|rOBJ("ooNewClass1R1")=^rOBJ("ooNewClass1R1")
USER>m ^|"SAMPLES"|rOBJ("ooNewClass1T1")=^rOBJ("ooNewClass1T1") 
Не особенно выдумывая, просто повторим соответствующим образом пять команд. В более объемном случае, конечно, лучше использовать некий скрипт, переносящий все узды с маской ^rOBJ("ooNewClass1*").

Для проверки работоспособности создадим класс как в области USER, так и в области SAMPLES:
USER>s obj=##class(NewClass1).%New()
USER>w
%obj(1,0)="я"
%obj(1,0,2)=1
%obj(1,0,3)=""
%obj(1,1)="   "
%obj(1,2)=""
obj=1
USER>zn "samples"
SAMPLES>s obj2=##class(NewClass1).%New()
SAMPLES>w
%obj(1,0)="я"
%obj(1,0,2)=1
%obj(1,0,3)=""
%obj(1,1)="   "
%obj(1,2)=""
%obj(2,0)="я"
%obj(2,0,2)=1
%obj(2,0,3)=""
%obj(2,1)="   "
%obj(2,2)=""
obj=1
obj2=2 
При этом в обоих областях есть кодовая поддержка этого класса и после переключения области мы можем оперировать с объектом, созданным в предыдущей области.

Поскольку в нашем случае создавался класс, являющийся хранимым, то остается вопрос об использовании его в качестве таблицы так же, как и в исходной области. Для переноса определений, сгенерированных компилятором для SQL - движка, требуется перенос данных из глобали ^mdd. Кроме того, если класс объявляет хранимые процедуры SQL (интересный термин - а есть ли в SQL не хранимые процедуры?), следует также переносить соответствующие узлы из глобали ^oddPROC. Опять же обращу внимание на то, что все эти глобали являются глобалями внутреннего хранения InterSystems, и в разных версиях Cache' возможны соответствующие вариации как в именовании, так и в структуре хранения.

Существует два препятствия для описания такого механизма переноса таблиц в открытом источнике. Во-первых, в этой глобали используются идентификаторы, уникальные для области. Например, если в области USER скомпилированный класс NewClass1 имел таблицу с идентификатором 1, то в области SAMPLES уже находится 4 таблицы. При переносе таблицы, соответствующей классу NewClass1, должны быть выполнены генерация нового идентификатора таблицы и перенос данных от исходного идентификатора в целевой идентификатор. И это довольно трудоемкая работа. Во-вторых, описание такого механизма, как мне кажется, может нарушить лицензионное соглашение, поскольку начнет описывать совершенно недокументированные вещи. Впрочем, отмечу, что непреодолимых проблем в таком переносе для программистов, работающих с Cache' постоянно, не вижу. 

Выше была только продемонстрирована возможность перенесения кода, необходимого для работы классов, без переноса исходников. Реально в приложение могут входить сотни классов. В этом случае, конечно, имеет смысл разработать небольшую соответствующую утилиту, которая на машине разработчика производит экспорт объектников, относящихся к проекту, в файл экспорта, и на целевой машине просто проведет импорт с соответствующими командами merge и kill. Дополнительная проблема переноса объектников проекта может возникнуть из-за того, что отдельные версии Cache' могут рассматривать дескрипторные записи в глобали ^rOBJ как специальные и не предлагать их к экспорту. Что легко решается путем использования низкоуровневых команд. 

Таким образом, перенос классов без исходников возможен. В отличие от переноса класса штатными средствами классы не смогут быть использованы в целях программирования, поскольку переносятся только объектники. Определенную трудность может представлять перенос табличных определений, но для классов, не имеющих отношения к SQL, такой перенос вполне даже имеет смысл. Например, перенос классов форм SMWrap или классов CSP, не имеющих файлового представления, то есть классы, наследованные от CSP.Page и вызываемые указанием в url не csp, а cls. 

Комментариев нет:

Отправить комментарий