Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.13. Вызов функций блоков

§2.13.1. Общие принципы вызова функций блоков

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.13. Вызов функций блоков

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

#### §2.13.1. Общие принципы вызова функций блоков

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

Рассматривавшиеся ранее способы взаимодействия блоков между собой – передача данных по связям и через динамические переменные – имеют один существенный недостаток: они позволяют передавать от блока к блоку только данные стандартных типов, предусмотренных в RDS. Таким образом нельзя передать, например, дескриптор файла, или указатель на какие-либо данные в памяти. Кроме того, передача данных по связям требует активного участия пользователя (он должен провести эти связи), а обмен через динамические переменные будет слишком сложен при большом количестве блоков. Если блоки обмениваются данными независимо, то для каждой пары связанных таким образом блоков необходима своя, постоянно существующая динамическая переменная, которой нужно дать какое-то уникальное имя, причем такое, чтобы оба связанных блока знали его. При этом нет никакой гарантии, что в обмен данными не вмешается третий блок, тоже подписавшийся на эту переменную.

Чтобы упростить (а в некоторых случаях и ускорить) передачу произвольных данных от блока к блоку, в RDS введен механизм вызова функций блоков, то есть вызова модели одного блока из модели другого. Этот механизм имеет несколько разновидностей: может вызываться модель одного блока или всех блоков подсистемы, модель может вызываться немедленно (и возвращенное ей значение будет передано вызвавшему блоку) или после завершения модели вызвавшего блока, модель может просто поддерживать выполнение какой-либо функции, или объявить свой блок ее исполнителем, чтобы другие заинтересованные блоки схемы знали это – в любом случае, механизм вызова реализуется через сервисные функции RDS и требует соблюдения некоторых правил. Рассмотрим их подробнее.

Каждая функция блока, доступная для вызова, должна иметь имя. Имя функции блока в данном случае – это не имя функции его модели, это просто некоторая строка, однозначно определяющая для всех моделей вызываемых и вызывающих блоков во всех библиотеках эту функцию и тип данных, который передается при ее вызове. Эту строку придумывает разработчик, который решил добавить в создаваемую им модель поддержку какой-либо новой функции, вызываемой другими блоками. Он обычно включает имя функции и описание передаваемых при ее вызове данных в документацию к блоку, чтобы разработчики других моделей могли ее вызывать или добавить ее поддержку в свои блоки. Нет никаких специальных правил, которым должна соответствовать эта строка – она просто должна быть уникальной. По этой причине имя функции обычно делают достаточно длинным и, для ясности, отражающим смысл выполняемых ей действий. Кроме того, обычно в нем не используют символы национальных алфавитов, чтобы избежать возможных проблем с кодировкой – для того, чтобы разные программисты могли правильно прочесть имя функции и вставить его в свои программы, лучше всего использовать в нем только латинские символы, цифры и знаки препинания. Часто в имя функции включают имя библиотеки, в блоке которой впервые появилась поддержка этой функции, имя разработчика и т.п. – все, что угодно, чтобы точно такое же имя не пришло в голову разработчику какой-либо другой модели, решившему придумать свою функцию. Имена вроде «функция1», «Установка значения» или «SetValue» использовать не стоит. А вот строка «InstituteOfControlSciences.Lab49.GeneralGraphics.SetValue», например, достаточно длинная и сложная, чтобы ее можно было относительно спокойно использовать в качестве имени новой функции.

Прежде чем модель сможет вызвать функцию другого блока или сама среагировать на такой вызов, функция должна быть зарегистрирована в RDS. Для этого необходимо вызвать сервисную функцию rdsRegisterFunction, передав в ее параметре строку с именем регистрируемой функции. Она вернет уникальный целый идентификатор, присвоенный этой функции. Вся дальнейшая работа с функцией будет вестись не по имени, а по этому целому идентификатору. Функцию rdsRegisterFunction для какого-либо конкретного имени функции блока можно вызывать сколько угодно раз, для одного и того же имени она будет возвращать один и тот же идентификатор, до тех пор, пока вся схема не будет выгружена из памяти при завершении работы с RDS или при загрузке другой схемы. Важно помнить, что идентификатор, присвоенный функции, действует только до тех пор, пока схема находится в памяти: при следующей загрузке той же самой схемы и регистрации той же самой функции RDS может присвоить ей другой целый идентификатор.

Регистрация имени функции никак не привязана к какому-либо блоку, поэтому ее можно производить не только в функции модели, но и, например, в главной функции библиотеки в момент загрузки DLL. Обычно для каждой функции, используемой в моделях блоков, находящихся в данной DLL, описывают глобальную целую переменную, которой присваивают результат вызова rdsRegisterFunction. При этом, поскольку переменная глобальная, с момента регистрации все модели блоков в библиотеке получают доступ к целому идентификатору функции. Чтобы не вызывать rdsRegisterFunction лишний раз (это безопасно, но вызывает лишние задержки из-за того, что RDS будет искать переданную строку в таблице уже зарегистрированных функций), можно пользоваться тем фактом, что RDS никогда не присваивает функциям нулевой идентификатор. Таким образом, дав глобальной переменной нулевое начальное значение, можно регистрировать функцию только в том случае, если значение переменной на данный момент осталось нулевым, то есть функция еще не зарегистрирована.

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

Допустим, в какой-либо библиотеке находятся модели Model1 и Model2. В модели Model1 используется функция «ProgrammersManual.Function1», а в модели Model2 – эта же функция, и, кроме нее, еще и «ProgrammersManual.Function2». Не важно, что это за модели, и что делают указанные функции – они нужны нам просто для примера. Если программист решит регистрировать функции при инициализации моделей блоков, ему нужно будет написать примерно следующее (фрагменты, относящиеся к регистрации функций, выделены цветом):

      #include <windows.h>
      #include <RdsDef.h>
      // Подготовка описаний сервисных функций
      #define RDS_SERV_FUNC_BODY GetInterfaceFunctions
      #include <RdsFunc.h>

      // Глобальные переменные для хранения идентификаторов функций
      int Function1Id=0,Function2Id=0;
      //========== Главная функция DLL ==========
      int WINAPI DllMain(HINSTANCE /*hinst*/,
                               unsigned long reason,
                               void* /*lpReserved*/)
      { if(reason==DLL_PROCESS_ATTACH) // Загрузка DLL
          { // Получение доступа к функциям
            if(!GetInterfaceFunctions())
              RDS_SERV_ERROR_MSGW // Сообщение: старая версия RDS
          }
        return 1;
      }
      //========= Конец главной функции =========

      // Модель 1
      extern "C" __declspec(dllexport)
        int RDSCALL Model1(int CallMode,
                          RDS_PBLOCKDATA BlockData,
                          LPVOID ExtParam)
      { switch(CallMode)
          { case RDS_BFM_INIT: // Инициализация
              // Регистрация функции
              if(!Function1Id)
                Function1Id=rdsRegisterFunction("ProgrammersManual.Function1");
        // …
      }
      //=========================================

      // Модель 2
      extern "C" __declspec(dllexport)
        int RDSCALL Model2(int CallMode,
                          RDS_PBLOCKDATA BlockData,
                          LPVOID ExtParam)
      { switch(CallMode)
          { case RDS_BFM_INIT: // Инициализация
              // Регистрация функций
              if(!Function1Id)
                Function1Id=rdsRegisterFunction("ProgrammersManual.Function1");
              if(!Function2Id)
                Function2Id=rdsRegisterFunction("ProgrammersManual.Function2");
        // …
      }
      //=========================================

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

Если программист решит регистрировать функции в момент загрузки DLL, пример будет выглядеть так:

      #include <windows.h>
      #include <RdsDef.h>
      // Подготовка описаний сервисных функций
      #define RDS_SERV_FUNC_BODY GetInterfaceFunctions
      #include <RdsFunc.h>

      // Глобальные переменные для хранения идентификаторов функций
      int Function1Id,Function2Id;
      //========== Главная функция DLL ==========
      int WINAPI DllMain(HINSTANCE /*hinst*/,
                               unsigned long reason,
                               void* /*lpReserved*/)
      { if(reason==DLL_PROCESS_ATTACH) // Загрузка DLL
          { // Получение доступа к функциям
            if(!GetInterfaceFunctions())
              RDS_SERV_ERROR_MSGW // Сообщение: старая версия RDS
            else
              { // Регистрация функций блоков
                Function1Id=rdsRegisterFunction("ProgrammersManual.Function1");
                Function2Id=rdsRegisterFunction("ProgrammersManual.Function2");
              }
          }
        return 1;
      }
      //========= Конец главной функции =========

      // В функциях моделей никакие действия по регистрации функций
      // не производятся

В этом примере регистрация функций производится один раз за все время существования DLL в памяти процесса, поэтому глобальным переменным не нужны нулевые значения по умолчанию и при регистрации не нужны какие-либо проверки.

После того, как функция зарегистрирована, ее целый идентификатор можно использовать для вызова функций и для реакций на них в моделях. Если необходимо вызвать функцию какого-либо конкретного блока, необходимо знать его уникальный идентификатор (RDS_BHANDLE). Этот идентификатор модель может получить разными способами: перебрав все блоки подсистемы и найдя в ней блок, удовлетворяющий какому-либо критерию; обнаружив блок на другом конце связи, соединенной с данным блоком, при ее анализе, и т.п. В любом случае, модель, вызывающая функцию другого блока, должна знать, к какому именно блоку схемы она обращается. Для прямого, то есть немедленного, вызова функции конкретного блока (отложенный вызов мы рассмотрим позже) используется сервисная функция rdsCallBlockFunction, принимающая три параметра: идентификатор вызываемого блока, целый идентификатор вызываемой в этом блоке функции и указатель произвольного типа (void*) на область памяти, в которой содержатся параметры этой функции (это может быть массив, структура, или просто набор байтов – RDS передает этот указатель в модель вызываемого блока без какой-либо обработки). Результат возврата rdsCallBlockFunction – целое число, которое вернула функция модели вызванного блока.

Допустим, параметром приведенной в примере выше функции «ProgrammersManual.Function1» является одно вещественное число двойной точности (double). Тогда вызов этой функции (с учетом того, что ее идентификатор уже записан в глобальную переменную Function1Id) будет выглядеть следующим образом:

      RDS_BHANDLE block=… // Здесь должен быть идентификатор
                          // вызываемого блока
      double val=10.0;    // Это число будет параметром функции
      int result;         // Сюда запишем возвращенное значение
      // Вызов функции
      result=rdsCallBlockFunction(block,Function1Id,&val);

В момент вызова rdsCallBlockFunction модель блока block вызывается в режиме RDS_BFM_FUNCTIONCALL, при этом в параметре ExtParam передается указатель на структуру RDS_FUNCTIONCALLDATA, в которой содержится идентификатор вызванной функции, указатель на переданные параметры, идентификатор вызвавшего блока, а также другая информация, связанная с вызовом функции:

      typedef struct
      { int Function;       // Идентификатор функции
        LPVOID Data;        // Указатель на параметры функции
        int Reserved;       // Зарезервировано
        RDS_BHANDLE Caller; // Вызвавший блок
        BOOL Broadcast;     // TRUE - вызов функции сразу у нескольких
                            // блоков, FALSE - только у одного
        int BroadcastCnt;   // При Broadcast==TRUE - порядковый номер
                            // вызванного блока, начиная с 0
        BOOL Stop;          // При Broadcast==TRUE и соответствующих
                            // параметрах вызова - остановить перебор
                            // блоков
        BOOL Delayed;       // TRUE - отложенный вызов,
                            // FALSE - прямой (немедленный)
        DWORD DataBufSize;  // При отложенном вызове - размер параметров
      } RDS_FUNCTIONCALLDATA;
      typedef RDS_FUNCTIONCALLDATA *RDS_PFUNCTIONCALLDATA;

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

Реакция на вызов функции «ProgrammersManual.Function1» будет выглядеть примерно следующим образом:

      // Модель 1
      extern "C" __declspec(dllexport)
        int RDSCALL Model1(int CallMode,
              RDS_PBLOCKDATA BlockData,
              LPVOID ExtParam)
      { RDS_PFUNCTIONCALLDATA func; // Вспомогательная переменная
        switch(CallMode)
          {
            // …
            case RDS_BFM_FUNCTIONCALL:	// Вызов функции
              func=(RDS_PFUNCTIONCALLDATA)ExtParam;
              if(func->Function==Function1Id)
                { // Приведение указателя на параметры функции
                  // к указателю на double
                  double *pVal=(double*)(func->Data);
                  // … можно выполнять какие-либо действия
                  // со значением *pVal …
                }
              break;
            // …
          }
        return RDS_BFR_DONE;
      }
      //=========================================

Очевидно, что для сравнения идентификатора вызванной функции с идентификаторами поддерживаемых нельзя использовать оператор switch, вместо него приходится использовать конструкции if(…) else if(…) … Идентификаторы функций – не константы, они каждый раз заново генерируются RDS при регистрации и, в данном случае, хранятся в глобальных переменных Function1Id и Function2Id, поэтому они не могут использоваться в метках case, где допустимы только выражения с константами.

Приведенный пример имеет один существенный недостаток. Мы договорились, что параметром функции «ProgrammersManual.Function1» будет одно число double. Но что произойдет, если из-за ошибки программиста при вызове этой функции вместо указателя на вещественное число двойной точности будет передан указатель, например, на тридцатидвухбитное целое? RDS никак не обрабатывает переданный при вызове указатель на параметры, он просто копирует его в поле Data структуры RDS_FUNCTIONCALLDATA. Если вызванный блок попытается считать 8 байтов (размер числа double) по этому адресу, это вызовет ошибку общей защиты, поскольку размер целого – всего 4 байта. Чтобы избежать этого, имеет смысл включить размер параметров функции в сами параметры. Для этого проще всего оформлять параметры функции как структуру, в самом первом поле которой размещать ее собственный размер. Большинство сервисных функций RDS, в параметрах которых передаются указатели на структуры, проверяют правильность параметров именно так.

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

      typedef struct
      { DWORD servSize; // Сюда будет записан размер структуры
        double Val;     // Собственно параметр функции
      } TFunction1Params;

Вызывать эту функцию нужно будет так:

      RDS_BHANDLE block=…      // Здесь должен быть идентификатор
                               // вызываемого блока
      TFunction1Params params; // Структура параметров функции
      params.servSize=sizeof(params); // Присваиваем размер структуры
      params.Val=10.0;         // Это число будет параметром функции
      int result;              // Сюда запишем возвращенное значение
      // Вызов функции
      result=rdsCallBlockFunction(block,Function1Id,&params);

Полю servSize введенной нами структуры параметров функции присваивается размер этой структуры, полученный оператором sizeof. Теперь в реакцию на вызов этой функции можно вставить проверку этого размера:

            case RDS_BFM_FUNCTIONCALL: // Вызов функции
              func=(RDS_PFUNCTIONCALLDATA)ExtParam;
              if(func->Function==Function1Id)
                { // Приведение указателя на параметры функции
                  // к указателю на структуру
                  TFunction1Params *par=(TFunction1Params*)(func->Data);
                  // Проверка размера параметров
                  if(par->servSize>=sizeof(TFunction1Params))
                    { // … можно выполнять какие-либо действия
                      // со значением par->Val …
                    }
                }
              break;

Можно заметить, что проверка размеров построена так, чтобы размер переданной при вызове структуры параметров par->servSize был не меньше ее ожидаемого размера sizeof(TFunction1Params). Почему бы не проверять их точное совпадение оператором «==»? Представим себе, что разработанная нами функция оказалась весьма полезной, и мы (а, может быть, и другие разработчики тоже) используем ее в большом количестве моделей. Затем мы решили добавить в параметры этой функции новые поля, которые будут использоваться в нескольких новых моделях, но никак не влияют на уже отлаженные старые. Если бы в проверке размеров стоял оператор «==», все ранее написанные и скомпилированные модели, которые не пользуются этими дополнительными полями, перестали бы работать, поскольку размер структуры изменился, и точное равенство перестало выполняться. Нам пришлось бы компилировать заново все модели, использующие эту функцию, а также заставить сделать это всех остальных разработчиков. Использование оператора «>=» позволяет не делать этого, поскольку он проверяет только достаточность размеров переданной структуры: если размер структуры окажется больше ожидаемого, мы можем спокойно продолжать работу, поскольку нужные нам поля в ней присутствуют. Разумеется, дополнительные поля в этом случае можно добавлять только в конец структуры, иначе старые модели, работающие с ее началом, считают неправильные данные. По этой же причине служебное поле, содержащее размер структуры, нужно делать самым первым.

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

RDS позволяет вызвать не только функцию конкретного блока, но и, например, функции всех блоков схемы, или всех блоков конкретной подсистемы. Для этого используется сервисная функция rdsBroadcastFunctionCallsEx:

      int RDSCALL rdsBroadcastFunctionCallsEx(
        RDS_BHANDLE System, // Подсистема, блоки которой вызываются
        int FuncId,         // Идентификатор вызываемой функции
        LPVOID Parameters,  // Указатель на параметры
        DWORD Flags);       // Флаги вызова

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

- RDS_BCALL_SUBSYSTEMS – если флаг взведен, будут вызываться не только блоки указанной подсистемы, но и все блоки подсистем, вложенных в нее;
- RDS_BCALL_ALLOWSTOP – если флаг взведен, очередной вызванный блок может прекратить дальнейший вызов других блоков, присвоив полю Stop структуры RDS_FUNCTIONCALLDATA значение TRUE;
- RDS_BCALL_CHECKSUPPORT – если флаг взведен, перед вызовом функции модель каждого блока будет вызываться в режиме RDS_BFM_CHECKFUNCSUPPORT, и, если при этом модель вернет нулевое значение, функция этого блока вызываться не будет. Это устаревший флаг – режим RDS_BFM_CHECKFUNCSUPPORT теперь практически не применяется, однако он до сих пор поддерживается и может использоваться по желанию программиста.

Вызов одной функции у всех блоков позволяет, например, уведомить всех «соседей» о наступлении какого-либо события, или подготовить блоки к началу какой-либо операции. Можно также использовать вызов функции всех блоков для поиска блока, удовлетворяющего какому-либо критерию, или для составления списка таких блоков. Реагируя на вызов функции и считав ее параметры, модель блока может передать вызвавшему блоку свой идентификатор для занесения в список (например, вызвав в ответ какую-либо функцию у вызвавшего блока – его идентификатор содержится в поле Caller структуры RDS_FUNCTIONCALLDATA). Или, если нужно найти единственный блок, она может занести идентификатор своего блока в какое-либо поле структуры параметров и остановить перебор блоков, присвоив TRUE полю Stop структуры RDS_FUNCTIONCALLDATA (при этом вызывать функции блоков нужно с флагом RDS_BCALL_ALLOWSTOP).