SysTools Logo SysTools


Аргумент VT_MISSING в PHP для COM-объектов


Разрабатывая для клиента программный комплекс на языке PHP (был указан как средство разработки в техническом задании к договору) пришлось столкнуться с проблемой передачи опционального аргумента при вызове метода из COM-объекта.

Создаваемое программное обеспечение должно было решать целый комплекс поставленных задач, в том числе предотвращать повторное подключение к базе данных Microsoft Access (файл *.MDB формата) когда с ней уже шла работа. Логичным и надёжным решением было опросить ADODB-объект о количестве подключённых пользователей[1] и, если их было более одного, то отсоединяться не предпринимая никаких дальнейших действий.

Рассмотрим на простом примере: создадим в Microsoft Access новую базу данных и сохраним её под именем "database.mdb" (создавать таблицы в ней не обязательно - для данного примера в этом нет необходимости). Теперь напишем небольшой код на Visual Basic Script (VBS) для проверки:

' database.vbs
Option Explicit
Dim db
Dim rs
Dim i
  Set db = CreateObject("ADODB.Connection")
  db.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=database.mdb"
  ' adModeRead (1)
  db.Mode = 1
  db.Open()
  ' adSchemaProviderSpecific (-1), *missing*, JET_SCHEMA_USERROSTER
  Set rs = db.OpenSchema(-1, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}")
  ' manual counting since rs.RecordCount is -1 (not supported)
  i = 0
  While (Not rs.EOF)
    i = i + 1
    rs.MoveNext()
  WEnd
  rs.Close()
  rs = Empty
  db.Close()
  db = Empty
  Wscript.Echo i

Если поместить код выше в текстовый файл с именем "database.vbs" в тот же каталог где лежит созданная ранее база данных "database.mdb", то при запуске через двойной щелчок мышкой в Windows, будет показано информационно окно с цифрой "1". Это значит, что на момент запуска скрипта к базе данных было всего одно подключение (собственно, сам скрипт). Но если открыть базу данных в Access, а уже потом запустить скрипт, то будет показано число "2" (скрипт и Access) - т.е. всё работает, как и должно.

Но вот при попытке перенести этот код на PHP возникла проблема. Если присмотреться к тексту скрипта повнимательнее, то можно заметить, что у метода OpenSchema отсутствует второй аргумент - там ничего нет:

OpenSchema(-1, , "{947bb102-5d43-11d1-bdbf-00c04fb92675}")

В PHP, в отличие от Visual Basic, подобная конструкция невозможна и не поддерживается.

Попытка вписать в это место значения 1, 0, -1, null, true, false и даже array(), а также некоторые другие, вызывала ошибку:

Object or provider is not capable of performing requested operation

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

Немного поискав информацию по Интернету, удалось найти ответ в английской Wikipedia: Missing arguments are actually a particular Error value titled "parameter not found".[2] После этого, наконец-то, стало понятно что именно нужно искать. Следующая найденная ссылка уже содержала подробности: VT_EMPTY - No value was specified. If an optional argument to an Automation method is left blank, do not pass a VARIANT of type VT_EMPTY. Instead, pass a VARIANT of type VT_ERROR with a value of DISP_E_PARAMNOTFOUND.[3]

Наконец-то всё прояснилось - для создания пустого аргумента нужен Variant с типом VT_ERROR и значением DISP_E_PARAMNOTFOUND. Делаем поиск в заголовочных файлах любого компилятора языка C для Windows и находим то что нужно в файле "winerror.h":

//
// MessageId: DISP_E_PARAMNOTFOUND
//
// MessageText:
//
//  Parameter not found.
//
#define DISP_E_PARAMNOTFOUND _HRESULT_TYPEDEF_(0x80020004L)

Теперь уже в справке по PHP смотрим как создавать и работать с Variant и пробуем:

<?php // database.php
  $vtMissing = new VARIANT(0x80020004, VT_ERROR);

После чего можно было бы уже праздновать победу, но нет:

Fatal error: Uncaught exception 'com_exception' with message
  'Variant type conversion failed: Type mismatch.'
  in Z:\database.php:2
Stack trace:
#0 Z:\database.php(2): variant->variant(2147614724, 10)
#1 {main}
  thrown in Z:\database.php on line 2

И тут выясняется, что в PHP 5, на котором шла разработка, создание Variant с типом VT_ERROR невозможно. Причём, даже последняя версия PHP 7 не могла этого сделать прерываясь той же самой ошибкой. Любые попытки "обмануть", в том числе через создание другого типа, а затем его смену, точно также не помогали:

<?php // database.php
  $vtMissing = new VARIANT(0x80020004, VT_UINT); // ok
  $vtMissing = variant_cast($vtMissing, VT_ERROR); // fatal error
  variant_set_type($vtMissing, VT_ERROR); // also fatal error

Не получалось "обмануть" даже через serialize() похожего типа и unserialize() назад с исправлениями (тут ещё выяснилось, что serialize() для Variant не может корректно работать, поэтому его принудительно запретили применять для этого типа в одной из последних версий PHP 7). Попасть в тупик всего в шаге от решения очень досадно.

Помимо безуспешно исследования Интернета в поисках решения, пришлось также ещё раз вернуться к сохранённому когда-то[4] справочнику PHP 5 и описанию Variant там. Внимание привлёк пример старого и нового кода (незначительно изменён ниже для удобства представления):

<?php
  // Example #1 Variant example, PHP 4.x style
  $v = new VARIANT(42);
  echo 'The type is '.$v->type.PHP_EOL;
  echo 'The value is '.$v->value.PHP_EOL;

  // Example #2 Variant example, PHP 5 style
  $v = new VARIANT(42);
  echo 'The type is '.variant_get_type($v).PHP_EOL;
  echo 'The value is '.$v.PHP_EOL;

Т.е. в PHP 4 к типу и значению Variant можно было обращаться как к свойствам объекта. Возможно, что и проверки на соответствие значения типу там не было. Тут же загрузив с официального сайта PHP museum последнюю версию PHP 4 (4.4.9 из архива "php-4.4.9-Win32.zip") - удалось проверить это предположение.

Оказалось что, действительно, на PHP 4 работает даже обычный код "new VARIANT(0x80020004, VT_ERROR)".

В итоге полный текст кода программы на PHP получился следующим:

<?php // database.php
  $db = new COM('ADODB.Connection');
  $db->ConnectionString = 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source=database.mdb';
  // adModeRead (1)
  $db->Mode = 1;
  $db->Open();
  // next line work ONLY on PHP 4 (PHP 5, 6 and even 7 are BROKEN!)
  $vtMissing = new VARIANT(0x80020004, VT_ERROR);
  // adSchemaProviderSpecific (-1), *missing*, JET_SCHEMA_USERROSTER
  $rs = $db->OpenSchema(-1, $vtMissing, '{947bb102-5d43-11d1-bdbf-00c04fb92675}');
  // manual counting since rs.RecordCount is -1 (not supported)
  $i = 0;
  while (!$rs->EOF) {
    $rs->MoveNext();
    $i++;
  }
  $rs->Close();
  $rs = null;
  $db->Close();
  $db = null;

  echo $i;

После чего весь проект был переведён на PHP 4. Из недостатков связанных с понижением версии можно отметить только несущественное замедление работы и необходимость внесения некоторого количества изменений в уже написанный код для его адаптации к более ранней версии PHP.


Сноски и полезные ссылки (отмечены как [*]):

[1] Microsoft Docs: Use ADO to Return a List of Users Connected to a Database

[*] Microsoft Docs: How to determine who is logged on to a database by using Microsoft Jet UserRoster in Access 2000 (архивная копия)

[2] Wikipedia: Variant type

[*] The Old New Thing: One of my favorite error codes: Optional parameter missing

[*] Microsoft Docs: How to pass optional arguments to a method on an ActiveX Control (архивная копия)

[3] Tenouk: The VARIANT and VARIANTARG stucture, union and typedef used in the MFC and IDispatch interface programming

[*] Microsoft Docs: VARIANT Type Constants

[4] PHP: VARIANT (последняя архивная копия, где ещё был пример для PHP 4)


2020.09.30


[ Статьи ]