Создание COM-компонентов с помощью скриптовых языков


Источник: «Технология Клиент-Сервер»
Опубликовано: 25.04.2001

Главными целями разработки СОМ были создание распределенной компонентной архитектуры, взаимодействие компонентов во время исполнения, поддержка автоматизации приложений и совместимость со скриптовыми языками. СОМ старается быть настолько терпимым к языку, насколько это возможно, и теперь вы можете использовать скрипты не только для использования готовых СОМ-объектов, но и для создания новых. Windows Script Components (WSC) позволяют упаковывать скрипты для использования в качестве СОМ-компонентов.

Крупный план

В общем, WSC - это просто XML-файлы, содержащие код скриптов, и прикидывающиеся СОМ-классами. В этой статье мы покажем, как работать с такими файлами. XML - язык разметки, схожий с HTML и позволяющий вставлять теги в документ. Но XML гораздо более гибок. Хотя и XML, и HTML используются для описания форматирования и экранных характеристик документа, XML может гораздо больше. Например, он может описать семантику и организационную структуру документа.

Создание компонентов на базе скриптов влечет за собой написание небольшого XML-файла, соответствующего синтаксису WSC. Листинг 1 показывает формат такого файла. Код этого файла представляет инфраструктуру скрипт-компонента. Обратите внимание на теги.

Листинг 1

<?xml version="1.0" encoding="windows-1251" ?>
 <component>
 <?component error="true" debug="true"?>
 
 <registration
     description="BareBones"
     progid="BareBones.WSC"
     version="1.00"
     classid="{eeb9e710-9c22-11d3-8069-367a6d000000}"
 >
 </registration>
 
 <public>
 </public>
 
 <script language="VBScript">
 <![CDATA[
 
 ]]>
 </script>
 
 </component>

Выражение в первой строке Листинга 1

<?xml version="1.0" encoding="windows-1251" ?>

говорит скрипт-машине, что XML-файл соответствует стандарту XML 1.0. (Соответствие XML отключается простым выкидыванием этой строки). При включенном соответствии XML имена элементов и атрибуты зависимы от регистра, а значения атрибутов должны быть заключены в вопросительные знаки. Также, зарезервированные XML-символы типа < и > должны быть четко разграничены при использовании в скрипте. Например, знак < может означать "меньше" в скриптовом языке, но он еще означает начало XML-тега. Особое внимание обратите на строку encoding="windows-1251". В принципе, без нее можно было бы обойтись, но руских букв в скрипте при этом использовать будет нельзя. Windows-1251 можно заменить на UTF-8, KOI8R или еще что нибудь, но зачем?..

Вот набор обязательных тегов:

Чтобы опробовать эту технологию, создадим конвертер целых чисел в строку прописью (на VBScript) и конвертер миль в километры (на Jscript).

Компонент-конвертер целых чисел в строку (VBScript)

Проект будет состоять из двух простых функций: L2TR и Convert. Компонент будет также иметь свойства TextRus - текст прописью на русском языке, доступное только на чтение, и Value - целое число, которое надо перевести в строку прописью, доступное как для чтения, так и для записи. Листинг 2 содержит исходный текст компонента. Функции с префиксами get_ и put_ и именами, совпадающими с именами описанных свойств, являются реализацией методов доступа для этих свойств.

Листинг 2

<?xml version="1.0" encoding="windows-1251" ?>
 <component>
 <?component error="true" debug="true"?>
 
 <registration
     description="Converter VBScript Component"
     progid="Converter.Long2TxtRus"
     version="1.00"
     classid="{5B4C45F9-B9E9-4789-AF25-3312D3D875A6}"
 >
 </registration>
 
 <public>
     <property name="Value" dispid = "1">
         <get/>
         <put/>
     </property>
     <property name="TextRus" dispid = "2">
         <get/>
     </property>
     <method name="Convert" dispid = "3">
     </method>
     <method name="L2TR"  dispid = "4">
         <PARAMETER name="nVal"/>
     </method>
 </public>
 
 <script language="VBScript">
 <![CDATA[

dim lValue
lValue = 0
dim sTextRus
sTextRus = ""
 
function get_Value()
     get_Value = lValue
end function

function put_Value(newValue)
     lValue = newValue
end function
    
function get_TextRus()
     get_TextRus = sTextRus
end function

Sub Convert()
    sTextRus = L2TR(lValue)
End Sub

function L2TR(nVal)
   Dim str1
   str1 = Trim(IntrnalL2TR(nVal))
   L2TR = UCase(Left(str1, 1)) & Mid(str1, 2)
End function

function IntrnalL2TR(nVal)
   mass1 = Array("один", "два", "три", "четыре", "пять",  "шесть", _
                      "семь", "восемь", "девять", "десять", "одинадцать", _
                      "двенадцать", "тринадцать", "четырнадцать",  _
                      "пятнадцать", "шестнадцать", "семнадцать", _
                      "восемнадцать", "девятнадцать")
   mass2 = Array("двадцать", "тридцать", "сорок", "пятьдесят",  _
                      "шестьдесят", "семьдесят", "восемьдесят", "девяносто")
   str1 = ""
   Dim nVal1
   If (nVal >= 1000000000) Then
      nVal1 = Int(nVal / 1000000000)
      str1 = str1 + IntrnalL2TR(nVal1) + "миллиард"
      nVal1 = Int(nVal1 Mod 100)
      If (nVal1 < 5 Or nVal1 > 20) Then
         nVal1 = Int(nVal1 Mod 10)
         If (nVal1 = 1) Then
            str1 = str1 + ""
         ElseIf (nVal1 > 1 And nVal1 < 5) Then
            str1 = str1 + "а"
         Else
            str1 = str1 + "ов"
         End If
      Else
         str1 = str1 + "ов"
      End If

      nVal1 = Int(nVal - nVal1 * 1000000000)
      str1 = str1 + " " + IntrnalL2TR(nVal1)
   ElseIf (nVal >= 1000000) Then
      nVal1 = Int(nVal / 1000000)
      str1 = str1 + IntrnalL2TR(nVal1) + "миллион"
      nVal1 = Int(nVal1 Mod 100)
      If (nVal1 < 5 Or nVal1 > 20) Then
         nVal1 = Int(nVal1 Mod 10)
         If (nVal1 = 1) Then
            str1 = str1 + ""
         ElseIf (nVal1 > 1 And nVal1 < 5) Then
            str1 = str1 + "а"
         Else
            str1 = str1 + "ов"
         End If
      Else
         str1 = str1 + "ов"
      End If

      nVal1 = Int(nVal Mod 1000000)
      str1 = str1 + " " + IntrnalL2TR(nVal1)
   ElseIf (nVal >= 1000) Then
      nVal1 = Int(nVal / 1000)
      str1 = str1 + IntrnalL2TR(nVal1) + "тысяч"
      nVal1 = Int(nVal1 Mod 100)
      If (nVal1 < 5 Or nVal1 > 20) Then
         nVal1 = Int(nVal1 Mod 10)
         If (nVal1 = 1) Then
            str1 = ATREPL("один", str1, "одна")
            str1 = str1 + "а"
         ElseIf (nVal1 > 1 And nVal1 < 5) Then
            str1 = ATREPL("два", str1, "две")
            str1 = str1 + "и"
         End If
      End If
      nVal1 = Int(nVal Mod 1000)
      str1 = str1 + " " + IntrnalL2TR(nVal1)
   ElseIf (nVal >= 100) Then
      nVal1 = Int(nVal / 100)
      If (nVal1 = 1) Then
        str1 = str1 + "сто "
      Else
        If (nVal1 = 2) Then
          str1 = str1 + "двести "
        Else
          If (nVal1 >= 3 And nVal1 <= 4) Then
              str1 = str1 + mass1(nVal1 - 1) + "ста "
          Else
             If (nVal1 >= 5) Then
               str1 = str1 + mass1(nVal1 - 1) + "сот "
             Else
                str1 = str1 + ""
             End If
          End If
        End If
      End If
       nVal1 = Int(nVal Mod 100)
     str1 = str1 + IntrnalL2TR(nVal1)
   ElseIf (nVal >= 20) Then
      str1 = str1 + mass2(Int(nVal / 10) - 2) + " "
      nVal1 = Int(nVal Mod 10)
      str1 = str1 + IntrnalL2TR(nVal1)
   ElseIf (nVal >= 1 And nVal <= 19) Then
      str1 = str1 + mass1(Int(nVal) - 1)
   End If
    
   IntrnalL2TR = Trim(str1) & " "
End function

function ATREPL(str0, str1, str2)
    'ATREPL("два", str1, "две")
    pos = InStr(str1, str0)
    Const LenTwenty = 8
    If pos > 0 Then
        If Mid(str1, pos, LenTwenty) = "двадцать" Then
            ATREPL = Left(str1, pos - 1) + str2 + Mid(str1, pos + Len(str0))
        Else
            ATREPL = str1
        End If
    Else
        ATREPL = str1
    End If
End function
 ]]>
 </script>
</component>

В добавление к инфраструктуре скрипта, показанной в Листинге 1, конвертер из Листинга 2 включает теги <property> и <method> и некоторый код внутри тега <component>. Тег <property> означает свойство объекта, доступные клиентам. Свойство доступно для записи, если в его описании присутствует тег <put/> и для чтения - если <get/>. Тег <method> описывает доступные методы объекта и их параметры (через тег <PARAMETER>). Информация, содержащаяся в этой части XML, чисто декларативна. Настоящие определения свойств и методов находятся в секции Script.

Реальный код скрипта начинается с тега <script>. Поскольку тег <script> указывает на использование VBScript, переменные и методы определяются через VBScript-выражение dim. Глобальные переменные (доступные во всем скрипте) объявляются в самом начале, до объявления первого метода. Код, размещенный вне методов, будет аналогичен коду, размещенному в конструкторе объектно-ориентированного языка. К сожалению, аналога деструктору не предусмотрено.

Протестировать только что созданный компонент можно опять же с помощью VBScript. Для этого создайте .vbs-файл следующего содержания:

Dim c
Set c = CreateObject("Converter.Long2TxtRus")
Err.Clear
On Error Resume Next
Dim n
n = CLng(InputBox("Введите номер", "Script Component Test", 1234567890))
If Err.Number <> 0 Or n = 0 Then
    MsgBox "Неправильно набран номер. Введите целое число, не равное нулю."
Else
    MsgBox n & " пишется так: " & vbCrLf &  c.L2TR(n)
End If

Конвертер миль в километры на JScript

В файле для второго компонента (см. Листинг 3) определяет свойства CumeMiles и CumeKilometers, доступные для записи и чтения и функции MilesToKilometers и KilometersToMiles.

Листинг 3

<?xml version="1.0" encoding="windows-1251"?>
 <component>
 <?component error="true" debug="true"?>
 
 <registration
     description="DistanceConverterJScript"
     progid="DistanceConverterJScript.WSC"
     version="1.00"
     classid="{f7b9ed70-9dd4-11d3-806a-a6b1d8000000}"
 >
 </registration>
 
 <public>
     <property name="CumeMiles">
         <get/>
         <put/>
     </property>
     <property name="CumeKilometers">
         <get/>
         <put/>
     </property>
     <method name="MilesToKilometers">
         <PARAMETER name="Miles"/>
     </method>
     <method name="KilometersToMiles">
         <PARAMETER name="Kilometers"/>
     </method>
 </public>
 
 <script language="JScript">
 <![CDATA[
 
 var description = new DistanceConverterJScript;
 
 function DistanceConverterJScript()
 {
     this.get_CumeMiles = get_CumeMiles;
     this.put_CumeMiles = put_CumeMiles;
     this.get_CumeKilometers = get_CumeKilometers;
     this.put_CumeKilometers = put_CumeKilometers;
 
     this.MilesToKilometers = MilesToKilometers;
     this.KilometersToMiles = KilometersToMiles;
 }
 
 var CumeMiles = 0;
 var CumeKilometers = 0;
 
 function get_CumeMiles()
 {
     return CumeMiles;
 }
 
 function put_CumeMiles(newValue)
 {
     CumeMiles = newValue;
 }
 
 function get_CumeKilometers()
 {
     return CumeKilometers;
 }
 
 function put_CumeKilometers(newValue)
 {
     CumeKilometers = newValue;
 }
 
 function MilesToKilometers(Miles)
 {
     CumeMiles += Miles;
     CumeKilometers += Miles * 5 / 2;
     return Miles *5 / 2;
 }
 
 function KilometersToMiles(Kilometers)
 {
     CumeKilometers += Kilometers;
     CumeMiles += Kilometers * 2 / 5;
     return Kilometers * 2 / 5;
 }
 
 ]]>
 </script>
 
 </component>

Главное различие между компонентами на VBScript и JScript обнаруживается в скрипт-секции WSC-файла.

Думаю, вам не составит труда собственноручно написать файл, запускающий этот компонент.

Выполнение

Теперь, когда у вас есть эти два файла с неким кодом скрипта внутри, как сделать из них СОМ-объекты? На этот вопрос проще всего ответить, посмотрев вхождения реестра для компонента, написанного на скриптовом языке. Но предварительно его надо зарегистрировать. О том, как это сделать, можно прочесть в разделе "Регистрация скрипт-объекта", а пока расмотрим описание, полученое из нашего реестра:

HKCR
    CLSID
       {F3DA80D0-9B63-11D3-8069-367A6D000000}
          InprocServer32 = c:\WinNT\System32\srcobj.dll
             ThreadingModel = "Apartment"
          ProgID = "DistanceConverterJScript.WSC.1.00"
          ScriptletURL = "file://D:DistanceConverterJScript.wsc"

За превращение XML-файла в СОМ-объект отвечает DLL с названием SRCOBJ.DLL. Когда скрипт-компоненты регистрируются как СОМ-объекты - наиболее важен здесь ключ реестра InprocServer32. Чтобы заставить скрипт работать, это ключ должен указывать на SRCOBJ.DLL. В дополнение к ключу InprocServer32 имеется ключ ProgID и ключ ScripletURL. В ключе ScripletURL хранится путь к WSC-файлу.

Для использования скриптового объекта ваш код просто вызывает обычные методы активации СОМ (CoCreateInstance для разработчиков, использующих C++, CreateObject - для использующих Visual Basic и скрипты) - так же, как и для любого другого СОМ-объекта. COM-runtime просматривает ключ CLSID, находит DLL, которую нужно загрузить, используя значение, ассоциированное с ключом InprocServer32, и загружает оболочку скрипт-объекта (SRCOBJ.DLL). Затем DLL-оболочка скрипт-объекта загружает файл, указанный в ключе ScriptletURL и перенаправляет вызовы методов в скрипт (реализуя Idispatch-интерфейс на базе описания из wsc-файла. Все это делается на базе технологии Active Scripting. То есть, SCROBJ.DLL реализует IActiveScriptSite и управляет выбранной машиной через ее интерфейсы IActiveScript и IActiveScriptParse.

Регистрация скрипт-объекта

Написав файл скрипт-объекта, нужно внести информацию в реестр. Это можно сделать несколькими способами. Можно использовать regsvr32 - утилиту используемую для регистрации любого СОМ-класса - или контекстное меню в Windows Explorer.

Новая версия regsvr32 поставляется со скрипт-компонентами. Командная строка выглядит примерно так:

regsvr32 file:\\d:\DistanceConverterJScript.wsc

Если новой версии у вас нет, вы можете использовать старую версию этой утилиты следующим образом:

regsvr32 scrobj.dll /n /i:file:\\d:\DistanceConverterJScript.wsc

Но самый простой путь зарегистрировать компонент - найти WSC-файл в Windows Explorer, щелкнуть по нему правой кнопкой мыши и выбрать Register из контекстного меню.

Скрипт-компоненты и библиотеки типов

Клиенту обычно все равно, что делается за завесой интерфейсов. Скрипт-компоненты - это просто обычные СОМ-компоненты в одеждах скриптов. Естественно, если у вас есть компонент, раньше или позже у вас появиться клиент, желающий использовать библиотеку типов, описывающую ваш компонент.

Когда это случится, вы должны будете создать для вашего скрипт-компонента библиотеку типов одним из трех способов. Первый - программно создать библиотеку типов используя runtime-генератор библиотеки типов скрипт-компонента. В скрипт-компонент можно встроить генератор библиотеки типов с помощью CreateObject, дающей указатель на генератор библиотеки типов. Затем вы указываете генератору библиотеки типов на WSC-файл, присваиваете ей имя и просите генератор создать ее. Когда вы помещаете код регистрации в секцию <registration> WSC-файла, библиотека типов будет создана при регистрации скрипт-компонента обычным путем.

Второй способ - попросить WSC-файл создать библиотеку типов, используя утилиту RUNDLL32. SCROBJ.DLL имеет точку входа с именем GenerateTypeLib, которая сгенерирует библиотеку типов на основе данных параметров командной строки. Например, следующая командная строка создаст библиотеку типов для скрипт-компонента:

rundll32.exe c:\winnt\system32\scrobj.dll,GenerateTypeLib
    -name:DistanceConverterJScriptTLib  
    -file:d:\DistanceConverterJScript.tlb 
    -doc:\"DistanceConverterJScript typelib\" 
    -guid:{6576834a-a252-11d1-9fa1-00a0c90fffc0} 
        -major:1  -minor:0 
    -URL:d:\components\MyComponent.wsc

Наконец, третий и простейший путь - найти файл скрипта в Windows Explorer, щелкнуть по нему правой кнопкой мыши и выбрать Generate Type Library из контекстного меню.

Windows Script Component Wizard

Как всегда с СОМ, создание скрипт-компонентов связано с написанием прорвы кода. Это достаточная причина, чтобы использовать Wizard для создания скрипт-компонентов. Его можно получить с WWW-сайта Microsoft: http://msdn.microsoft.com/scripting.

Первый экран просит ввести имя вашего компонента и указать каталог для размещения исходного текста. Второй экран предлагает выбрать используемый скриптовый язык. Вы можете выбрать VBScript, JScript или любой другой скриптовый язык, если у вас, разумеется, установлена соответствующая скрипт-машина.

Вы можете также приказать wizard'у добавить к объекту образ действий Dynamic HTML или обработчик ASP. Можно включить или отключить отладку и обработку ошибок. Обработка ошибок заставляет скрипт-машину генерировать сообщения об ошибках исполнения и синтаксиса, а опция отладки вызывает отладчик скрипта в случае ошибки.

Наконец, Script Component Wizard выводит три экрана для добавления к компоненту свойств, методов и событий. В результате получается простыня кода, что экономит массу времени при разработке.

Выводы

СОМ позволяет совместно использовать ПО, даже если оно было написано на разных языках. Почему скрипты должны отличаться? Технология Windows Script Components позволяет писать код скрипта, а представлять его как COM-объект. Все, что нужно - простой XML-файл с нужными тегами и некий код скрипта, позволяющий скрипт-машине (SCROBJ.DLL) разобрать файл и представить код скрипта как объект. Если добавить в тег <registration> "remotable=true", то вы получите полноценный DCOM-объект, который можно вызывать на удаленном компьютере.


Впервые статья была опубликована в журнале <Технология Клиент-Сервер>.
Эту и множество других статей по программированию, разработке БД, многоуровневым технологиям (COM, CORBA, .Net, J2EE) и CASE-средствам вы можете найти на сайте www.optim.su и на страницах журнала.