Хабрахабр

[Из песочницы] Программное создание библиотеки типов

В практических руководствах по программированию COM‐компонентов обычно рассказывают как создавать библиотеку типов вручную через комплилятор midl.exe, но сегодня рассмотрим как это делать программно через интерфейсы ICreateTypeLib2 и ICreateTypeInfo2. Библиотека типов TLB может хранить в себе информацию о возможностях COM‐компонентов: классы, интерфейсы, методы, типы параметров и возвращаемые значения.

В качестве «языка программирования» будет выступать FreeBASIC.

Интерфейсы

Для демонстрации серьёзности намерений построим интерфейс натуральной дроби и посмотрим на все этапы построения интерфейсов с нуля. Во фрибейсике отсутствует ключевое слово Interface для объявления интерфейсов, но мы справимся с этой задачей и сами голыми руками.

Упростим понятие интерфейса до набора функций, которые следует обязательно реализовать.

Интерфейс как тип данных

У такого класса должны быть свойства, задающие числитель и знаменатель, а также мы добавим туда операцию сложения дробей. Определим класс натуральной дроби. Завернём их в структуру: Так как нам впоследствии необходимо будет иметь доступ из унаследованного объекта к реализованным функциям, это значит, что такой набор должен состоять из указателей на наши будущие функции.

Type IRational Dim GetNumerator As Function()As Integer Dim SetNumerator As Sub(ByVal Numerator As Integer) Dim GetDenominator As Function()As Integer Dim SetDenominator As Sub(ByVal Denominator As Integer) Dim AddRational As Sub(ByVal pRational As IRational Ptr) End Type

Добавление контекста вызова

Чтобы функции интерфейса знали, какой объект их вызывает, добавим в них первым параметром указатель на объект, реализующий наш интерфейс:

Type IRational Dim GetNumerator As Function(ByVal this As IRational Ptr)As Integer Dim SetNumerator As Sub(ByVal this As IRational Ptr, ByVal Numerator As Integer) Dim GetDenominator As Function(ByVal this As IRational Ptr)As Integer Dim SetDenominator As Sub(ByVal this As IRational Ptr, ByVal Denominator As Integer) Dim AddRational As Sub(ByVal this As IRational Ptr, ByVal pRational As IRational Ptr)
End Type

Таблица виртуальных методов

Название VirtualTable часто сокращают до Vtable или даже Vtbl: На практике набор функций интерфейса выделяют в отдельную структуру, которую теперь называеют таблицей виртуальных методов, а в самом интерфейсе оставляют ссылку на неё.

Type IRationalVirtualTable Dim GetNumerator As Function(ByVal this As IRational Ptr)As Integer Dim SetNumerator As Sub(ByVal this As IRational Ptr, ByVal Numerator As Integer) Dim GetDenominator As Function(ByVal this As IRational Ptr)As Integer Dim SetDenominator As Sub(ByVal this As IRational Ptr, ByVal Denominator As Integer) Dim AddRational As Sub(ByVal this As IRational Ptr, ByVal pRational As IRational Ptr)
End Type Type IRational Dim lpVtbl As IRationalVirtualTable Ptr
End Type

Разрешение циклических ссылок

Дело в том, что виртуальная таблица IRationalVirtualTable ссылается на интерфейс IRational, объявленный позднее, а интерфейс IRational ссылается на виртуальную таблицу IRationalVirtualTable, и как их не меняй местами, ссылаться друг на друга от этого они не перестают. Пытаемся всё скомпилировать, но компилятор почему‐то сопротивляется такому коду. Выйти из ситуации поможет дополнительное имя для нашего интерфейса, введённое оператором Type, а к названию оригинального интерфейса добавим подчёркивание:

Type IRational As IRational_ Type IRationalVirtualTable Dim GetNumerator As Function(ByVal this As IRational Ptr)As Integer Dim SetNumerator As Sub(ByVal this As IRational Ptr, ByVal Numerator As Integer) Dim GetDenominator As Function(ByVal this As IRational Ptr)As Integer Dim SetDenominator As Sub(ByVal this As IRational Ptr, ByVal Denominator As Integer) Dim AddRational As Sub(ByVal this As IRational Ptr, ByVal pRational As IRational Ptr)
End Type Type IRational_ Dim lpVtbl As IRationalVirtualTable Ptr
End Type

Ну вот теперь‐то определение нашего интерфейса готово.

COM‐интерфейсы

Все COM‐интерфейсы строятся по описанному выше принципу, но с дополнительными ограничениями: Вся технология COM построена на интерфейсах.

  • все COM‐интерфейсы прямо или косвенно наследуются от интерфейса IUnknown;
  • за исключением методов AddRef и Release, все процедуры и функции должны возвращать тип данных HRESULT;
  • «настоящее» возвращаемое значение заносится функцией в переданный ей указатель последним параметром.

Интерфейсы, поддерживающие вызовы функций одновременно через таблицу и IDispatch, называются дуальными. Для того, чтобы нашим интерфейсом можно было манипулировать не только через таблицу виртуальных функций, но и по именам функций из скриптовых языков программирования, рекомендуется наследовать интерфейсы не прямо от IUnknown, а от IDispatch. Также сменим тип операндов функций с Integer на Long, чтобы соответствовать типам автоматизации.

GUID

Для этого используются 128‐битные числа, вычисляемые по специальному алгоритму, гарантирующему уникальность. У каждого интерфейса и реализующего класса должны быть уникальные идентификаторы. В заголовочном файле GUID определён в виды одноимённой структуры с дополнительными именами IID (идентификатор интерфейса) и CLSID (идентификатор класса). Такие числа называют GUID. В программе GUID для интерфейсов и классов записываются так: Получить GUID можно через утилиту guidgen.exe либо через функцию CoCreateGuid.

' Идентификатор интерфейса IRational '
Dim Shared IID_IRational As IID = Type(&h4116b36a, &hb0d, &h48fd, _ {&h8d, &hb6, &hb9, &h86, &h7f, &h2a, &h1a, &h37}) ' Идентификатор класса Rational, реализующего IRational ' {DD6C5B70-592D-41C1-A391-BCB8C7F7639A}
Dim Shared CLSID_Rational As CLSID = Type(&hdd6c5b70, &h592d, &h41c1, _ {&ha3, &h91, &hbc, &hb8, &hc7, &hf7, &h63, &h9a})

Переделывание интерфейса IRational в COM‐совместимый

Вот окончательный заголовочный файл IRational.bi: Изменим наш интерфейс в соответствии с этими требованиями, добавив заголовочные файлы и сторожей от повторного включения кода.

#ifndef IRATIONAL_BI
#define IRATIONAL_BI #ifndef unicode
#define unicode
#endif #include once "windows.bi"
#include once "win\ole2.bi" ' {4116B36A-0B0D-48FD-8DB6-B9867F2A1A37}
Dim Shared IID_IRational As IID = Type(&h4116b36a, &hb0d, &h48fd, _ {&h8d, &hb6, &hb9, &h86, &h7f, &h2a, &h1a, &h37}) ' {DD6C5B70-592D-41C1-A391-BCB8C7F7639A}
Const CLSIDS_Rational = "{DD6C5B70-592D-41C1-A391-BCB8C7F7639A}"
Dim Shared CLSID_Rational As CLSID = Type(&hdd6c5b70, &h592d, &h41c1, _ {&ha3, &h91, &hbc, &hb8, &hc7, &hf7, &h63, &h9a}) Type IRational As IRational_ Type IRationalVirtualTable ' Наследование от IDispatch Dim VirtualTable As IDispatchVtbl Dim GetNumerator As Function(ByVal this As IRational Ptr, ByVal pResult As Long Ptr)As HRESULT Dim SetNumerator As Function(ByVal this As IRational Ptr, ByVal Numerator As Long)As HRESULT Dim GetDenominator As Function(ByVal this As IRational Ptr, ByVal pResult As Long Ptr)As HRESULT Dim SetDenominator As Function(ByVal this As IRational Ptr, ByVal Denominator As Long)As HRESULT Dim AddRational As Function(ByVal this As IRational Ptr, ByVal pRational As IRational Ptr)As HRESULT
End Type Type IRational_ Dim lpVtbl As IRationalVirtualTable Ptr
End Type #endif

Библиотека типов

Наша библиотека типов будет состоять из определения интерфейса IRational и реализующего его класса Rational.

Подготовительный этап

Когда она больше не нужна, вызываем соответствующий CoUnInitialize(). Для работы среды COM её следует инициализировать вызовом CoInitialize(0).

Определим идентификатор будущей библиотеки:

' {23F94DA0-5C11-46C1-9F27-6A3FE27985CF}
Dim Shared LIBID_Rational As GUID = Type(&h23f94da0, &h5c11, &h46c1, _ {&h9f, &h27, &h6a, &h3f, &he2, &h79, &h85, &hcf})

Так как IRational наследуется от IDispatch, то нам необходимо загрузить библиотеку stdole32.tlb, где хранится ITypeInfo от IDispatch, чтобы потом добавить на него ссылку:

CoInitialize(0) Dim pIDispatchTypeInfo As ITypeInfo Ptr = Any Dim pStdOleTypeLib As ITypeLib Ptr = Any
LoadTypeLib("stdole32.tlb", @pStdOleTypeLib)
pStdOleTypeLib->lpVtbl->GetTypeInfoOfGuid(pStdOleTypeLib, @IID_IDispatch, @pIDispatchTypeInfo) ' stdole32.tlb больше не нужна
pStdOleTypeLib->lpVtbl->Release(pStdOleTypeLib)

Создание библиотеки

Она принимает три параметра: тип системы (SYS_MAC, SYS_WIN16, SYS_WIN32 или SYS_WIN64), имя файла библиотеки и указатель на интерфейс. Получить интерфейс ICreateTypeLib2 для создания библиотеки можно функцией CreateTypeLib2.

Dim pCreateTypeLib As ICreateTypeLib2 Ptr = Any CreateTypeLib2(SYS_WIN32, "Rational.tlb", @pCreateTypeLib) ' Заполняем имя библиотеки, GUID, версию, язык и описание
pCreateTypeLib->lpVtbl->SetName(pCreateTypeLib, "Rational")
pCreateTypeLib->lpVtbl->SetGuid(pCreateTypeLib, @LIBID_Rational)
pCreateTypeLib->lpVtbl->SetVersion(pCreateTypeLib, 1, 0)
pCreateTypeLib->lpVtbl->SetLcid(pCreateTypeLib, 1049) ' русский язык
pCreateTypeLib->lpVtbl->SetDocString(pCreateTypeLib, "Библиотека натуральных дробей")

Добавление интерфейса IRational в библиотеку

Для этого ему нужно передать одно из значений перечисления TYPEKIND. Метод CreateTypeInfo интерфейса ICreateTypeLib позволяет добавлять в библиотеку интерфейсы, классы, модули с функциями, перечисления, структуры, объединения и псевдонимы.

Тип сущности, TYPEKIND

Описание

TKIND_ALIAS

Тип, который является псевдонимом для другого типа.

TKIND_INTERFACE

Интерфейс с чистыми виртуальными функциями, то есть такими, у которых нет реализации.

TKIND_COCLASS

Класс, унаследованный от интерфейсов.

TKIND_DISPATCH

Набор методов и свойств, доступных через IDispatch.Invoke. По умолчанию дуальные интерфейсы возвращают TKIND_DISPATCH.

TKIND_ENUM

Перечисление.

TKIND_MAX

Метка окончания перечисления.

TKIND_MODULE

Модуль, который может содержать только статические функции и данные (например, DLL).

TKIND_RECORD

Структура без методов.

TKIND_UNION

Объединение переменных, имеющих нулевое смещение.

Возвращаемым значением является ICreateTypeInfo для настройки полученной сущности. Мы будем рассматривать добавление только классов и интерфейсов.

Dim pCreateTypeInfoIRational As ICreateTypeInfo Ptr = Any
pCreateTypeLib->lpVtbl->CreateTypeInfo(pCreateTypeLib, @IRationalInterfaceName, TKIND_INTERFACE, @pCreateTypeInfoIRational) ' Устанавливаем IID и текстовое описание
pCreateTypeInfoIRational->lpVtbl->SetGuid(pCreateTypeInfoIRational, @IID_IRational)
pCreateTypeInfoIRational->lpVtbl->SetDocString(pCreateTypeInfoIRational, @"Интерфейс для поддержки натуральных дробей") ' Интерфейс будет дуальным и поддерживающим автоматизацию
pCreateTypeInfoIRational->lpVtbl->SetTypeFlags(pCreateTypeInfoIRational, TYPEFLAG_FDUAL Or TYPEFLAG_FOLEAUTOMATION)

Небольшая палочка в колесо: придётся самостоятельно следить за индексами добавляемых ссылок на всех отцов. Для наследования необходимо создать ссылку на интерфейс‐папу.

' Наследуем от IDispatch
Dim RefType As HREFTYPE = Any
hr = pCreateTypeInfoIRational->lpVtbl->AddRefTypeInfo(pCreateTypeInfoIRational, pIDispatchTypeInfo, @RefType) ' 0 — это индекс ссылки
hr = pCreateTypeInfoIRational->lpVtbl->AddImplType(pCreateTypeInfoIRational, 0, RefType) ' IDispatchTypeInfo больше не потребуется
pIDispatchTypeInfo->lpVtbl->Release(pIDispatchTypeInfo)

Добавление функций в интерфейс IRational

Функции представлены структурой FUNCDESC, параметры — массивом из структур ELEMDESC. У функций есть параметры и возвращаемое значение. Так как все функции интерфейса возвращают HRESULT, можно заранее создать возвращаемое значение для всех функций:

Dim HresultReturnedValue As ELEMDESC
With HresultReturnedValue .tdesc.vt = VT_HRESULT .idldesc.wIDLFlags = IDLFLAG_NONE
End With

Флаги могут принимать комбинацию из следующих значений: Здесь vt определяет одно из значений перечисления VARENUM, а IDLFLAG_NONE указывает, что флагов не присвоено.

Флаги параметров и возвращаемых значений

Описание

IDLFLAG_FIN

Входящий параметр.

IDLFLAG_FOUT

Исходящий параметр, возвращает сведения из вызываемого объекта в вызывающий объект, обычно является указателем.

IDLFLAG_FRETVAL

«Настоящее» возвращаемое значение функции, обычно комбинируют с флагом IDLFLAG_FOUT.

IDLFLAG_NONE

Не установлено.

Со стороны свойство выглядит как переменная, но изнутри обеспечивается с помощью функций установки и возврата значения. В некоторых случаях у объекта могут существовать свойства. Функции можно описать одним из следующих значений:

Виды функций по поведению, INVOKEKIND

Описание

INVOKE_FUNC

Обычная функция.

INVOKE_PROPERTYGET

Свойство, возвращающее значение.

INVOKE_PROPERTYPUT

Свойство, устанавливающее значение.

INVOKE_PROPERTYPUTREF

Свойство, устанавливающее значение по ссылке.

Ещё функции подразделаются по реализованности:

Виды функций по реализованности, FUNCKIND

Описание

FUNC_STATIC

Статическая функция с реализацией, без контекста вызова. Такие функции обычно живут в DLL.

FUNC_NONVIRTUAL

Статическая функция‐член класса с реализацией, внутри себя принимает неявный контекст вызова.

FUNC_VIRTUAL

Виртуальная функция‐член класса с реализацией, внутри себя принимает неявный контекст вызова.

FUNC_PUREVIRTUAL

Чистая виртуальная функция, внутри себя принимает неявный контекст вызова.

FUNC_DISPATCH

Функция доступна только через IDispatch.Invoke.

Функция GetNumerator

Введём описание параметров функции GetNumerator: В высокоуровневых языка программирования контексты вызова функций (указатель на объект), как и таблицы виртуальных функций, скрыты от программиста и не учитываются в интерфейсах, поэтому при настройке параметров мы не будем их указывать.

Const MaxArgumentGetNumeratorNamesLength As UINT = 2
Const MaxArgumentGetNumeratorLength As SHORT = 1 ' Массив из имени функции и параметра ' Так как эта функция часть свойства, то мы указываем её имя без префикса Get.
Dim GetNumeratorArgumentNames(MaxArgumentGetNumeratorNamesLength - 1) As WString Ptr = Any
GetNumeratorArgumentNames(0) = @"Numerator"
GetNumeratorArgumentNames(1) = @"pResult" ' Исходящий параметр — «настоящее» возвращаемое значение
Dim GetNumeratorArguments(MaxArgumentGetNumeratorLength - 1) As ELEMDESC
Dim RetvalGetNumerator As TYPEDESC
With RetvalGetNumerator .vt = VT_I4 ' Тип автоматизации Long
End With
With GetNumeratorArguments(0) .tdesc.vt = VT_PTR ' Указатель .tdesc.lptdesc = @RetvalGetNumerator ' Тип данных указателя .idldesc.wIDLFlags = IDLFLAG_FOUT Or IDLFLAG_FRETVAL
End With

Заполнение структуры FUNCDESC для функции GetNumerator:

Dim GetNumeratorDefinition As FUNCDESC = Any
With GetNumeratorDefinition .memid = 0 ' Номер функции при вызове через IDispatch.Invoke, у парных функций‐свойств это поле должно совпадать .lprgscode = 0 ' Массив ориентировочных возвращаемых значений HRESULT .cScodes = 0 ' Размер массива возвращаемых значений .lprgelemdescParam = @GetNumeratorArguments(0) ' Указатель на массив параметров функций .cParams = MaxArgumentGetNumeratorLength ' Количество параметров .cParamsOpt = 0 ' Количество необзязательных параметров .elemdescFunc = HresultReturnedValue ' Функция возвращает HRESULT .funckind = FUNC_PUREVIRTUAL ' Чисто виртуальная функция .invkind = INVOKE_PROPERTYGET ' Это получение свойства .callconv = CC_STDCALL ' Соглашение о вызове STDCALL .oVft = 0 ' Индекс функции в виртуальной таблице, указывается только для FUNC_VIRTUAL .wFuncFlags = 0
End With

Здесь опять придётся следить за индексом добавляемой функции, в нашем случае это 0: Теперь функцию можно насаживать на интерфейс.

' Функция и параметры
pCreateTypeInfoIRational->lpVtbl->AddFuncDesc(pCreateTypeInfoIRational, 0, @GetNumeratorDefinition) ' Имена функций и параметров
pCreateTypeInfoIRational->lpVtbl->SetFuncAndParamNames(pCreateTypeInfoIRational, 0, @GetNumeratorArgumentNames(0), MaxArgumentGetNumeratorNamesLength) ' Текстовое описание функции
pCreateTypeInfoIRational->lpVtbl->SetFuncDocString(pCreateTypeInfoIRational, 0, @"Возвращает числитель")

Функция SetNumerator

Похожим образом определяем вторую часть свойства Numerator:

Const MaxArgumentSetNumeratorNamesLength As UINT = 2
Const MaxArgumentSetNumeratorLength As SHORT = 1 Dim SetNumeratorArgumentNames(MaxArgumentSetNumeratorNamesLength - 1) As WString Ptr = Any
SetNumeratorArgumentNames(0) = @"Numerator"
SetNumeratorArgumentNames(1) = @"Numerator" ' Всего один входящий параметр
Dim SetNumeratorArguments(MaxArgumentSetNumeratorLength - 1) As ELEMDESC
With SetNumeratorArguments(0) .tdesc.vt = VT_I4 ' Long .idldesc.wIDLFlags = IDLFLAG_FIN
End With Dim SetNumeratorDefinition As FUNCDESC = Any
With SetNumeratorDefinition .memid = 0 ' Номер функции такой же как у GetNumerator .lprgscode = 0 .cScodes = 0 .lprgelemdescParam = @SetNumeratorArguments(0) ' Массив параметров функций .cParams = MaxArgumentSetNumeratorLength ' Количество параметров .cParamsOpt = 0 .elemdescFunc = HresultReturnedValue .funckind = FUNC_PUREVIRTUAL .invkind = INVOKE_PROPERTYPUT ' Установка свойства .callconv = CC_STDCALL .oVft = 0 .wFuncFlags = 0
End With

Однако в методе SetFuncAndParamNames необходимо указать индекс предыдущей функции GetNumerator, так как она является парной для свойства. Для добавления функции в интерфейс опять придётся следить за индексами вручную.

pCreateTypeInfoIRational->lpVtbl->AddFuncDesc(pCreateTypeInfoIRational, 1, @SetNumeratorDefinition) ' Вот здесь указываем 0 — индекс предыдущей части свойства
pCreateTypeInfoIRational->lpVtbl->SetFuncAndParamNames(pCreateTypeInfoIRational, 0, @SetNumeratorArgumentNames(0), MaxArgumentSetNumeratorNamesLength)
pCreateTypeInfoIRational->lpVtbl->SetFuncDocString(pCreateTypeInfoIRational, 1, @"Устанавливает числитель")

Аналогичным образом добавляются функции для возвращения и установки знаменателя, с индексами 2 и 3.

Функция AddRational

Но в автоматизации такой тип данных отсутствует, поэтому заменяем его на IDispatch: Функция AddRational принимает параметр типа указатель на IRational.

Const MaxArgumentAddRationalLength As SHORT = 1 Dim AddRationalArguments(MaxArgumentAddRationalLength - 1) As ELEMDESC
Dim RetvalAddRational As TYPEDESC
With RetvalAddRational .vt = VT_DISPATCH
End With
With AddRationalArguments(0) .tdesc.lptdesc = @RetvalAddRational .tdesc.vt = VT_PTR ' IDispatch Ptr .idldesc.wIDLFlags = IDLFLAG_FIN
End With

В обозревателе объектов параметр будет виден как Object, а передача его будет идти по ссылке ByRef.

Можно завести отдельный переменную под индексы и увеличивать её после добавлении нового элемента. Определение функции AddRational заполняется схожим образом с предыдущими, остаётся только изменить тип функции на INVOKE_FUNC, а при насаживании функции на интерфейс указать индекс на единицу больше предыдущей добавляемой сущности, в нашем случае это 4.

Закрытие интерфейса IRational

Получим интерфейс ITypeInfo от IRational, чтобы можно было унаследовать от него наш будущий класс Rational:

Dim pIRationalTypeInfo As ITypeInfo Ptr = Any
pCreateTypeInfoIRational->lpVtbl->QueryInterface(pCreateTypeInfoIRational, @IID_ITypeInfo, @pIRationalTypeInfo)

После настройки содержимого IRational необходимо запечатать всё его содержимое и уничтожить:

pCreateTypeInfoIRational->lpVtbl->LayOut(pCreateTypeInfoIRational)
pCreateTypeInfoIRational->lpVtbl->Release(pCreateTypeInfoIRational)

Добавление класса Rational в библиотеку

Добавление классов идёт намного быстрее, чем интерфейсов с функциями, потому что здесь требуется только добавить к классу интерфейсов‐пап:

' Добавляем класс
Dim pCreateTypeInfoRational As ICreateTypeInfo Ptr = Any
pCreateTypeLib->lpVtbl->CreateTypeInfo(pCreateTypeLib, @"Rational", TKIND_COCLASS, @pCreateTypeInfoRational) ' Устанавливаем GUID и текстовое описание
pCreateTypeInfoRational->lpVtbl->SetGuid(pCreateTypeInfoRational, @CLSID_Rational)
pCreateTypeInfoRational->lpVtbl->SetDocString(pCreateTypeInfoRational, @"Натуральная дробь") ' Наследуемся от IRational
Dim RefType As HREFTYPE = Any
pCreateTypeInfoRational->lpVtbl->AddRefTypeInfo(pCreateTypeInfoRational, pIRationalTypeInfo, @RefType)
pCreateTypeInfoRational->lpVtbl->AddImplType(pCreateTypeInfoRational, 0, RefType)
pIRationalTypeInfo->lpVtbl->Release(pIRationalTypeInfo) ' Запечатываем класс и уничтожаем его
pCreateTypeInfoRational->lpVtbl->LayOut(pCreateTypeInfoRational)
pCreateTypeInfoRational->lpVtbl->Release(pCreateTypeInfoRational)

Сохранение библиотеки

Пора сохранять результаты на диск:

pCreateTypeLib->lpVtbl->SaveAllChanges(pCreateTypeLib)
pCreateTypeLib->lpVtbl->Release(pCreateTypeLib) CoUnInitialize()

Выводы

Данный пример показывает, что библиотеки типов можно создавать программно без знания языка определения интерфейсов IDL и компилятора midl.exe, несмотря на всю громоздкость кода.

Такой подход используется в Visual Basic 6 для связывания с DLL через таблицу импорта. С помощью ICreateTypeLib2 и ICreateTypeInfo2 можно создавать не только описание COM‐интерфейсов и классов, но и обычных функций из DLL.

В серьёзных программах всегда следует проверять HRESULT возвращаемого значения и предпринимать меры когда что‐то пошло не так. Для упрощения кода удалены проверки на ошибки.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть