RDM/2 The Russian Electronic Developer Magazine  
RDM/2 Русский электронный журнал разработчика  
ДомойОт редактораПишите намОбратная связьRU/2

Конфигурация: кое-какие проблемы и решения.

Вступление

Эта статья посвящена некоторым аспектам работы с файлами конфигурации (ini-файлам, профилям, registry и прочим). На фоне стандартных (распространенных) методов организации файлов настроек (конфигурации) описывается разработанный для конкретной задачи (Буровой тренажер под OS/2), но имеющий вполне универсальное значение язык описания информационных (неисполняемых) объектов, а также средства (API) использования его в среде OS/2.

Всем известны самые примитивные средства описания в виде пар "параметр=значение", столь любимые программистами в среде различных *ix (автор, конечно же, не подымет руку на охаивание синтаксиса правил разбора в sendmail.cf - сами знаете за что), от которых не слишком далеко ушли структуры (синтаксис) файлов config.sys и autoexec.bat, возможное содержание которых достаточно хорошо всем знакомо. Ниже, в качестве примера, приведен кусок моего файла config.sys:

  SET EPMPATH=C:\OS2\APPS;C:\opendoc\BIN;
  PROTECTONLY=NO
  SHELL=C:\OS2\MDOS\COMMAND.COM C:\OS2\MDOS
  FCBS=16,8
  RMSIZE=640
  DEVICE=C:\OS2\MDOS\VEMM.SYS
  DOS=LOW,NOUMB
  DEVICE=C:\OS2\MDOS\VXMS.SYS /UMB
  DEVICE=C:\OS2\MDOS\VDPMI.SYS
  DEVICE=C:\OS2\MDOS\VDPX.SYS
  DEVICE=C:\OS2\MDOS\VWIN.SYS
  DEVICE=C:\OS2\MDOS\VW32S.SYS
Критиковать такой синтаксис несложно - он не позволяет синтаксически отделять описания разных объектов в одном файле. Зато его использование примитивно реализуется программно и во многих случаях оправдано. Но не во всех случаях возможно.

Незначительное расширение такой синтаксис получил в ini-файлах от Windows X.XX (например, 3.11) за счет введения секций. Именованные секции позволяют структурировать описания и формулировать запросы на элементы описания уже по двум параметрам "имя секции"-"имя параметра", что гораздо удобнее. Вот пример из файла SYSTEM.INI для WIN-OS2 сессии:

  [keyboard]
  subtype=
  type=4
  keyboard.dll=kbdru.dll
  oemansi.bin=xlat866.bin
  typeofswitch=2
  secondkeyb.dll=kbdusx.dll

  [boot.description]
  keyboard.typ=Enhanced 101 or 102 key US and Non US keyboards
  mouse.drv=Microsoft, or IBM PS/2
  network.drv=No Network Installed
  language.dll=Russian
  system.drv=MS-DOS System
  codepage=866
  woafont.fon=Russian (866)
К недостаткам этого метода можно отнести синтаксическую неопределенность при работе с переменным числом записей в рамках одной секции и отсутствие вложенности секций, что иногда полезно, т.к. не все информационные объекты линейны. Конечно, все это можно так или иначе реализовать программно, но эта реализация будет не естественным следствием идеи, заложенной в синтаксис, а лишь свидетельством изворотливости программиста.

Здесь следует отметить, что критика и далее будет бить по одной-двум позициям, намечающим путь к совершенству, не более того.

Следующим шагом в развитии идеи скриптов-описателей стали registry от Windows9X и далее, а также ini-файлы в OS/2. Привести их примеры не представляется возможным в силу их непечатности. Отметим следующее: они подразумевают древовидную структуру элементов (развитие идеи вложенности) и позволяют хранить объекты любого типа за счет введения бинарных объектов, трактуемых теми, кто их использует.

Для критики существующих реализаций подобного ресурса (в OS/2 он реализован в WINPRF.DLL) необходимо уточнить требования к синтаксису файлов конфигурации вообще.

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

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

Эти требования повлекли за собой создание цепочки информационно связанных между собой программных средств:

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

Вышеописанные проблемы со сценарием и являются, по мнению авторов, вполне содержательной критикой использования Profile API для сопровождения сложных конфигураций.

Все это подвигло нас на формулировку следующих требований к концепции конфигурирования:

Эти требования и были реализованы в предлагаемой подсистеме работы с файлами конфигурации. Она предлагает синтаксис, реализацию базовых объектов, WPS-объект визуализации и печати, а также API для записи и чтении их программно.

Неформальное описание

Любой файл конфигурации рассматривается как набор полей некоторых типов. Типы могут быть простыми и составными. Таким образом сам файл конфигурации является объектом некоего составного типа (класса).

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

Литералы используются такие же, как в C++ : \xx, \\, \n, \t, etc, а также специальные литералы %.

Комментарии обозначаются сочетанием символов //, /* */, правила их использования аналогичны С++. Версионность поддерживается вхождением (необязательным) версии в имя класса -<classname>[<version>]

Система оперирует следующими объектами:

классификаторы transient берется из класса как значение по умолчанию; может быть изменен в каждом представителе
базовые типы данных int целое (4 байта)
double вещественное (8 байт)
string строка символов (не обязательно ограничивается \0)
group массив произвольных объектов - позволяет доступ по номеру элемента
namedgroup основан на базе group, используется длп описания диалога и печати
базовые типы данных для визуализации и печати IntField класс редактируемого и печатаемого int
DoubleField класс редактируемого и печатаемого double
StringField класс редактируемого и печатаемого string
TextField класс редактируемого и печатаемого текстового окна (textarea)
RadioToggleField класс выбора в диалоге с помощью RadioButton
ComboTogleField класс выбора из списка в диалоге с помощью ComboBox
ReferenceField класс-ссылка - в диалоге редактируется с помощью drag&drop

Далее приведены их описания в предлагаемом синтакисе :

  IntField = {
     string title = ""  // название, выводимое при печати и редактировании в диалоге
     int value = 0      // значение поля
     int min = 0        // контролируемое в диалоге минимальное значение целого value
     int max = 0        // контролируемое в диалоге максимальное значение целого value
     int delta = 0      // шаг автоизменения при редактировании в диалоге
  }

  DoubleField = {
     string title = ""  // название, выводимое при печати и редактировании в диалоге
     double value = 0   // значение поля
     double min = 0     // контролируемое в диалоге минимальное значение вещественного value
     double max = 0     // контролируемое в диалоге максимальное значение вещественного value
     double coef = 0    // коэффициент перевода (для случая редактирования в одних единицах
                        // измерения, а использования в других)
     double delta = 0   // шаг автоизменения при редактировании в диалоге
     int decimals = 0   // количество выводимых при печати и редактировании знаков
                        // после десятичной точки
  }

  StringField = {
     int max = 0        // максимальное количество символов в строке
     string title = ""  // название, выводимое при печати и редактировании в диалоге
     string value = ""  // если строка ограничена одинарными кавычками '...' ограничитель строки
                        // (\0 не ставится)
  }

  TextField = {         // аналогичен StringField, но представляет собой textarea (MLE)
     int max = 0
     string title = ""
     string value = ""
  }

  RadioToggleField = {     // название говорит само за себя - реализация диалогового элемента
     string title = ""
     int value = 0         // содержит номер выбранной строки
     group elements = { }  // набор строк типа string
  }

  ComboToggleField = {     // аналогично предыдущему для объекта ComboBox
     string title = ""
     int value = 0
     group elements = { }
  }

  ReferenceField = {       // Объект-ссылка на другой объект. В диалоге реализует drag&drop
     string title = ""
     string class = "void" // имя класса, объекты которого могут заполнять данное поля
                           // (их может быть много - class0|ckass1|...|classN)
     string value = ""     // полное имя файла, содержащего объект
     int value2 = 0        // WPS-objectid
  }
Класс ReferenceField позволяет использовать множество настроек в одной, если, например, программный продукт состоит из множества задач, каждая из которых имеет свою настройку с индивидуальной и общей частью, и наоборот, если настройки одного объекта используютсп во множестве других.

В качестве чисто практического (для описания изображения пород), реализован класс масски заливки:

  ColorMaskField = {
     string title = ""
     int fcolor = 0      // цвет переднего плана
     int bcolor = 0      // цвет фона
     string value = ''   // маска заливки 8*8
  }
Также можно заметить, что существуют и предустановленные данные: На основе этих простых и сложных типов можно создавать свои классы скриптов и их реализации использовать для настройки задач. Так, например, выглядит класс-описание забойного двигателя:
  DEngine drill={
    string title="Забойный двигатель"

    DoubleField diam_zd    ={ title="Диаметр заб.двигателя" coef=0.001 decimals=1 delta=0.1 min=30 max=400}
    DoubleField leng_zd    ={ title="Длина заб.двигателя" coef=1 decimals=2 delta=0.01 max=50}
    DoubleField q_zd       ={ title="Расход заб.двигателя" coef=1000 decimals=1 delta=0.1 min=30 max=400}
    DoubleField dens_zd    ={ title="Плотность жидкости" coef=1000 decimals=2 delta=0.01  min=0.8 max=2.5}
    DoubleField loss_px_zd ={ title="Потери давления на холостом ходу" coef=98100 min=1 max=200}
    DoubleField freq_nx_zd ={ title="Частота вращения на холостом ходу" coef=0.01666 min=10 max=999}
    DoubleField loss_pt_zd ={ title="Потери давления при торможении" coef=98100 min=1 max=200}
    DoubleField moment_t_zd={ title="Тормозной момент" coef=10 min=1 max=1500}
    ComboToggleField drop   ={
      string title ="Тип двигателя"
      group elements = {
        string = "турбобур с постоянной линией"
        string = "турбобур с падающей линией"
        string = "объемный двигатель"
        }
      }
  }
А так выглядит описание конкретного забойного двигателя (/* комментарий */ в первой строке необходим для интерпретации текста как скрипта):
  /* XObject */
  DEngine = {
    diam_zd = {value = 30.0000}
    leng_zd = {value = 0.0000}
    q_zd = {value = 30.0000}
    dens_zd = {value = 0.8000}
    loss_px_zd = {value = 1.0000}
    freq_nx_zd = {value = 10.0000}
    loss_pt_zd = {value = 1.0000}
    moment_t_zd = {value = 1.0000}
    drop = {value = 0}
  }
Данный синтаксис показывает возможность изменения в каждом конкретном объекте и вспомогательных полей: min, max и прочих.

Как восклицала Алиса Кэррола - "Кому нужна книжка без картинок и диалогов!" Обратимся к заявленной самодиалоговости и документированности, т.е визуализации и печати.

Создан, как говорилось ранее, WPS-объект, интерпретирующий специально организованные части описания классов как страницы своих свойств (property). Для этого в описание класса вносятся специальные описания - group с именами pages (для диалога) и sections (для печати).

Вот как выглядит описывающая диалог часть класса:

  group pages = {
    namedgroup = { // описание страницы диалога
      title = "%MajorTab" // символ % означает MajorTab
      elements = {
      string = "имя 1" // элемент класса
      string = "имя 2"
      ...
      string = "имя N"
    }
  }
  namedgroup = { // вторая страница диалога
    title = "MinorTab страницы"

    elements = {
       string = "имя 1"
       string = "имя 2"
              ...
       string = "имя N"
       }
    }
    ...          // другие страницы диалога
  }
Вот как выглядит (и что порождает) часть описания класса забойного двигателя (DEngine), относящаяся к диалогу:
  DEngine drill={
    ...
    group pages = {
      namedgroup = {
        string title = "%Забойный двигатель"
        group elements = {
          string string = "drop"
          string string = "diam_zd"
          string string = "leng_zd"
          string string = "q_zd"
          string string = "dens_zd"
          string string = "loss_px_zd"
          string string = "freq_nx_zd"
          string string = "loss_pt_zd"
          string string = "moment_t_zd"
        }
      }
    }
  }
Обратите внимание на вторую закладку - Тип. Она создается всегда и дает возможность явно задать класс объекта и выбрать способ сохранения (текстовый/двоичный). Последнее позволяет легко подправить руками любое поле объекта с помощью текстового редактора. И наоборот, любой объект может быть создан в текстовм редакторе, а затем сохранен в двоичном виде.
Для описания печати объекта используется аналогично организованная group sections; и вот, что получается при печати:
  DEngine drill={
    ...
    group sections={
      namedgroup={
        title = "Забойный двигатель"
        elements={
          string ="drop"
          string ="diam_zd"
          string ="leng_zd"
          string ="q_zd"
          string ="dens_zd"
          string ="loss_px_zd"
          string ="freq_nx_zd"
          string ="loss_pt_zd"
          string ="moment_t_zd"
        }
      }
    }
  }
Печать объекта производится либо из его меню, либо простым натаскиванием его иконы на икону (фолдер) принтера. Выше приведен пример печати одного объекта. Тот же объект, как ReferenceField внутри сценария, при печати имеет следующий вид:

Как все это использовать

Для использования предлагаемой системы работы с конфигурациями необходимо в корне или каталоге указанном переменной окружения AMT_PATH создать файл config.ini следующего содержания (на примере тренажера):
  /* AMT */
  group Config = {
    string customer = "ЗАО АМТ"
    int loglevel = 0          // -1 полный отладочный вывод в файл \xobject.log
                              //  0 выводить только сообщения обошибках
                              //  1 выводить предупреждения исообщения об ошибках
    group classes = {
      string = "classes.ini"  // описание общих классов (обпзательно)
      string = "common.ini"   // описание другой группы общих классов
      string = "DSTclass.ini" // описание классов бурового тренажера
      string = "KRSclass.ini" // описание классов тренажера для капитального ремонта скважин
    }
  }
Этот файл используется WPS-объектом AMTWPSXObject, реализующем диалоги и печать объектов конфигурации. Этот объект ассоциирован с расширением AMT. При программировании следует самому прочитать описания используемых классов - например, весь config.ini и все перечисленные в нем файлы или только нужные задаче.

Следующий текст дает пример считывания всех библиотек классов, описанных в config.ini:

  #include "common.h"

  XObject *o_main, *o_sub, *o_ref, *o_target, *o_config;  // для сценария

  // функция инициализирует подсистему предустановленных классов XObject
  // и возвращает кол-во классов в библиотеках
  int InitXClasses(void)
  {
    char buf[256]="", *str;
    char *amtPath = getenv(AMT_PATH);
    int num=0;

    XOBJECT_INITIALIZE

    // загрузка библиотек классов
    buf[0] = 0;
    if (amtPath) strcat(buf, amtPath);
    strcat(buf, "\\");
    strcat(buf, AMT_CONFIG);
    o_config = XObject::loadObject(buf);
    if (!o_config)
       return 1;

    o_config = o_config->getGroupElement("classes");
    for (int i = 0; igetGroupSize(); i++ )
    {
      str = o_config->getGroupElement(i)->getStringValue();
      if (str)
      {
        buf[0] = 0;
        if (amtPath)
           strcat(buf, amtPath);
        strcat(buf, "\\");
        strcat(buf, str);
        num += XObject::loadClasses(buf);
      }
    }
    return num;
  }
Следующие строки дают представление о получении значений некоторых элементов класса сценария для тренажера:
  {
    ....
    o_main=XObject::loadObject(file_name);  // загрузка файла конкретного сценария

    if (!o_main)
       return 1;   // ошибка чтения сценария - переход к тестовому режиму
    ...

    o_sub = o_main->getGroupElement("title");
    st->Scen->Name=strdup(o_sub->getStringValue());

    o_sub = o_main->getGroupElement("model_type");
    st->Scen->model_type=o_sub->getIntValue(); // тип (номер) модели данного тренажера

    o_sub = o_main->getGroupElement2("mode|value"); // здесь используется составное имя
    st->Scen->mode=o_sub->getIntValue(); // тип (номер) модели данного тренажера

    ...

    // если элемент является ссылкой, можно воспользоваться методом GetValue(name),
    // который сам и загрузит скрипт ссылки
       o_ref=o_main->getValue("derrik")  // загрузить описание буровой вышки
    // вычислить значение параметра g_kv в системе СИ
       st->Scen->g_kv=o_ref->getGroupElement2("g_kv|value")->getDoubleValue() *
                      o_ref->getGroupElement2("g_kv|coef")->getDoubleValue();
  }
Как видно из примеров, доступ к элементам конфигурации (параметрам) может производится по именам полей класса (name), по составным именам ("name|subname"), а также по их порядковым номерам в group. В приведенном выше описании забойного двигателя величина параметра <diam_zd|value> может браться по имени <%1|value>, т.к. diam_zd имеет порядковый номер один в классе.

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

  XObject *les, *student, *acc;     // для студента

  int InitStudentRecords(DST_STATE *st, int alarmtime, int alarmactive)
  {
  int res;
     student=XObject::loadObject(st->StudentName);  // загружаем файл объекта класса студент
     if (!student)
        return 1;
     les=XObject::getInstance("Lesson", CLASS_NAMEDGROUP_NAME);    // создаем объект класса занятие
     acc=XObject::getInstance("Accid", CLASS_NAMEDGROUP_NAME);     // создаем объект класса авария

  // заполняем описание занятия
     les->getGroupElement("sim")->setStringValue(st->SIM);
     les->getGroupElement("mode")->setIntValue(st->Scen->mode);
     les->getGroupElement("date")->setIntValue((int)(st->BeginTime));
     les->getGroupElement("scen")->setStringValue(st->Scen->Name);
     les->getGroupElement("ft")->setIntValue(alarmtime);
     les->getGroupElement("hl")->setIntValue(st->TimeFromBeginReal);
     if (alarmactive)
        res=2;
     else
        res=1;
     les->getGroupElement("res")->setIntValue(res);
     return 0;
  }

  int AddAccidRecord(int num, int begintime, int howlong, int source)
  {
  acc->getGroupElement("num")->setIntValue(num);
  acc->getGroupElement("bt")->setIntValue(begintime);
  acc->getGroupElement("hl")->setIntValue(howlong);
  acc->getGroupElement("src")->setIntValue(source);
  les->getGroupElement("accids")->addGroupElement(acc->clone()); // клонирование класса
  return 0;
  }

  int MakeStudentRecord(DST_STATE *st)
  {
  student->getGroupElement("lessons")->addGroupElement(les);
  XObject::saveObject(st->StudentName,student);
  delete acc;
  delete student;
  return 0;
  }


  void SaveAlarm(void)
  {
  AMessage *AMt;
     if (!STATUS->StudentName)
        return;

     if (InitStudentRecords(STATUS, AlarmTime, AlarmActive))
        return;
     AMt=AccBeginList;
     while (AMt)
        {
        AddAccidRecord(AMt->Anum, AMt->beginTime, AMt->howLong, AMt->source);
        AMt=AMt->next;
        }
     MakeStudentRecord(STATUS);
  }
Для заинтересовавшихся приведем описание public объектов класса XObject:
  class _Export XObject {
  public:
          static void setLogLevel(int);
          static int log(char*, ...);
          static int log(int, char*, ...);
          static int logObject(XObject*, char* = NULL);
          static void logClasses();

          static void     initClass();
          static void     unInitClass();
          static int      loadFile(char*, char**, int*);
          static int      saveFile(char*, char*, int);
          static XObject* getInstance(char*, char*, int = FLAGS_NORMAL, int =-1);
          static XObject* getInstanceText(char*, int*, int =-1);
          static XObject* getInstanceBin(char*, int*, int = -1);
          static int      loadClasses(char*);
          static XObject* loadObject(char*);
          static int      saveObject(char*, XObject*);
  ...
  public:
          static int      getClassNum();
          static char*    getClassName(int);
  ...
  public:
          virtual ~XObject();
          int     isVoid();
          int     isInteger();
          int     isDouble();
          int     isString();
          int     isComplex();
          int     isGroup();
          int     isObject();
          int     isObject(char*);
          int     isBasic();

          virtual XObject*        clone();
          int     initFrom(XObject*);

          char*   getName();
          int     setName(char* n);
          char*   getClassName();
          int     getClassVersion();

          int     getFlags();
          int     setFlags(int);
          void    flagSet(int);
          void    flagClear(int);
          int     flagIsSet(int);

          int     getSizeBin();
          int     getSizeText();

          int     getIntValue();
          int     setIntValue(int);

          double  getDoubleValue();
          int     setDoubleValue(double);

          char*   getStringValue();
          int     getStringSize();
          int     getStringLength();
          int     setStringSize(int);
          int     setStringValue(int, char*);
          int     setStringValue(char*);

          int     getGroupSize();
          XObject*        getGroupElement(int);
          XObject*        getGroupElement(char*);
          XObject*        getGroupElement2(char*);
          int     addGroupElement(XObject*);
          int     removeGroupElement(int);
          int     removeGroup();

          int     loadBinFile(char*);
          int     readText(char*, int);
          int     readBin(char*, int);
          int     writeBin(char*, int);
          int     writeText(char*, int, int = 0);

          virtual XObject* getValue();
          virtual int show(int);
          virtual int print(int);

  // misc
          static char*    strPrintInteger(char*, int, int);
          static char*    strPrintDouble(char*, double, int, int);
          static char*    strPrintString(char*, char*, int, int*);
          static char*    strPrintStringEsc(char*, char*, int, int*);
  ...
  };
Если вам понравилась описанная реализация идеи, вы можете скачать комплект для разработчика: xobject.zip, 324k.

NB! скоро будет готова новая версия комплекта -- следите за новостями.

Если вы хотите поспорить или просто поговорить с автором, пишите сюда.

Gregory Shrago - идея и реализация
Joseph Shrago - автор текста и потребитель :)

---
Интересные ссылки:

---

---
Комментариев к странице: 0 | Добавить комментарий
---
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов
(C) Russian Underground/2