Руководство программиста
Глава 3. Создание модулей автоматической компиляции
§3.4. Компиляция моделей
Описывается способ формирования исходного текста модели блока на языке C и вызова для этого текста внешнего компилятора.
Для того, чтобы модуль автокомпиляции мог преобразовывать введенные пользователем модели в исполняемые файлы библиотек (DLL), нам нужно добавить в его функцию реакции на два вызова: RDS_COMPM_PREPARE и RDS_COMPM_COMPILE. В реакции на RDS_COMPM_PREPARE модуль должен проверить, нужно ли компилировать указанную в параметрах вызова модель, и подготовить ее к компиляции, если это необходимо (и, кроме того, записать в параметры блока имя файла библиотеки, которая будет создана в результате компиляции, и имя экспортированной из нее функции блока). В реакции на RDS_COMPM_COMPILE модуль должен будет скомпилировать либо все модели, которые он сам пометил как требующие компиляции в реакции на RDS_COMPM_PREPARE, либо вообще все свои модели по требованию пользователя, который выбрал пункт главного меню RDS «». В процессе компиляции должны будут быть созданы файлы DLL, имена которых занесены в блоки в реакции на RDS_COMPM_PREPARE, и всем блокам, связанным с каждой скомпилированной моделью, должна быть присвоена структура переменных этой модели.
Кроме этих двух реакций нам нужно будет добавить еще и реакцию на присоединение модели к блоку RDS_COMPM_ATTACHBLOCK. Дело в том, что, даже если модель не нужно компилировать заново, при присоединении ее к блоку структура переменных этого блока должна быть приведена в соответствие со структурой, описанной в файле модели. Если этого не сделать, скомпилированная модель не сможет работать с этим блоком до тех пор, пока пользователь не изменит файл модели и модулю не придется опять компилировать ее.
Нам нужно решить, где мы будем размещать DLL, полученные в результате компиляции наших моделей, и какие имена мы будем им давать. Поскольку тексты автоматически компилируемых моделей мы храним в отдельных файлах, причем каждую модель – в отдельном файле, имеет смысл помещать файл скомпилированной DLL в одну папку с файлом модели и давать ему имя, отличающееся от имени модели только расширением. Причем нужно также учитывать, что схема с этой моделью может загружаться как в 32-битную, так и в 64-битную версию RDS, при этом будут формироваться разные файлы DLL – будем давать им расширения «.dll32» и «.dll64» соответственно. Таким образом, из файла «testmodel.txt» в 32-битной версии получится библиотека «testmodel.dll32», а в 64-битной – «testmodel.dll64», и обе они будут находиться в одной папке с файлом модели. Библиотеки имеет смысл размещать в одной папке с моделями еще и потому, что, раз пользователь смог сохранить файл модели в этой папке, запись в эту папку ему разрешена, и мы сможем переписать туда же скомпилированный файл DLL.
Прежде чем мы включим реакцию на два упомянутых выше вызова в наш модуль, напишем несколько вспомогательных функций. Для того, чтобы проверять, нужно ли компилировать модель, мы будем сравнивать время последней записи в файл модели с временем последней записи в файл DLL, получающийся в результате ее компиляции. Если запись в файл модели производилась позже записи в DLL, значит, модель изменена уже после последней компиляции, и ее необходимо компилировать снова. Таким образом, нам понадобится функция, определяющая время последней записи заданного файла:
// Получить время последней записи в файл BOOL GetFileLastWrite(const char *filename,FILETIME *pLastWrite) { HANDLE file; BOOL ok=TRUE; // Преобразуем путь в UTF16 для Windows RDSWSTR fullpath_w=rdsUTF8toUTF16(filename,FALSE); // Открываем файл для чтения file=CreateFileW(fullpath_w,GENERIC_READ,FILE_SHARE_READ, NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); rdsFree(fullpath_w); if(file==INVALID_HANDLE_VALUE) // К файлу нет доступа return FALSE; if(pLastWrite) { // Файл открыт - получаем время изменения ok=GetFileTime(file,NULL,NULL,pLastWrite); } CloseHandle(file); return ok; } //=========================================
В эту функцию передается имя файла filename, время изменения которого нужно получить, и указатель pLastWrite на структуру Windows FILETIME, в которую функция должна записать это время. Функция возвращает TRUE, если ей удалось считать время, и FALSE в случае возникновения ошибок (например, если файла не существует). Фактически, функция состоит из трех вызовов функций Windows API: CreateFile для открытия файла (время изменения можно получить только у открытого файла), GetFileTime для получения времени его изменения, и CloseHandle для его закрытия. Мы не будем рассматривать ее более подробно.
На базе этой функции мы сделаем еще одну, которая будет проверять, нужно ли компилировать модель. В нее будут передаваться имена файлов модели и DLL с полными путями, а она будет сравнивать их времена изменения.
// Проверить необходимость компиляции DLL BOOL CheckDllTime(const char *modelfile,const char *dllfile) { FILETIME modeltime,dlltime; // Получаем время изменения DLL if(!GetFileLastWrite(dllfile,&dlltime)) return FALSE; // Файла DLL, вероятно, нет // Получаем время изменения файла модели if(!GetFileLastWrite(modelfile,&modeltime)) return FALSE; // Нет доступа к файлу модели - ошибка // Если modeltime<=dlltime, можно не компилировать return CompareFileTime(&modeltime,&dlltime)<=0; } //=========================================
Эта функция будет возвращать TRUE, если компиляция не требуется, и FALSE, если ее нужно провести. В параметре modelfile передается имя файла модели, а в dllfile – имя файла DLL, оба имени должны содержать полные пути. Времена изменения этих файлов записываются в переменные modeltime и dlltime соответственно. Если время изменения DLL определить не удалось, вероятнее всего, файл DLL просто еще не существует, то есть модель еще ни разу не компилировалась – в этом случае функция возвращает FALSE: компиляция необходима. Неудача при определении времени изменения файла модели сигнализирует о какой-то ошибке, причины которой не ясны (например, файл модели может быть открыт другой программой с исключительным доступом). В этом случае функция тоже возвращает FALSE: лучше попытаться скомпилировать модель лишний раз, чем игнорировать ее изменение, которое не удалось определить из-за ошибки. Наконец, если оба времени изменения определены, они сравниваются друг с другом функцией Windows API CompareFileTime. Если время изменения модели modeltime окажется меньшим времени изменения DLL dlltime, функция вернет TRUE, поскольку в это случае компиляция не нужна. В противном случае функция вернет FALSE: модель изменена позднее DLL, и DLL устарела.
Нам также потребуется функция, которая запустит заданный EXE-файл с заданными параметрами командной строки и дождется его завершения – мы используем ее для запуска компилятора и редактора связей. Причем, поскольку в настройках блока в параметрах командной строки мы решили использовать символические константы «$INCLUDE$» и «$LIB$», не поддерживаемые RDS, нам либо нужно заменять эти константы на их реальные значения перед передачей командной строки в нашу функцию, либо делать это внутри нее. Последнее удобней, поскольку нам нужно вызывать два разных исполняемых файла (компилятор и редактор связей) с разными параметрами командной строки, и если мы будем заменять в этих параметрах константы на их значения вне функции запуска, нам придется повторить один и тот же фрагмент программы два раза. Таким образом, лучше производить замену внутри функции, для чего мы будем, кроме имени исполняемого файла и строки параметров, передавать в нее два массива: массив имен констант и массив их значений. Возвращать эта функция будет логическое значение: TRUE при успешном запуске и FALSE при ошибке.
// Запуск программы и ожидание ее завершения BOOL RunAndWait( const char *path, // Имя EXE-файла const char *parameters, // Параметры командной строки const char **search, // Массив имен констант const char **replace) // Массив значений констант { char *cmd,*commandline,*tempdir; STARTUPINFO startup; // Структура параметров запуска процесса PROCESS_INFORMATION info; // Структура описания процесса BOOL ok=TRUE; RDSWSTR commandline_w,tempdir_w; // Формирование полной командной строки cmd=rdsDynStrCat("\"",path,FALSE); rdsAddToDynStr(&cmd,"\" ",FALSE); rdsAddToDynStr(&cmd,parameters,FALSE); rdsAddToDynStr(&cmd," ",FALSE); // Пробел (на всякий случай) // В cmd теперь - команда запуска, символические константы // в ней еще не обработаны // Заменяем все константы на их значения commandline=rdsStringReplace(cmd,search,replace, -1,RDS_SRF_STDPATHS); rdsFree(cmd); // cmd больше не нужна // В commandline - полная командная строка запуска программы if(commandline==NULL) return FALSE; // Получение имени временной папки RDS без '\' tempdir=rdsTransformFileName( rdsGetSystemPath(RDS_GSPTEMPPATH), RDS_TFN_EXCLUDEPATHBS,NULL,NULL); // Заполнение структуры STARTUPINFO ZeroMemory(&startup,sizeof(STARTUPINFO)); startup.cb=sizeof(STARTUPINFO); // Запуск процесса commandline_w=rdsUTF8toUTF16(commandline,FALSE); tempdir_w=rdsUTF8toUTF16(tempdir,FALSE); if(!CreateProcessW(NULL,commandline_w,NULL,NULL,FALSE, 0,NULL,tempdir_w,&startup,&info)) ok=FALSE; // Запустить не получилось if(ok) { // Ждем завершения процесса WaitForSingleObject(info.hProcess,INFINITE); // Процесс завершен - закрываем полученные дескрипторы CloseHandle(info.hThread); CloseHandle(info.hProcess); } // Освобождаем все динамические строки rdsFree(commandline); rdsFree(tempdir); rdsFree(commandline_w); rdsFree(tempdir_w); return ok; } //=========================================
В параметре path в эту функцию передается полный путь к запускаемому EXE-файлу, в параметре parameters – строка его параметров (объединение этих двух строк даст полную командную строку для запуска программы). В параметре search передается указатель на массив символических констант, которые нужно заменить в командной строке (он должен завершаться значением NULL), а в параметре replace – массив значений этих констант. Два последних параметра подобраны так, чтобы их можно было без изменений передать в сервисную функцию RDS rdsStringReplace, которая занимается поиском и заменой в строках. Эта функция описана следующим образом:
RDSSTR RDSCALL rdsStringReplaceA( RDSCSTR string, // исходная строка (UTF8) RDSCSTR *search, // массив строк для поиска (UTF8) RDSCSTR *replace, // массив строк для замены (UTF8) int count, // размер массива search или -1 DWORD flags); // флаги RDSWSTR RDSCALL rdsStringReplaceW( RDSWCSTR string, // исходная строка (UTF16) RDSWCSTR *search, // массив строк для поиска (UTF16) RDSWCSTR *replace, // массив строк для замены (UTF16) int count, // размер массива search или -1 DWORD flags); // флаги // Функция-псевдоним RDSXSTR RDSCALL rdsStringReplace( RDSXCSTR string, // исходная строка (кодировка по умолчанию) RDSXCSTR *search, // массив строк для поиска (кодировка по умолчанию) RDSXCSTR *replace, // массив строк для замены (кодировка по умолчанию) int count, // размер массива search или -1 DWORD flags); // флаги
Она ищет в строке string все вхождения элементов массива search, заменяет их на элементы массива replace с тем же индексом, и возвращает динамически сформированную строку, получившуюся в результате этих замен (как обычно, ее нужно освобождать функцией rdsFree). В параметре count передается число элементов в массиве search или −1, если массив завершается константой NULL: в этом случае функция самостоятельно определит его размер. Параметр flags может быть произвольной комбинацией битовых флагов RDS_SRF_STDPATHS (заменять в строке стандартные символические константы путей RDS на их значения) и RDS_SRF_IGNORECASE (не учитывать регистр символов при поиске). Естественно, для нормальной работы этой функции размер массива replace должен быть равен размеру массива search, или может быть на единицу меньше, если последний элемент search равен NULL. Если, например, выполнить следующую программу:
const char *names[]={"_PI_VAL_","$PI$",NULL}; const char *values[]={"3.1415926","Пи"}; char *result=rdsStringReplace( "Значение $PI$ равно _PI_VAL_", names,values,-1,0); rdsMessageBox(result,"Результат",MB_OK); rdsFree(result);
то функция rdsMessageBox выведет сообщение «Значение Пи равно 3.1415926».
Вернемся к функции RunAndWait. Сначала из параметров path и parameters нам нужно сформировать команду запуска программы path с параметрами командной строки parameters. Для этого мы функциями rdsDynStrCat и rdsAddToDynStr (они уже неоднократно рассматривались ранее) формируем динамическую строку cmd вида «<двойная кавычка>+path+<двойная кавычка и пробел>+parameters+<пробел>». Поскольку в пути к EXE-файлу запускаемой программы могут содержаться пробелы, мы заключаем его в двойные кавычки – так принято в Windows. Путь к исполняемому файлу и строку параметров мы разделяем пробелом, и в конец строки добавляем еще один пробел (практика показывает, что без этого пробела некоторые программы работают неправильно). Затем получившуюся строку мы обрабатываем функцией rdsStringReplace, чтобы заменить в ней все символические константы (как стандартные для RDS, так и переданные нами в массиве search) на их значения. В результате мы получаем динамическую строку commandline. После ее получения мы освобождаем строку cmd – она больше не нужна.
Затем мы формируем еще одну динамическую строку, содержащую путь к временной папке RDS без завершающего этот путь символа «\». Для этого мы вызываем функцию rdsGetSystemPath с параметром RDS_GSPTEMPPATH (она вернет указатель на строку во внутренней памяти RDS, содержащую путь к временной папке), а затем обрабатываем возвращенное ей значение функцией rdsTransformFileName с параметром RDS_TFN_EXCLUDEPATHBS. Эта функция предназначена для обработки имен файлов и путей и всегда возвращает динамическую строку, так что нужно будет не забыть освободить ее в конце. Параметр RDS_TFN_EXCLUDEPATHBS указывает ей на необходимость удалить в конце переданного ей имени символ «\», если, конечно, он там присутствует. Мы могли бы удалить его и не пользуясь сервисными функциями RDS, но так текст функции получается короче.
После этого мы подготавливаем структуру параметров запуска STARTUPINFO, необходимую для запуска процесса (вся подготовка заключается в занесении в поле cb размера этой структуры и обнулении всех остальных ее полей – параметры запуска нам не нужны), переводим командную строку и путь к временной папке в кодировку UTF16 и вызываем функцию Windows API CreateProcessW, передавая ей преобразованные строки: командную строку для запуска commandline_w и путь к папке tempdir_w (эта папка станет текущей папкой процесса). CreateProcessW запустит программу, указанную в commandline_w (если это возможно, иначе она немедленно вернет FALSE), запишет в структуру info типа PROCESS_INFORMATION, указатель на которую передан в ее последнем параметре, дескрипторы запущенного процесса и его потока, и вернет нам управление. Нам нужно дождаться окончания работы запущенной программы, поэтому мы вызываем функцию Windows API WaitForSingleObject, передавая ей дескриптор запущенного процесса info.hProcess и константу INFINITE (бесконечность) в качестве времени ожидания. Эта функция вернет управление только после завершения указанного в ее параметре процесса.
Дождавшись завершения запущенной программы, мы закрываем дескрипторы процесса и потока, возвращенные нам функцией CreateProcess (мы обязаны это сделать), освобождаем созданные динамические строки и возвращаем логическую переменную ok (если программу запустить не удалось, она будет иметь значение FALSE).
У нашего модуля автокомпиляции есть логические параметры, указывающие, нужно ли добавлять пути к компилятору и редактору связей в системную переменную окружения «PATH» – без этого некоторые компиляторы (в частности, версии GCC, которые мы собираемся использовать) не будут работать. Напишем еще одну функцию, которая будет вызывать RunAndWait с добавлением в «PATH» пути к запускаемому EXE-файлу перед вызовом и восстановлением значения этой переменной после завершения запущенной программы. Необходимостью добавления этого параметра будет управлять логический параметр функции.
// Запуск программы и ожидание ее завершения с возможным добавлением пути к exe-файлу в PATHS BOOL RunAndWaitSetPath( const char *path, // Имя EXE-файла const char *parameters, // Параметры командной строки const char **search, // Массив имен констант const char **replace, // Массив значений констант BOOL setpath) // Установить путь к exe в PATHS { #define PATHVAR "Path" if(setpath) { char *exepath=rdsTransformFileName(path,RDS_TFN_GETPATHNOBS,NULL,NULL); char *oldpath=rdsGetEnvironmentVariable(PATHVAR); BOOL ok; if(oldpath) // Есть пути { char *newpath=NULL; rdsAddToDynStr(&newpath,exepath,FALSE); rdsAddToDynStr(&newpath,";",FALSE); rdsAddToDynStr(&newpath,oldpath,FALSE); rdsSetEnvironmentVariable(PATHVAR,newpath,FALSE); rdsFree(newpath); } else // Было пусто rdsSetEnvironmentVariable(PATHVAR,exepath,FALSE); ok=RunAndWait(path,parameters,search,replace); if(oldpath) // Восстанавливаем rdsSetEnvironmentVariable(PATHVAR,oldpath,FALSE); rdsFree(oldpath); rdsFree(exepath); return ok; } return RunAndWait(path,parameters,search,replace); #undef PATHVAR } //=========================================
Первые четыре параметра этой функции совпадают с параметрами RunAndWait, в пятом (setpath) передается TRUE, если путь к запускаемымо EXE-файлу необходимо добавить в «PATH», и FALSE в противном случае. При истинном setpath выполняются следующие действия:
- при помощи rdsTransformFileName из полного пути к EXE-файлу выделяется полный путь к его папке;
- при помощи rdsGetEnvironmentVariable считывается текущее значение переменной окружения «PATH» (оно записывается в oldpath);
- если у «PATH» было значение, при помощи последовательных вызовов rdsAddToDynStr к нему добавляется точка с запятой и путь к EXE-файлу;
- при помощи rdsSetEnvironmentVariable устанавливается новое значение «PATH»;
- вызывается RunAndWait (функция вернет управление только после завершения запущенного файла);
- при помощи rdsSetEnvironmentVariable восстанавливается прежнее значение «PATH».
При ложном значении setpath функция просто вызывает RunAndWait без дополнительных действий.
Теперь все вспомогательные функции готовы, и мы можем ввести в функцию модуля новые реакции. Для этого добавим в класс TCAutoCompData три новых функции-члена:
// Класс личной области данных модуля автокомпиляции class TCAutoCompData { private: // … без изменений …// Сформировать в файле исходный текст программы BOOL WriteSourceCode(HANDLE file,RDS_HOBJECT varset, const char *prog); // Загрузить и скомпилировать одну модель void LoadAndProcessModel(RDS_COMPMODELDATA *data, int fileset,BOOL varsonly);// Сообщение об ошибке в модели static void ModelErrorMsg(const char *modelname,int errorcode); public:// Подготовиться к компиляции модели void PrepareToCompileModel(RDS_COMPPREPAREDATA *param); // Компилировать модели void CompileModels(RDS_COMPILEDATA *param); // Связать блок с моделью void AttachBlock(RDS_COMPBLOCKOPDATA *param);// … далее без изменений … }; //=========================================
Функции PrepareToCompileModel и CompileModels будут вызываться из функции модуля при ее вызове с параметрами RDS_COMPM_PREPARE и RDS_COMPM_COMPILE соответственно. Из функции CompileModels мы будем вызывать для каждой компилируемой модели функцию LoadAndProcessModel, кроме того, формирование исходного текста компилируемой библиотеки мы для удобства вынесем в функцию WriteSourceCode. При присоединении модели к каждому блоку (в реакции на RDS_COMPM_ATTACHBLOCK) мы будем вызывать функцию AttachBlock – в ней мы присвоим блоку структуру переменных модели, если это необходимо.
Внутрь оператора switch в функции модуля нужно добавить следующие метки case:
// Подготовка к компиляции case RDS_COMPM_PREPARE: data->PrepareToCompileModel((RDS_PCOMPPREPAREDATA)ExtParam); break; // Компиляция моделей case RDS_COMPM_COMPILE: data->CompileModels((RDS_PCOMPILEDATA)ExtParam); break; // Присоединение модели к блоку case RDS_COMPM_ATTACHBLOCK: data->AttachBlock((RDS_PCOMPBLOCKOPDATA)ExtParam); break;
Нам также понадобится добавить в функцию ModelErrorMsg новые коды ошибок и сообщения, связанные с формированием исходного текста программы для компиляции и с самой компиляцией:
// Коды ошибок #define MEC_SIMPLEBLOCK 1 // Только для простых блоков #define MEC_READERROR 2 // Ошибка чтения файла #define MEC_NOTAMODEL 3 // Файл - не модель #define MEC_NOSECTIONS 4 // Нет нужных разделов в модели #define MEC_MODELWRITEERROR 5 // Ошибка записи файла модели#define MEC_BADVARS 6 // Ошибка в описаниях переменных #define MEC_SRCWRITEERROR 7 // Ошибка записи исходного текста программы #define MEC_NOOBJFILE 8 // Нет объектного файла #define MEC_COMPSTARTERROR 10 // Ошибка запуска компилятора #define MEC_NODLLFILE 11 // Нет файла DLL #define MEC_LINKSTARTERROR 12 // Ошибка запуска редактора связей #define MEC_DLLCOPYERROR 13 // Ошибка копирования DLL //=========================================// Сообщение об ошибке в модели // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" void TCAutoCompData::ModelErrorMsg( const char *modelname,int errorcode) { const char *errortext; // Сообщение по коду ошибки switch(errorcode) { case MEC_SIMPLEBLOCK: errortext="Модель может подключаться только к простому блоку"; break; case MEC_READERROR: errortext="Ошибка чтения файла"; break; case MEC_NOTAMODEL: errortext="Файл не является моделью блока"; break; case MEC_NOSECTIONS: errortext="В файле нет необходимых разделов"; break; case MEC_MODELWRITEERROR: errortext="Ошибка записи файла модели"; break;case MEC_BADVARS: errortext="Ошибка в разделе описания переменных"; break; case MEC_SRCWRITEERROR: errortext="Невозможно создать файл исходного текста"; break; case MEC_NOOBJFILE: errortext="Объектный файл не создан - " "в тексте модели есть ошибки"; break; case MEC_COMPSTARTERROR: errortext="Ошибка запуска компилятора"; break; case MEC_NODLLFILE: errortext="Файл DLL не создан - " "в модели есть ошибки"; break; case MEC_LINKSTARTERROR: errortext="Ошибка запуска редактора связей"; break; case MEC_DLLCOPYERROR: errortext="Ошибка копирования полученного файла DLL"; break;default: errortext="Неизвестная ошибка"; } // Название модели, если есть if(modelname) { char *msgtext; // Здесь формируется динамический текст msgtext=rdsDynStrCat("Модель: ",modelname,FALSE); rdsAddToDynStr(&msgtext,"\n",FALSE); // Описание ошибки rdsAddToDynStr(&msgtext,errortext,FALSE); // Показываем сообщение rdsMessageBox(msgtext,"Автокомпиляция",MB_OK | MB_ICONWARNING); // Освобождаем память, занятую динамическим текстом rdsFree(msgtext); } else rdsMessageBox(errortext,"Автокомпиляция",MB_OK | MB_ICONWARNING); } //=========================================
Функция PrepareToCompileModel должна проверить, требуется ли компиляция модели, указанной в ее параметре, и передать в блок имя DLL и имя функции блока, которые получатся в результате компиляции:
// Подготовиться к компиляции модели void TCAutoCompData::PrepareToCompileModel( RDS_COMPPREPAREDATA *param) { char *dllpath; // Получаем имя DLL с сокращенным путем для записи в параметры блока #ifdef RDS_WIN64 dllpath=rdsTransformFileName(param->Model->ModelName,RDS_TFN_CHANGEEXT,".dll64",NULL); #else dllpath=rdsTransformFileName(param->Model->ModelName,RDS_TFN_CHANGEEXT,".dll32",NULL); #endif // Выясняем, нужно ли перекомпилировать DLL if(param->Rebuild) // Принудительно компилировать все param->Model->Valid=FALSE; else // Компилировать при необходимости { // Время последней записи DLL должно быть не меньше // времени записи текста модели char *modelpath,*dllfullpath; // Полный путь к файлу модели modelpath=rdsGetFullFilePath( param->Model->ModelName,NULL,NULL); // Полный путь к файлу DLL dllfullpath=rdsGetFullFilePath(dllpath,NULL,NULL); // Сравниваем времена последней записи param->Model->Valid=CheckDllTime(modelpath,dllfullpath); // Освобождаем строки rdsFree(modelpath); rdsFree(dllfullpath); } // Записываем в параметры блоков библиотеку и функцию в ней, // которые получатся в результате компиляции rdscompSetModelFunction(param->Model->Model,dllpath,Exported); rdsFree(dllpath); } //=========================================
В эту функцию передается указатель param на структуру RDS_COMPPREPAREDATA с двумя полями: указателем на структуру данных модели Model и признаком принудительной компиляции всех моделей Rebuild. Функция должна установить Model->Valid в TRUE, если данной модели не требуется компиляция, и в FALSE, если компиляция требуется.
Независимо от того, требуется модели компиляция или нет, мы будем настраивать блоки на работу с нужной библиотекой и функцией. Если пользователь, например, подключает к блоку новую автоматически компилируемую модель, которой в данный момент не требуется компиляция (файл модели старше файла DLL), в параметрах блока еще не установлены библиотека и функция модели, и модуль автокомпиляции все равно должен сообщить блоку, с какой библиотекой и какой функцией он теперь будет работать. Для этого мы преобразуем имя модели в имя файла DLL dllpath заменой расширения при помощи вызова rdsTransformFileName. В данном случае мы берем имя файла модели не с полным путем, а с относительным (param->Model->ModelName), чтобы получившееся имя библиотеки тоже содержало относительные пути. Таким образом, например, если файл модели находится в одной папке со схемой, в которой используется эта модель, в параметрах блока имя модели не будет содержать пути вообще, и имя файла DLL блока, полученное из имени модели, тоже не будет содержать пути. Именно это нам и нужно: при переносе всей папки со схемой, моделями и полученными из них файлами DLL, схема не утратит работоспособности, поскольку все пути в параметрах блоков – относительные. Расширение, которое мы даем файлу DLL, зависит от наличия определения define-константы RDS_WIN64. Если она определена, значит, мы создаем модуль автокомпиляции, который будет работать в 64-битной версии RDS, и будет скомпилирована строка прорграммы с заменой расширения файла на «.dll64». В противном случае создается модуль для 32-битной версии, и скомпилируется строка замены расширенмя на «.dll32».
Если поле param->Rebuild истинно (пользователь приказал заново скомпилировать все модели), param->Model->Valid устанавливается в FALSE – модель нужно компилировать. В противном случае мы должны сравнить времена последнего изменения файлов модели и DLL – если модель изменена позднее, DLL нужно компилировать заново. Для этого мы сначала преобразуем имя модели param->Model->ModelName, которое является относительным путем к ее файлу, в полный путь modelpath функцией rdsGetFullFilePath. Затем таким же образом из dllpath мы получаем имя файла DLL с полным путем dllfullpath (и modelpath, и dllfullpath – динамически строки, поэтому их нужно будет освободить функцией rdsFree). Получив оба пути, мы передаем их в написанную ранее CheckDllTime, которая сравнит времена изменения этих файлов и вернет FALSE, если модель изменена позднее библиотеки. Возвращенное ей значение мы присваиваем param->Model->Valid.
Перед завершением функции мы настраиваем все блоки, использующие данную автоматически компилируемую модель, на работу с нужной библиотекой и функцией блока в ней вызовом сервисной функции rdscompSetModelFunction, в первом параметре которой передается идентификатор данной модели (param->Model->Model), во втором – сформированное нами имя библиотеки с относительными путями (dllpath), в третьем – имя экспортированной функции (поле Exported класса личной области нашего модуля, которое мы сделали одним из настраиваемых параметров). После этого мы освобождаем динамическую строку dllpath и завершаем функцию PrepareToCompileModel. При вызове rdscompSetModelFunction не требуется указывать, для какой именно (32-битной или 64-битной) версии RDS выполняется настройка. В схемах связь с библиотеками хранится независимо для обеих версий. Если вызов rdscompSetModelFunction сделан при работе в 32-битной версии, будет установлено имя библиотеки и имя функции только для 32-битной версии, а связь для 64-битной версии останется неизменной. Точно так же вызов, сделанный в 64-битной версии, установит имена библиотеки и функции только для «своей» версии.
Функция CompileModels, в которой мы будем компилировать модели, в структуре данных которых поле Valid имеет значение FALSE, будет достаточно простой, поскольку собственно компиляцию модели мы вынесли в отдельную функцию LoadAndProcessModel. Здесь же мы просто будем перебирать в цикле все модели:
// Компилировать все модели void TCAutoCompData::CompileModels(RDS_COMPILEDATA *param) { // Проверяем необходимые параметры if(CompPath==NULL || LinkPath==NULL) return; // Компиляция невозможна // В цикле перебираем все модели из param->InvalidModels for(int i=0;i<param->IMCount;i++) { int tempset; // Создаем новый набор временных файлов tempset=rdsTMPCreateFileSet(); // Компилируем модель LoadAndProcessModel(param->InvalidModels[i],tempset,FALSE); // Удаляем все созданные в процессе временные файлы rdsTMPDeleteFileSet(tempset); } } //=========================================
В параметре param этой функции передается указатель на структуру RDS_COMPILEDATA. В ней нас будут интересовать два поля: массив указателей на структуры данных моделей InvalidModels и его размер IMCount. При вызове модуля для компиляции в этот массив включаются только те модели, которые требуют компиляции, поэтому нам не нужно вручную проверять поле Valid в их структурах данных. Таким образом, нам просто нужно вызвать LoadAndProcessModel для каждой модели, указатель на структуру данных (RDS_COMPMODELDATA) которой находится в массиве param->InvalidModels. Однако, для некоторого облегчения написания функции компиляции модели LoadAndProcessModel, перед ее вызовом мы создаем набор временных файлов сервисной функцией RDS rdsTMPCreateFileSet, а после него удаляем все временные файлы этого набора функцией rdsTMPDeleteFileSet, при этом идентификатор созданного набора мы передаем в функцию LoadAndProcessModel, чтобы она могла с ним работать. Остановимся на этих функциях для работы с временными файлами подробнее.
В процессе компиляции нам придется создать на диске некоторое количество дополнительных файлов: для работы компилятора нужно сформировать в каком-либо файле исходный текст библиотеки на языке C, компилятор преобразует его в объектный файл и т.д. После завершения компиляции все эти файлы желательно удалить, чтобы они зря не занимали место на диске (лучше не оставлять после себя «мусор»). Значит, нам нужно запоминать где-то имена всех созданных нами файлов, а потом, когда эти файлы уже не нужны, удалять их. В RDS есть специальные сервисные функции, которые позволяют автоматизировать этот процесс – ими мы и будем пользоваться. Эти функции, кроме того, позволяют решить проблему подбора имен для временных файлов. Конечно, мы можем заложить в нашу программу жестко заданные имена файлов: например, исходный текст библиотеки, который будет обрабатываться компилятором, мы можем всегда формировать в файле «source.cpp» в стандартной папке временных файлов RDS – кажется, что там он никому не будет мешать. Однако, представим себе, что на одной машине запущено две копии RDS, и им обеим одновременно пришлось заняться компиляцией своих моделей. Обе копии попытаются записать в папку файл с одним и тем же именем, что приведет к конфликту. К счастью, мы можем предоставить выбор имени для файла сервисной функции RDS – она подберет такое имя, которое не совпадает ни с одним файлом в заданной папке, причем она сразу создаст файл с этим именем нулевого размера, чтобы ни одна другая программа не смогла «захватить» это имя и помешать нам использовать этот файл. Нам останется только открыть его для записи и сформировать в нем исходный текст библиотеки.
Все временные файлы, с которыми работает RDS, объединены в наборы, каждый из которых имеет целый идентификатор. Прежде чем начать работать с временными файлами, нужно вызвать функцию rdsTMPCreateFileSet, которая вернет уникальный, нигде в данный момент не используемый, идентификатор набора. Этот идентификатор используется в вызовах всех остальных функций работы с временными файлами.
Для того, чтобы подобрать имя для временного файла и создать файл нулевого размера, «заняв» таким образом это имя, мы будем вызывать функцию rdsTMPCreateEmptyFileAnyExt:
RDSSTR RDSCALL rdsTMPCreateEmptyFileAnyExtA( // UTF8 int SetId, // Набор временных файлов RDSCSTR DesiredName // Желаемое имя с путем (UTF8) ); RDSWSTR RDSCALL rdsTMPCreateEmptyFileAnyExtW( // UTF16 int SetId, // Набор временных файлов RDSWCSTR DesiredName // Желаемое имя с путем (UTF16) ); // Функция-псевдоним RDSXSTR RDSCALL rdsTMPCreateEmptyFileAnyExt( // Кодировка по умолчанию int SetId, // Набор временных файлов RDSXCSTR DesiredName // Желаемое имя с путем (кодировка по умолчанию) );
В параметре setid передается идентификатор набора, к которому будет относиться создаваемый файл, а в параметре name – его желаемое имя с указанием пути (можно использовать как полные пути, так и относительные, с символическими константами RDS). Если файл с именем name в данный момент не существует, а также нет ни одного файла с тем же именем и любым другим расширением, функция оставит это имя без изменений. Если же такие файлы есть, она изменит имя файла, сохранив путь и расширение и сделав это имя уникальным в данной папке с любым расширением. Подобрав имя, функция создаст пустой файл, запомнит его имя во внутренней памяти RDS и вернет указатель на это запомненное имя с полным путем. Поскольку функция не создает динамических строк, результат ее возврата не нужно освобождать в вызвавшей программе.
Таким образом, если «заказать» при помощи этой функции создание файла с именем, например, «source.cpp» в папке временных файлов, функция проверит, что в этой папке нет не только этого файла, но и «source.obj», «source.dll», «source.txt» и т.п. Если хотя бы один такой файл будет обраружен, функция изменит имя так, чтобы оно стало уникальным (для этого к имени будет добавлено число).
В RDS также есть функция rdsTMPCreateEmptyFile, которая создает файл с уникальным именем, не проверяя наличие файлов с другими расширениями. Для нас она была бы менее удобна, поскольку компилятор и редактор связей будут в процессе работы создавать файлы с тем же именем, но другими расширениями. Лучше убедиться, что ни одного из подобных файлов в папке нет.
Если имя для файла подбирать не нужно, можно просто добавить заданное имя к списку временных файлов набора функцией rdsTMPRememberFileName. Она может быть полезна в тех случаях, когда временные файлы создаются какой-либо другой программой (например, компилятором), но мы знаем их имена и хотим удалить их по окончании работы. В эту функцию тоже можно передать имя с относительным путем, она возвращает указатель на запомненное во внутренней памяти RDS имя с полным путем.
По окончании работы с временными файлами можно удалить их все одним вызовом функции rdsTMPDeleteFileSet, в которую передается идентификатор удаляемого набора. Зная полное имя временного файла, можно удалить только этот файл и с диска, и из набора функцией rdsTMPDeleteFile, однако, следует помнить, что даже после удаления всех файлов набора вызовами rdsTMPDeleteFile, пустой набор все равно останется в памяти, и его нужно будет удалить вызовом rdsTMPDeleteFileSet. Также следует учитывать, что при завершении RDS или загрузке новой схемы все временные файлы всех наборов удаляются автоматически, поэтому нет способа не удалять файл, объявленный временным при помощи функций rdsTMPCreateEmptyFile или rdsTMPRememberFileName.
Возвращаясь к функции CompileModels, можно видеть, что, вызывая в цикле функцию LoadAndProcessModel, мы передаем ей не только указатель на структуру данных компилируемой модели param->InvalidModels[i] и идентификатор созданного для нее набора временных файлов tempset, но еще и значение FALSE в третьем параметре. Этот третий параметр функции LoadAndProcessModel мы будем использовать для ограничения ее работы: если он равен FALSE, функция должна будет скомпилировать модель и присвоить всем блокам, обслуживаемым моделью, ее структуру переменных, если же параметр будет равен TRUE, функция должна будет только присвоить блокам структуру переменных, не компилируя модель. Это ограничение позволит нам использовать одну и ту же функцию и для компиляции модели в реакции RDS_COMPM_COMPILE, и для установки структуры переменных блока в реакции RDS_COMPM_ATTACHBLOCK.
Прежде чем заняться функцией LoadAndProcessModel, напишем сначала функцию формирования исходного текста компилируемой библиотеки WriteSourceCode. Будем считать, что файл, в который нужно записать исходный текст, уже открыт на запись, а файл модели уже считан и разобран на структуру переменных блока и текст реакции на такт расчета. Эта функция будет довольно объемной из-за большого размера исходного текста, который она формирует.
// Сформировать в файле исходный текст программы BOOL TCAutoCompData::WriteSourceCode( HANDLE file, // дескриптор открытого файла RDS_HOBJECT varset, // набор переменных const char *prog) // исходный текст реакции { BOOL ok; RDS_VARDESCRIPTION vdescr; char *undef=NULL; // Начальные описания и тип главной функции DLL ok=WriteString(file, "#include <windows.h>\r\n" "#include <stdlib.h>\r\n" "#include <math.h>\r\n" #ifdef RDS_WIN64 ok=ok && WriteString(file,"#define RDS_WIN64\r\n"); #else ok=ok && WriteString(file,"#define RDS_WIN32\r\n"); #endif ok=ok && WriteString(file, "#include <RdsDef.h>\r\n" "#define RDS_SERV_FUNC_BODY GetServiceFunc\r\n" "#include <RdsFunc.h>\r\n" "double _HugeDouble;\r\n\r\n" // значение ошибки ); // Имя главной функции DLL (из настроек модуля) ok=ok && WriteString(file,DllMainName); // Тело главной функции DLL ok=ok && WriteString(file, "(HINSTANCE /*hinst*/,unsigned long reason," "void */*lpReserved*/)\r\n" "{ if(reason==DLL_PROCESS_ATTACH)\r\n" " { if(!RDS_SERV_FUNC_BODY())\r\n" " RDS_SERV_ERROR_MSGW\r\n" " else\r\n" " rdsGetHugeDouble(&_HugeDouble);\r\n" " }\r\n" " return 1;\r\n" "}\r\n\r\n"); // Заголовок функции блока (из настроек) ok=ok && WriteString(file,ModelFuncHdr); // Продолжение функции блока и проверка типов переменных ok=ok && WriteString(file, "(int CallMode,RDS_PBLOCKDATA BlockData,LPVOID ExtParam)\r\n" "{ switch(CallMode)\r\n" " { case RDS_BFM_VARCHECK:\r\n" " if(strcmp((char*)ExtParam,\""); // Строка типа переменных блока (из varset) ok=ok && WriteString(file, rdsGetObjectStr(varset,RDS_HVAR_GETTYPESTRING,0)); // Завершение проверки типа переменных ok=ok && WriteString(file, "\")) return RDS_BFR_BADVARSMSG;\r\n" " break;\r\n"); // Такт расчета ok=ok && WriteString(file, " case RDS_BFM_MODEL:\r\n"); // Макрос для начала дерева переменных ok=ok && WriteString(file,"#define _pVarDataStart " " ((BYTE*)(BlockData->VarTreeData))\r\n"); // Макросы для переменных блока vdescr.servSize=sizeof(vdescr); if(rdsVSGetVarDescription(varset,-1,&vdescr)) { // В vdescr теперь - описание всей структуры переменных блока const char *type; char *soff; int offset=0; int n=vdescr.StructFields; // Число полей // Записываем макрос для каждой переменной for(int i=0;i<n;i++) if(rdsVSGetVarDescription(varset,i,&vdescr)) { // Получили описание i-й переменной блока switch(vdescr.Type) // Тип переменной { case RDS_VARTYPE_SIGNAL: case RDS_VARTYPE_LOGICAL: case RDS_VARTYPE_CHAR: type="char"; break; case RDS_VARTYPE_SHORT: type="short int"; break; case RDS_VARTYPE_INT: type="RDSINT32"; break; case RDS_VARTYPE_FLOAT: type="float"; break; case RDS_VARTYPE_DOUBLE: type="double"; break; default: // Тип не поддерживается // Смещение к следующей переменной offset+=vdescr.DataSize; continue; } if(i==0 && vdescr.Type!=RDS_VARTYPE_SIGNAL) break; // Первая переменная - не сигнал // Продолжать не имеет смысла ok=ok && WriteString(file,"#define "); // Имя переменной ok=ok && WriteString(file,vdescr.Name); ok=ok && WriteString(file," (*(("); ok=ok && WriteString(file,type); ok=ok && WriteString(file,"*)(_pVarDataStart+"); // Смещение к переменной (offset) в виде строки soff=rdsItoA(offset,10,0); ok=ok && WriteString(file,soff); rdsFree(soff); ok=ok && WriteString(file,")))\r\n"); // Смещение к следующей переменной offset+=vdescr.DataSize; // Добавление к undef директивы отмены этого макроса rdsAddToDynStr(&undef,"#undef ",FALSE); rdsAddToDynStr(&undef,vdescr.Name,FALSE); rdsAddToDynStr(&undef,"\r\n",FALSE); } // rdsVSGetVarDescription(varset,i,...)) } // if(rdsVSGetVarDescription(...) // Вставляем текст пользователя ok=ok && WriteString(file,"{\r\n"); ok=ok && WriteString(file,prog); ok=ok && WriteString(file,"\r\n}\r\n"); // Отмена макросов переменных if(undef!=NULL) { ok=ok && WriteString(file,undef); rdsFree(undef); } // Отмена макроса начала дерева переменных ok=ok && WriteString(file,"#undef _pVarTreeDataStart\r\n"); // Завершение функции модели ok=ok && WriteString(file, " break;\r\n" " } \\ switch\r\n" " return RDS_BFR_DONE;\r\n" "}\r\n" ); return ok; } //=========================================
В параметре file этой функции передается дескриптор открытого для записи файла, в который мы будем писать исходный текст, в параметре varset – идентификатор вспомогательного объекта RDS, содержащего считанную из модели структуру переменных, и, наконец, в параметре prog – указатель на текст реакции на такт расчета.
Прежде всего, нам нужно записать директивы включения заголовочных файлов, необходимых для компиляции модели, и главную функцию DLL – то есть, все то, что мы до сих пор писали вручную в примерах моделей блоков. Для этого мы вызываем функцию WriteString, передавая ей текст, содержащий все указанные строки программы, разделенные возвратом каретки и переводом строки («\r\n»), до включения файла «RdsDef.h». Перед командой его включения мы записываем описание define-константы RDS_WIN32 или RDS_WIN64 в зависимости от того, для какой версии мы компилируем сам модуль. Далее мы продолжаем записывать обычные команды включения заголовков для компиляции модели блока вплоть до главной функции DLL. Имя главной функции мы сделали параметром модуля, поэтому в качестве него мы записываем в файл поле DllMainName нашего класса. Затем мы записываем в файл параметры и тело главной функции (они не зависят от параметров модуля). Заголовок функции модели блока мы опять берем из настроек модуля: он находится в поле ModelFuncHdr. Затем опять следует фиксированный фрагмент текста, до тех пор, пока мы не дойдем до формирования реакции модели на вызов проверки типов переменных RDS_BFM_VARCHECK: строку типа переменных, с которой нужно сравнить полученную формируемой нами функцией модели строку, мы должны получить из набора переменных модели varset. Для этого мы вызываем функцию получения строки объекта rdsGetObjectStr, передавая ей идентификатор объекта varset и константу RDS_HVAR_GETTYPESTRING (функция вернет указатель на строку во внутренней памяти объекта, поэтому освобождать эту строку нам не придется). Полученную строку мы записываем в файл, за ней снова следует фиксированный фрагмент текста: завершение реакции RDS_BFM_VARCHECK и начало реакции на такт расчета RDS_BFM_MODEL. Теперь нам нужно записать текст модели, переданный в параметре prog, но сначала нужно сформировать макросы для доступа к статическим переменным, чтобы в тексте реакции можно было использовать имена этих переменных.
Перед записью макроса для самой первой переменной мы должны, как обычно, ввести вспомогательное макроопределение для указателя на начало дерева переменных блока, приведенного к какому-либо однобайтовому типу (во предыдущих примерах мы почти всегда называли этот макрос «pStart»). Конечно, можно было бы обойтись и без него, выполняя это приведение внутри каждого макроса переменной, но при этом макроопределение становится длинным и сложным для понимания (слишком много скобок и приведений типов друг за другом). В данном случае понятность макроса нам не важна – формируемый нами текст не предназначен для чтения человеком, однако, поскольку раньше мы использовали такой вспомогательный макрос, будем использовать его и сейчас, чтобы записываемый функцией WriteSourceCode текст было проще сопоставить с примерами моделей, которыми мы занимались раньше. Мы записываем в файл следующую строчку:
#define _pVarTreeDataStart ((BYTE*)(BlockData->VarTreeData))
Теперь мы сможем обращаться к любой переменной блока, отсчитывая от pVarTreeDataStart заданное число байтов.
В отличие от строки типа, макросы для переменных мы не можем получить из объекта varset одним вызовом. Придется перебрать все его переменные, формируя макрос для каждой из них вручную. Для этого нам понадобится структура описания переменной vdescr типа RDS_VARDESCRIPTION, в поле servSize которой нужно занести ее собственный размер, чтобы сервисные функции RDS смогли проверить, ту ли структуру мы просим заполнить.
Чтобы перебрать все переменные объекта varset, нам, прежде всего, нужно узнать их число и получить идентификатор содержащейся в этом объекте структуры, полями которой и являются эти переменные. Для этого мы заполняем vdescr описанием внутренней структуры переменных объекта, вызывая сервисную функцию rdsVSGetVarDescription и передавая ей идентификатор объекта varset, −1 в качестве номера переменной (так мы получим не описание одной из переменных объекта, а описание всей его структуры переменных) и указатель на заполняемую структуру &vdescr. Если функция вернет TRUE, в поле vdescr.StructFields будет находиться общее число переменных в объекте varset. Мы переписываем его во вспомогательную переменную n и используем ее как условие завершения цикла, в котором мы перебираем переменные объекта. Поскольку в этом цикле мы будем получать описание каждой переменной через ту же самую структуру vdescr, мы не можем использовать поле vdescr.StructFields в качестве условия завершения цикла – после первого же вызова rdsVSGetVarDescription в цикле в этом поле окажется другое значение. Именно поэтому перед циклом мы переписываем его в n. Для того, чтобы вставлять в макросы смещение к переменной от начала дерева, мы вводим вспомогательную переменную offset и присваиваем ей нулевое значение – в цикле мы будем каждый раз добавлять к ней размер очередной переменной.
В цикле мы получаем описание i-й переменной объекта, и, в зависимости от ее типа vdescr.Type, записываем в type указатель на строку, в которой находится описание типа этой переменной в синтаксисе языка C. Для типа RDS_VARTYPE_DOUBLE ('D') это будет строка «double», для RDS_VARTYPE_INT ('I') – «RDSINT32», и т.д. Если переменная не относится ни к одному из простых типов (в этом случае мы попадем на метку default оператора switch), мы добавляем к offset размер этой переменной и переходим к началу цикла – наши модели не поддерживают такие переменные, и макросы для них не нужны.
Если счетчик цикла i равен нулю, значит, сейчас мы будем записывать в файл макрос для самой первой переменной блока. Эта переменная обязательно должна быть сигналом, поскольку наши модели предназначены для работы только с простыми блоками, структура переменных которых всегда начинается с двух сигналов. Если тип первой переменной vdescr.Type отличается от RDS_VARTYPE_SIGNAL, мы выходим из цикла записи макросов оператором break – модель не предназначена для работы с такой структурой переменных, и продолжать не имеет смысла.
Далее в теле цикла мы формируем макроопределение для переменной i. Для этого мы записываем в файл конструкцию вида
#define vdescr.Name (*((type*)(_pVarTreeDataStart+offset)))
Здесь vdescr.Name, type и offset – значения соответствующих переменных нашей функции (для записи в файл целое число offset предварительно переводится в строку функцией rdsItoA, эта строка затем освобождается вызовом rdsFree). После этого мы добавляем к offset размер текущей переменной vdescr.DataSize (таким образом offset становится смещением к следующей переменной). Далее мы добавляем к динамической строке undef (в начале функции мы присвоили ей NULL) директиву для отмены только что записанного макроса. Когда мы запишем в файл текст реакции на такт расчета и нам понадобится отменить все введенные макросы, нам не нужно будет снова перебирать все переменные объекта varset – нам достаточно будет просто записать в файл строку undef.
Записав в файл макросы для всех простых переменных объекта varset, мы вставляем в него текст реакции на такт расчета из параметра prog, окружив его фигурными скобками (так мы даем пользователю возможность описывать свои вспомогательные переменные в тексте реакции), после чего записываем строку отмены макросов undef, если она не пустая, и освобождаем занятую ей память. Затем мы записываем в файл директиву отмены макроса _pVarTreeDataStart, дописываем завершающие строки функции модели, включая оператор return, и возвращаем значение логической переменной ok, в которую все это время записывали результат вызова функции WriteString. Таким образом, если при записи текста в файл произойдет ошибка, функция WriteSourceCode вернет FALSE.
Если обработать этой функцией пример модели из §3.3 (для определенности будем считать, что функция вызывалась из 32-битной версии RDS), то, при параметрах модуля по умолчанию получится следующий текст программы (фрагменты текста, зависящие от модели и параметров модуля автокомпиляции, выделены цветом):
#include <windows.h> #include <stdlib.h> #include <math.h> #define RDS_WIN32 #include <RdsDef.h> #define RDS_SERV_FUNC_BODY GetServiceFunc #include <RdsFunc.h> double _HugeDouble; extern "C" __declspec(dllexport) BOOL APIENTRY DllMain(HINSTANCE /*hinst*/, unsigned long reason,void */*lpReserved*/) { if(reason==DLL_PROCESS_ATTACH) { if(!RDS_SERV_FUNC_BODY()) RDS_SERV_ERROR_MSGW else rdsGetHugeDouble(&_HugeDouble); } return 1; } extern "C" __declspec(dllexport) int RDSCALL autocompModelFunc( int CallMode,RDS_PBLOCKDATA BlockData,LPVOID ExtParam) { switch(CallMode) { case RDS_BFM_VARCHECK: if(strcmp((char*)ExtParam,"{SSDDD}")) return RDS_BFR_BADVARSMSG; break; case RDS_BFM_MODEL: #define _pVarTreeDataStart ((BYTE*)(BlockData->VarTreeData)) #define Start (*((char*)(_pVarTreeDataStart+0))) #define Ready (*((char*)(_pVarTreeDataStart+1))) #define x1 (*((double*)(_pVarTreeDataStart+2))) #define x2 (*((double*)(_pVarTreeDataStart+10))) #define y (*((double*)(_pVarTreeDataStart+18))) { y=x1+x2; } #undef Start #undef Ready #undef x1 #undef x2 #undef y #undef _pVarTreeDataStart break; } // switch return RDS_BFR_DONE; }
Этот текст мало чем отличается от исходных текстов простых моделей, которые мы до сих пор писали вручную.
При работе с компилятором и редактором связей, которые будут преобразовывать сформированный функцией WriteSourceCode текст программы в работоспособную библиотеку, нам нужно будет проверять, удалось ли компилятору создать объектный файл, а редактору связей – файл DLL. Для проверки наличия этих файлов и получения их размера мы напишем вспомогательную функцию ReadFileSize, которая, получив имя файла с полным путем, вернет младшие четыре байта его размера (файлы, размер которых не умещается в четырехбайтовое беззнаковое целое, нас не интересуют):
// Получить размер файла по его имени DWORD ReadFileSize(const char *filename) { HANDLE f; DWORD size; // Преобразуем путь в UTF16 для Windows RDSWSTR fullpath_w=rdsUTF8toUTF16(filename,FALSE); // Открываем файл на чтение f=CreateFileW(fullpath_w,GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL); rdsFree(fullpath_w); if(f==INVALID_HANDLE_VALUE) // Ошибка – нет файла return 0; // Получаем размер файла size=GetFileSize(f,NULL); // Закрываем файл CloseHandle(f); return size; } //=========================================
Теперь мы можем, наконец, написать функцию LoadAndProcessModel, которая будет загружать файл модели, вызывать WriteSourceCode, после чего запускать компилятор с редактором связей.
// Загрузить и скомпилировать одну модель void TCAutoCompData::LoadAndProcessModel( RDS_COMPMODELDATA *data, // структура данных модели int fileset, // набор временных файлов BOOL varsonly) // только установка переменных { char *modeltext,*dllpath,*aux; char *vars,*prog; char *st_srcfile; char *dyn_namenoext,*dyn_objfile1,*dyn_objfile2,*dyn_dllfile; RDS_HOBJECT varset; RDS_BLOCKDESCRIPTION bdescr; BOOL ok=TRUE; // Массивы для автоматической замены строк const char *search[]={"$INCLUDE$", // 0 "$LIB$", // 1 "$NAME$", // 2 NULL}; const char *replace[sizeof(search)/sizeof(char*)]; bdescr.servSize=sizeof(bdescr); // Полный путь относительно RDS #define DYNFULLPATH(x) rdsGetFullFilePath(x,rdsGetSystemPath(RDS_GSPAPPPATH),NULL) // Читаем весь файл модели в память modeltext=ReadTextFile(data->ModelName,0); if(modeltext==NULL) // Ошибка чтения { ModelErrorMsg(data->ModelName,MEC_READERROR); return; } // Файл модели считан в modeltext - разбиваем на секции vars и prog if(!ProcessModelText(modeltext,&vars,&prog)) { rdsFree(modeltext); ModelErrorMsg(data->ModelName,MEC_NOSECTIONS); return; } // Создаем объект для работы с переменными varset=rdsVSCreateEditor(); // Записываем в объект получившийся текст описания переменных if(!rdsVSCreateByDescr(varset,vars)) { rdsFree(modeltext); rdsDeleteObject(varset); ModelErrorMsg(data->ModelName,MEC_BADVARS); return; } // Копируем переменные во все блоки, к которым подключена модель for(int i=0;i<data->NBlocks;i++) { // Берем очередной блок в списке блоков модели RDS_BHANDLE block=rdscompGetModelBlock(data->Model,i,&bdescr); if(block==NULL) continue; if(bdescr.BlockType!=RDS_BTSIMPLEBLOCK) continue; // Только для простых блоков // Записываем переменные в блок rdsVSApplyToBlock(varset,block,NULL); } if(varsonly) { // Нужно только установить переменные, компилировать не нужно rdsFree(modeltext); rdsDeleteObject(varset); return; } // Формирование исходного текста компилируемой библиотеки st_srcfile=rdsTMPCreateEmptyFileAnyExt(fileset,"$TEMP$\\model.cpp"); // В st_srcfile - указатель на имя созданного временного // файла (строка имени находится во внутренней памяти RDS) if(st_srcfile) { RDSWSTR fullpath_w=rdsUTF8toUTF16(st_srcfile,FALSE); HANDLE file=CreateFileW(fullpath_w,GENERIC_WRITE, 0,NULL,CREATE_ALWAYS,0,NULL); rdsFree(fullpath_w); if(file==INVALID_HANDLE_VALUE) // Ошибка ok=FALSE; else { // Записываем в файл исходный текст программы ok=WriteSourceCode(file,varset,prog); CloseHandle(file); } } if(!ok) ModelErrorMsg(data->ModelName,MEC_SRCWRITEERROR); // Текст модели и cписок переменных больше не нужны rdsFree(modeltext); rdsDeleteObject(varset); // Формирование имени файла без расширения и пути aux=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,"",NULL); dyn_namenoext=rdsTransformFileName(aux,RDS_TFN_GETNAME,NULL,NULL); rdsFree(aux); // Запоминание имен временных файлов, формируемых компилятором, // и создание пустых для дальнейшей проверки rdsTMPRememberFileName(fileset, dyn_objfile1=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,".obj",NULL)); rdsTMPRememberFileName(fileset, dyn_objfile2=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,".o",NULL)); rdsTMPRememberFileName(fileset, dyn_dllfile=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,".dll",NULL)); rdsTMPRememberFileName(fileset, aux=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,".lib",NULL)); rdsFree(aux); rdsTMPRememberFileName(fileset, aux=rdsTransformFileName(st_srcfile,RDS_TFN_CHANGEEXT,".tds",NULL)); rdsFree(aux); // Занесение различных путей в список замены replace replace[0]=IncludePath; // "$INCLUDE$" replace[1]=LibPath; // "$LIB$" replace[2]=dyn_namenoext; // "$NAME$" // Запуск компилятора if(ok) { char *fullpath=DYNFULLPATH(CompPath); if(RunAndWaitSetPath(fullpath,CompParams,search,replace,CompSetPath)) { // Проверяем наличие .obj if(ReadFileSize(dyn_objfile1)==0 && ReadFileSize(dyn_objfile2)==0) // Нулевого размера { ok=FALSE; ModelErrorMsg(data->ModelName,MEC_NOOBJFILE); } } else // Ошибка запуска { ok=FALSE; ModelErrorMsg(data->ModelName,MEC_COMPSTARTERROR); } rdsFree(fullpath); } // Запуск редактора связей if(ok) { char *fullpath=DYNFULLPATH(LinkPath); if(RunAndWaitSetPath(fullpath,LinkParams,search,replace,LinkSetPath)) { // Проверяем наличие .dll if(ReadFileSize(dyn_dllfile)==0) // Нулевого размера { ok=FALSE; ModelErrorMsg(data->ModelName,MEC_NODLLFILE); } } else // Ошибка запуска { ok=FALSE; ModelErrorMsg(data->ModelName,MEC_LINKSTARTERROR); } rdsFree(fullpath); } // Полный путь к файлу DLL (куда ссылается блок) dllpath=rdsGetFullFilePath(data->CompDllName,NULL,NULL); // Копируем созданный во временной директории файл DLL в папку модели if(ok) { RDSWSTR from_w=rdsUTF8toUTF16(dyn_dllfile,FALSE), to_w=rdsUTF8toUTF16(dllpath,FALSE); ok=CopyFileW(from_w,to_w,FALSE); rdsFree(from_w); rdsFree(to_w); if(!ok) ModelErrorMsg(data->ModelName,MEC_DLLCOPYERROR); } // Освобождаем динамически созданные пути rdsFree((void*)(replace[0])); rdsFree((void*)(replace[1])); rdsFree(dyn_namenoext); rdsFree(dyn_objfile1); rdsFree(dyn_objfile2); rdsFree(dyn_dllfile); rdsFree(dllpath); #undef DYNFULLPATH } //=========================================
В эту функцию передается указатель на структуру данных модели data, идентификатор набора временных файлов fileset (набор мы создали в функции CompileModels перед вызовом LoadAndProcessModel) и логический параметр varsonly, принимающий значение TRUE, если нам нужно только установить структуру переменных в блоках, использующих эту модель. В самом начале функции мы описываем массивы строк search и replace, которые мы будем использовать для замены символических констант, введенных нами в командных строках компилятора и редактора связи, на их реальные значения. Эти массивы будут передаваться в написанную нами вспомогательную функцию RunAndWait (через вызывающую ее RunAndWaitSetPath), в которой они используются в вызове rdsStringReplace. Массив search мы сразу инициализируем указателями на имена используемых нами констант, завершая его значением NULL, а массив replace пока оставляем пустым – указатели на значения констант мы запишем в него позже. Число элементов в массиве replace мы делаем равным числу элементов массива search, вычисляя его как размер search в байтах (sizeof(search)), деленный на размер одного элемента (sizeof(char*)).
Сначала мы считываем в память файл модели, имя которой находится в параметре data->ModelName, функцией ReadTextFile, и записываем возвращенный ей указатель на динамически отведенную область памяти с загруженным текстом вы переменную modeltext. Мы написали эту функцию так, чтобы в нее можно было передавать имя файла с относительными путями и символическими константами, каким является имя модели в нашем модуле автокомпиляции, поэтому нам не нужно предварительно обрабатывать эти имя функцией rdsGetFullFilePath. Считанный текст мы разбиваем на секцию описания переменных vars и секцию реакции на такт расчета prog, создаем объект для работы с переменными varset и формируем в нем набор переменных, указанных в секции vars. Теперь текст модели разобран – можно приступать к установке переменных блоков, формированию текста программы на языке C и его компиляции.
Сначала мы должны скопировать в каждый блок, к которому присоединена данная модель, переменные из объекта varset. Блоки, обслуживаемые моделью, мы перебираем в цикле for со счетчиком i, изменяющимся от 0 до data->NBlocks (в этом поле структуры данных модели всегда хранится общее число блоков с этой моделью). В цикле мы получаем идентификатор i-го блока модели функцией rdscompGetModelBlock, которая попутно заполняет структуру описания этого блока bdescr, и, если этот блок – простой (его тип RDS_BTSIMPLEBLOCK), копируем в него структуру переменных объекта varset функцией rdsVSApplyToBlock.
После того, как всем блокам присвоена структура переменных модели, мы проверяем, не равен ли TRUE параметр varsonly. Если это так, функция LoadAndProcessModel вызвана только для установки переменных блоков, и записывать текст программы DLL и вызывать компилятор не нужно. В этом случае мы уничтожаем все созданные динамические строки и объекты и немедленно завершаем функцию.
Теперь нам нужно сформировать исходный текст DLL с функцией модели блока согласно разобранному файлу модели и параметрам модуля автокомпиляции. После компиляции этот файл нужно будет удалить, поэтому мы воспользуемся встроенным в RDS механизмом работы с временными файлами – создадим пустой временный файл вызовом функции rdsTMPCreateEmptyFileAnyExt, передав ей идентификатор набора временных файлов из параметра fileset и имя создаваемого файла «$TEMP$\model.cpp». Эта функция преобразует константу «$TEMP$» в путь к папке временных файлов RDS и, если там нет файлов с с именами «model.*» (т.е. с именем «model» и любым расширением), создаст пустой файл с этим именем. Если такие файлы там уже есть, функция автоматически изменит имя файла так, чтобы оно было уникальным. В любом случае, функция вернет указатель на строку с полным путем к созданному файлу, которую мы запишем в переменную st_srcfile. Эта строка находится во внутренней памяти RDS и нам не нужно освобождать ее вызовом rdsFree. Созданный таким образом файл будет автоматически удален при удалении набора fileset (мы делаем это в функции CompileModels после вызова LoadAndProcessModel) или при завершении RDS.
Создав пустой временный файл, мы преобразуем его имя в кодировку UTF16, открываем файл для записи вызовом функции Windows API CreateFileW и формируем в нем исходный текст программы DLL вспомогательной функцией WriteSourceCode. После этого мы можем освободить память, занимаемую загруженным текстом модели modeltext и удалить объект varset, содержащий набор переменных модели – они нам больше не потребуются.
Теперь мы имеем файл с текстом модели блока на языке C, который нам нужно скомпилировать. Для этого нам предстоит вызвать компилятор, который преобразует исходный текст в объектный файл, а затем – редактор связей, который преобразует объектный файл в исполняемый файл DLL и создаст еще пару вспомогательных файлов. Все эти файлы нам тоже нужно будет удалить после компиляции, поэтому мы будем добавлять их к уже созданному набору временных файлов с идентификатором fileset. Для этого мы сначала формируем в переменной dyn_namenoext имя файла исходного текста, который нам подобрала функция rdsTMPCreateEmptyFileAnyExt, но без пути и расширения. Именно на это имя в параметрах командной строки мы будем заменять символическое имя «$NAME$». Затем из имени файла исходного текста мы формируем еще несколько имен файлов добавляем эти имена в набор временных файлов вызовом rdsTMPRememberFileName:
- предполагаемое имя объектного файла с расширением «.obj», используемое большинством компиляторов в Windows (записывается в переменную dyn_objfile1);
- предполагаемое имя объектного файла с расширением «.o», используемое компиляторами GCC (записывается в переменную dyn_objfile2);
- имя созданного файла DLL расширением «.dll» (записывается в переменную dyn_dllfile);
- имя файла библиотеки с расширением «.lib», который может быть создан в процессе компиляции;
- имя файла с отладочной информацией с расширением «.tds», который создается некоторыми компиляторами.
Мы заранее не знаем, какие именно расширения объектных файлов будет использовать компилятор, который будет вызываться из нашего модуля, поэтому мы предусматирваем два возможных варианта. Это недостаток нашего модуля: расширения файлов, с одной стороны, указываются пользователем в параметрах модуля внутри командных строк компилятора и редактора связей. Но, с другой стороны, они жестко «зашиты» в программу самого модуля. То есть, если, например, компилятор будет формировать объектный файл с расширением, отличным от «.obj» и «.o», пользователь сможет указать это в настройках модуля, но сам модуль с таким файлом работать не сможет. В «правильно написанном» модуле следовало бы предусмотреть настройки для расширения объектного файла, создаваемого компилятором, а также, на всякий случай, для расширения результирующего файла DLL. Кроме того, следовало бы добавить настройку для указания расширений всех создаваемых временных файлов для их последующего удаления. Мы не будем этого делать чтобы не усложнять пример.
На данный момент мы уже знаем значения всех символических констант, которые мы разрешаем пользователю указывать в командной строке, поэтому мы заносим их в массив replace. В нулевой и первый элементы массива записываются указатели на пути к папкам заголовков и библиотек компилятора из параметров модуля IncludePath и LibPath, а в третий – указатель на имя исходного файла без пути и расширения dyn_namenoext.
Для запуска компилятора мы вызываем вспомогательную функцию RunAndWaitSetPath, передавая ей путь к исполняемому файлу компилятора из параметра модуля CompPath, его командную строку с символическими константами CompParams, массивы для поиска и замены символических констант search и replace, а также признак необходимости добавления пути к исполняемому файлу в переменную окружения CompSetPath. Если RunAndWaitSetPath вернула TRUE, значит, компилятор был успешно запущен и уже завершил работу (функция RunAndWait ждет завершения процесса) – при этом мы проверяем размер объектных файлов dyn_objfile1 и dyn_objfile2, один из которых должен быть создан в результате компиляции. Если оба эти файла отсутствуют или имеют нулевой размер, значит, компилятору не удалось создать объектный файл. При правильных настройках модуля это будет означать наличие ошибок в тексте модели, о чем мы сообщаем пользователю. Если объектный файл имеет ненулевой размер, мы точно так же вызываем редактор связей, после чего проверяем наличие и размер исполняемого файла dyn_dllfile. Если редактор связей успешно создал этот файл, нам нужно скопировать его в ту же папку, в которой находится файл модели, под другим именем, ранее установленным нами в реакции модуля на RDS_COMPM_PREPARE. Это имя, а точнее, относительный путь к файлу DLL модели блока, находится в поле CompDllName структуры данных модели RDS_COMPMODELDATA. Мы преобразуем этот относительный путь в полный путь dllpath вызовом rdsGetFullFilePath, после чего функцией Windows API CopyFileW копируем файл в папку модели, предварительно преобразовав оба пути в кодировку UTF16. Теперь библиотека с моделью блока сформирована и помещена в нужную папку с нужным именем, и RDS подключит ее к блоку, как только модуль автокомпиляции вернет управление.
Нам осталось написать функцию AttachBlock, которая вызывается из реакции RDS_COMPM_ATTACHBLOCK при подключении модели к блоку. В ней мы должны, при необходимости, скопировать в блок переменные модели, иначе, если мы присоединяем к новому блоку модель, не требующую в данный момент компиляции, блок останется со старой структурой переменных.
// Связать блок с моделью void TCAutoCompData::AttachBlock(RDS_COMPBLOCKOPDATA *param) { // Если модель устанавливается пользователем вручную, // нужно поменять у блока структуру переменных if(param->AttachReason==RDS_COMP_AR_MANUALSET) LoadAndProcessModel(param->Model,-1,TRUE); } //=========================================
Здесь мы поступаем просто: если модель подключена к блоку пользователем вручную (параметр param->AttachReason имеет значение RDS_COMP_AR_MANUALSET), мы вызываем функцию LoadAndProcessModel для модели param->Model со значением −1 вместо идентификатора набора временных файлов и TRUE в последнем параметре varsonly. При таком сочетании параметров функция загрузит файл указанной модели, скопирует во все обслуживаемые ей блоки структуру переменных, описанную в этом файле, после чего завершится без вызова компилятора. Конечно, нам нужно переписать структуру переменных только в один блок, идентификатор которого передается в param->Block, но, чтобы не усложнять LoadAndProcessModel, мы обработаем все блоки. Это не приведет к сколько-нибудь существенным задержкам.
Может возникнуть вопрос: почему мы копируем переменные модели в блок только при ручном подключении? Дело в том, что во всех остальных случаях подключения модели к блоку – при загрузке блока из файла, вставке из буфера обмена и т.п. – структура переменных блока уже должна быть правильной, ведь на момент сохранения блока или его копирования в буфер модель уже была подключена, а значит, она уже установила в блоке правильные переменные. Можно придумать ситуацию, в которой это будет не так: например, если мы сохраним схему с блоком, к которому подключена модель, а потом заменим файл модели и соответствующий ей скомпилированный файл DLL на другие, то на момент загрузки схемы переменные блока не будут соответствовать подключаемой модели, причем модель не будет требовать компиляции. Однако, ничего страшного при этом не произойдет: формируемые нами модели содержат стандартную проверку типов переменных RDS_BFM_VARCHECK, и при неправильной структуре переменных модель просто откажется работать, пока пользователь не скомпилирует ее заново.
Если бы мы вызывали LoadAndProcessModel при каждом подключении модели к блоку, это привело бы к существенному замедлению загрузки схемы, потому что при этом каждый раз загружался бы файл модели. Выбранный нами вариант позволяет добиться некоторого компромисса между удобством и надежностью без сильного усложнения модуля автокомпиляции. Для того, чтобы структура переменных блока всегда соответствовала подключаемой модели, можно было бы создать для модели личную область данных и загружать туда описание переменных из файла модели при ее подключении к самому первому блоку, а для последующих подключений использовать уже загруженные данные (тогда файл загружался бы только один раз), но это существенно усложнило бы модуль, поэтому в данном примере мы не будем этого делать.
Теперь мы имеем работоспособный настраиваемый модуль автоматической компиляции, позволяющий задавать структуру переменных блока и его реакцию на такт расчета. При внесении изменений в модель этот модуль будет автоматически вызывать указанный в его параметрах компилятор и заменять DLL блоков на новую. Компилятор будет вызываться в отдельном окне консоли. После внесения изменений в модель компилятор будет вызываться при переходе в режим моделирования или расчета, при загрузке схемы, использующей измененные модели, или по команде пользователя.
На самом деле, созданный нами модуль подходит только для создания очень простых моделей. В нем не хватает многих возможностей – в частности, работы с матрицами, строками и динамическими переменными. Входящие в комплект RDS модули для работы с основными компиляторами языка C++ поддерживают динамические переменные, сохранение и загрузку параметров блоков, окна настройки, реакцию на функции и т.п. Все это можно добавить и в наш модуль за счет усложнения пользовательского интерфейса редактора модели и функции формирования текста компилируемой DLL, но, поскольку созданный нами модуль предназначен только для иллюстрации основных приемов создания модулей автоматической компиляции, мы не будем этого делать.