Вступление
За годы своего развития платформа 1С:Предприятия становилась все более открытой. Возможность работы с текстовыми файлами и табличными данными в dbf-формате в 90-е годы в 2000-е дополнилась доступностью многих современных средств интеграции. Здесь и веб-сервисы, и поддержка схем xml при обменах со сторонними системами, и возможность подключения к внешним источникам данных, и встроенный почтовый клиент. К сожалению, в развитии технологии внешних компонент наблюдается противоположный процесс. Это можно видеть, последовательно просмотрев диски ИТС с демонстрационными примерами от самой фирмы 1С. С выходом технологии разработчикам были доступны образцы создания компонент на C++ и Delphi, затем в список языков включили Visual basic 6, еще позже C#. Тогда 1С декларировала, что компоненту можно создать на любом компилируемом языке, поддерживающем COM. Ситуация резко изменилась после выхода версии 8.2 и создания технологии NativeAPI. Теперь программисты вынуждены довольствоваться ее демонстрацией только на C++. Причины такого сужения доступных средств лежат на поверхности, прежде всего это необходимость поддержки 64-битной архитектуры и кроссплатформенность.
Не будем уподобляться многим программистам (в том числе очень авторитетным), которые не переносят C++. Автор этой статьи придерживается более философского подхода, который можно сформулировать, как «каждому инструменту - свое место», но он не уверен, что технология внешних компонент является монопольным правом C++. Другое дело, что этот язык трудно вытеснить из-за жестких ограничений, накладываемых на формат компоненты 1С: необходимость создания машинных исполняемых файлов для каждой платформы (а не промежуточных образов как в Java или C#), возможность статической сборки компоненты в один, независящий от внешних библиотек и сред, файл. Кроме того, возросшие вкусы современных программистов выдвигают повышенные требования к инструменту замены: полноценная поддержка ООП, включая автоматическую сборку мусора, красота и элегантность; иными словами, необходимо оставить всю мощь, гибкость и эффективность C++, но убрать его недостатки и сложность. Казалось бы, требования невыполнимые и невозможно найти такой язык и платформу. Но автор смело заявляет: Есть такой язык!
История развития технологии внешних компонент
Давайте посмотрим, как менялся подход 1С к созданию внешних компонент с конца 90-х по настоящее время.
Кроме перечисленных плюсов и минусов COM-компонент, следует отметить, что для их создания программисту 1С достаточно незначительно расширить багаж своих знаний. Недостаток, указанный в третьем пункте думаю знаком даже программистам, не писавшим компоненты: стоит написать вместо ПодключитьВнешнююКомпоненту - ЗагрузитьВнешнююКомпоненту в надежде что 1С сама установит файл, и (привет системным администраторам!) потребуется регистрация компоненты в реестре Windows, которая невозможна без прав локального администратора. А если мы создаем компоненту на .NET, то нужно писать свой инсталлятор или пользоваться инструментами платформы .NET ( утилита regasm).
Технология внешних компонент на основе NativeAPI
Теперь посмотрим на внешние компоненты, разработанные на основе NativeAPI.
- Эта технология удобна для 1С-разработчиков тем, что установка компоненты не требуется, 1С ее разворачивает и устанавливает сама.
- А недостатком является сложность их создания. Фактически, сейчас 1С в качестве примера компонент использует только C++.
Конечно, можно создавать внешние компоненты и на Delphi. Но нюанс в том, что Kylix (компилятор приложений, написанных на Delphi, для работы под Linux) начиная с 2002 года уже не поддерживается, поэтому, у таких компонент могут возникнуть проблемы с кроссплатформенностью.
Язык программирования Eiffel
Язык Eiffel был разработан Бертраном Мейером, одним из ведущих специалистов по объектно-ориентированному программированию (ООП) во второй половине 80-х годов, промышленный компилятор и среда разработки выпущены в середине 90-х, им же основанной фирмой Eiffel Software (тогда ISE). Уже в те годы Eiffel удовлетворял всем критериям объектной-ориентированного языка, которые указаны на слайде:
Сила языка Eiffel заключается в его бескомпромиссном рационализме. Никаких лишних понятий и концепций, не имеющих выражения в языке или математической модели, которой является теория абстрактных типов данных. По сути, абстрактный тип данных – это класс без его конкретной реализации. Абстрактный тип данных определяется набором аксиом, характеризующих его поведение. Аксиомы при реализации типа как класса преобразуются в сущности (features), которые делятся на запросы, команды и конструкторы. Запрос возвращает данные о состоянии экземпляра класса (объекта), команда – изменяет это состояние, конструктор создает объект класса.
Уже первые версии реализации языка и среды разработки (Eiffel Studio) значительно опережали «конкурентов» (Java и позже появившейся C#) в плане следования стандартам ООП. Eiffel служил и служит лабораторией, где впервые реализуются новые (и как правило успешные) возможности, которые со временем появляются в более распространенных языках и средах. Например, множественное наследование до сих пор недоступно программистам Java и C#, ковариантность шаблонов появилась в C# только в net framework 3.5 и в это же время в Java. Ковариантное согласование параметров (аргументов) невозможно в других языках. Например, для команды копирования в базовом классе в Eiffel можно написать:
copy (other: like Current)
В классе-потомке аргумент other будет согласованно изменять свой тип. Неотъемлемой чертой Eiffel является идея контрактного программирования, берущая начало в теории абстрактных типов данных. Контракты позволяют на порядок повысить качество программного продукта и облегчают отладку. Контракты – прямое выражение алгебраического подхода моделирования внешнего мира, присущего программным объектам. Аксиоматические сущности предельно четко выражаются через классы и контракты. (Контракты были введены в C# начиная с версии 4.5.)
Синтаксис класса в Eiffel
На скриншоте показано, как может выглядеть реализация класса натуральных чисел в аксиоматике Пеано.
- В разделе invariant в конце класса описываются инварианты (аксиомы) класса.
- Члены класса (features) не делятся как в традиционных языках на поля, свойства и методы. По их функциональности они могут быть разделены на запросы и команды. Запросы предоставляют информацию и состоянии класса, команды - модифицируют это состояние.
Примеры технологий, реализованных в Eiffel раньше других языков программирования
В Eiffel были реализованы многие революционные подходы, которые потом "перетекли" в другие промышленные языки. Например:
- Ковариантность, которая в .NET появилась в 2000-х годах, а в Eiffel была изначально.
- Контрактное программирование, для которого только после выхода .NET версии 4.5 появились аддон для VisualStudio и соответствующий namespace.
- Многопоточное программирование на основе async/await, которые совсем недавно появились в .NET. Аналогичный по простоте механизм был введен в Eifel в серенине 2000-х.
- Проблема обращение к нулевой ссылке, когда вызывается метод или свойство переменной, которая еще не инициализирована. В Eiffel изначально предлагается очень простое решение этой проблемы. (см. операторы detachable/attached языка).
Дополнительные возможности Eiffel, которые позволяют создавать полноценные внешние компоненты
Рассмотрим, как Eiffel удовлетворяет требованиям по сборке внешних компонент:
Первое требование реализуется средой разработки от Eiffel Software. Реализация же второго пункта во многих языках представляет сложную задачу, но не в Eiffel, который имеет несколько средств интеграции с кодом на C++. Они нам потребуются для создания этих интерфейсов, поэтому вкратце рассмотрим их.
Средства интеграции Eiffel c С++
В первом примере читатели статьи, пасавшие NativeAPI компоненты лего узнают выделение памяти для объектов, создаваемых в компоненте и передаваемых в 1С. Имена параметров из Eiffel предваряются в C++ знаком $. Пример демонстрирует интеграцию кода C++ в Eiffel. Второй пункт слайда - вызов процедур Eiffel из C++ реализуется с помощью библиотеки CECIL, которая включает функции и макросы преобразования типов и API для управления сборщиком мусора. Для использования CECIL скомпилированный проект на Eiffel studio собирается утилитой make (nmake) в статическую библиотеку (*.lib), которая затем подключается к проекту на C++. Именно этим способом собирается внешняя компонента, реализуемая на языке Eiffel.
Архитектура шаблона внешней компоненты для 1С, написанного на Eiffel
Рассмотрим строение компоненты на Eiffel с точки зрения модульной архитектуры:
EiffelStudio для диаграмм классов употребляет BON (business object notation) нотацию, которую будем использовать и мы. В ней класс отображается в виде овала, связи между классами в виде сплошной (родитель – потомок) или пунктирной стрелки (поставщик – клиент). В виде прямоугольника мы обозначили набор экспортируемых для dll функций. Как известно, их назначение – дать декларативное описание объекта (или объектов) компоненты и создать по запросу указанный объект. Чтобы не заставлять программиста, использующего этот шаблон, каждый раз модифицировать код на C++, требуется Eiffel класс COMPONENTNAMES, который возвращает имена объектов компоненты. Как и в EiffelStudio класс выделен более светлым фоном, потому что он заморожен, т.е. не допускает наследования.
Запуск движка Eiffel средствами CECIL рассмотрим на примере экспортной функции GetClassNames:
Для старта необходимо подготовить ряд параметров среды (командную стоку, глобальные переменный ОС) и вызвать функцию eif_rtinit(), что делается в процедуре EifEnvInit. Окончание работы должно завершаться reclaim() для освобождения памяти и принудительного вызова сборщика мусора либо завершение работы произойдет автоматически при завершении процесса или выгрузке DLL, что актуально для внешней компоненты. Код функции представляет собой пример вызова сущности класса Eiffel из C++. eif_create выделяет память для объекта, но не вызывает конструктор класса, поэтому за ее вызовом требуется вызов конструктора для инициализации свойств и соблюдения инварианта класса. В данном примере инициализация не требуется, поэтому вызов отсутствует. Объекты Eiffel в C++ могут иметь тип EIF_OBJECT или EIF_REFERENCE, а также для некоторых типов EIF_INTEGER, EIF_POINTER и др. Оба типа представляют собой указатели, различие между ними в том, что EIF_OBJECT является «зафиксированным» в памяти объектом, который защищен от манипуляций сборщика мусора (перемещение в памяти с целью оптимизации), а EIF_REFERENCE – нет. Понятно, что вызовы сущностей из C++ должны производится только от EIF_OBJECT, но по окончании работы с объектом необходимо обязательно перевести его в нефиксированный статус вызовом eif_wean. Аналогичная инициализация среды Eiffel выполняется функцией GetClassObject, возвращающей EiffelAddIn, который служит по сути классом-оберткой над объектом компоненты в Eiffel. Его методы вызывают соответствующие сущности класса ECOMPONENTBASE. Их структура однотипна, приведем для примера код метода CallAsProc:
Массив параметров tVariant преобразуется тип TUPLE. Кортеж TUPLE[X1…Xn] в Eiffel определяется как последовательность n элементов, которая может быть больше чем n. Они широко используются в агентах (указателях на функции), inline агентах (анонимных функциях) и лямбда выражениях. Класс EiffelAddIn содержит также служебные процедуры для преобразования строк и параметров, передаваемых из 1С в Eiffel и обратно. Типу tVariant соответствует тип V8_ARG, который будет рассмотрен позже.
Базовый класс ECOMPONENTBASE, реализация свойств и методов компоненты
Класс ECOMPONENTBASE является передаточным звеном между декларативными вызовами свойств и методов компоненты из 1С и реальным ее объектом в Eiffel. Таким образом он должен выполнять две функции: предоставлять сущности для вызовов из промежуточного слоя C++ (класс EiffelAddIn) и отражать вызовы на объект компоненты, который от него наследуется. Поскольку среда и компилятор языка Eiffel не использует промежуточные метаданные, вроде байт-кода или msil, он не имеет развернутых средств рефлексии. В какой-то степени класс ECOMPONENTBASE их заменяет. Посмотрим на его инициализацию и некоторые свойства:
Напомним, что свойства (запросы) класса, которые в других языках называются полями в Eiffel’е всегда доступны только для чтения. Запрос addinname должен быть обязательно переопределен в классе-потомке (deferred), это имя класса-компоненты в 1С, которое создается инструкцией Новый(«AddIn.name»). Команда make – вспомогательная и служит для вызова из конструктора наследника, например:
В секции объявления наследования (inherit) указывается, что сущность make базового класса переопределяется. Кроме инструкции redefine в этой секции могут встречаться ряд других (undefined, select, rename, export), служащих для управления видимостью и разрешения конфликтов при множественном и дублируемом наследовании. Eiffel позволяет сделать любую сущность – конструктором, в свою очередь конструкторы базового класса не являются таковыми у потомка. В секции create происходит объявление make конструктором, а внутри можно видеть вызов предшественника Precursor. Сущность make служит для сохранения контекста 1С и указателя на менеджер памяти в компоненте. Также они инициализируют описатели свойств и методов V8_PROP и V8_METHOD, код которых рассмотрим позднее. Описатели хранятся списком LINKED_LIST[G]:
Инварианты класса очевидны и всегда подразумеваются программистом, пишущим класс, но только в Eiffel’е они имеют явное выражение.
Класс V8_ARG является представление типа Variant, а это означает, что он может содержать значение переменной языка 1С:Предприятия, которая передается в компоненту. На его примере рассмотрим неявную конвертацию типов в Eiffel:
Конструктор класса может быть одновременно конвертером, если он дополнительно объявлен в секции convert и в этой же секции объявлена функция обратной конверсии.
И в заключение, рассмотрим устройство механизма рефлексии для параметров и свойств компоненты.
Класс V8_FEATURE определяет общие свойства свойств и методов и алгоритм их поиска для методов FindMethods и FindProps:
Переопределив сущность is_equal базового класса ANY, мы получаем возможность поиска путем пообъектного сравнения в списках LINKED_LIST.
В Eiffel в отличие от платформы NET отсутствует рефлексия типов, поэтому описатели свойств и методов компоненты приходится писать в подобном стиле:
Пожалуй, это единственный минус в архитектуре компоненты.
В заключение раздела на примере сущности callasfunc познакомим читателей с синтаксисом попытки приведения типов в Eiffel:
В кортеже параметров (TUPLE) могут быть члены любого типа, поэтому такая проверка гарантирует правильную обработку типов и исключает вызов к нулевой ссылке (Void reference exception). Ее использование в данном контексте – требование языка, оно необходимо для правильной работы динамического связывания
Заключение
Как известно, описание любого паттерна должно состоять из двух частей: указания по применению («как использовать») и описание самого шаблона. В предыдущих разделах мы постарались обратить внимание на ключевые места внутреннего строения компоненты, сейчас же нам осталось дать пошаговый алгоритм использования. Он очень простой:
Шаблон компоненты приложен к даной статье.
**************
Данная статья написана по итогам доклада, прочитанного на конференции INFOSTART EVENT 2015 CONNECTION 15-17 октября 2015 года.
Приглашаем вас на новую конференцию INFOSTART EVENT 2019 INCEPTION.