ROPS (Rem Object Pascal Script) - встраиваемый интерпретатор языка Pascal. Введение, состав дистрибутива, компонент PSScript


Введение

  Pascal Script (PS) — это интерпретатор, совместимый с Object Pascal/Delphi/Lazarus, с компилятором байт-кода, который предоставляет среду сценариев для прикладных программ. В Lazarus включен в состав дистрибутива и входит в палитру компонентов.
  В настоящее время он работает в macOS, Windows и Linux на 32-битных и 64-битных процессорах x86, PowerPC и ARM.
Он был создан и поддерживается Carlo Kok в 2001 г. и защищен авторскими правами программного обеспечения RemObjects как бесплатное программное обеспечение с доступным полным исходным кодом.

  Его основные характеристики:
  • поддерживается почти весь синтаксис Object Pascal;
  • Поддерживаются классы Delphi/Lazarus (однако их нельзя объявить внутри скрипта);
  • может создавать полностью рабочие формы GUI с компонентами;
  • легко импортировать новые классы в скриптовый движок;
  Дистрибутив доступен на GitHub. Дистрибутив постоянно обновляется, на момент написания статьи (uPSUtils.PSCurrentversion = '1.31') содержит установочные пакеты для Delphi 3… Delphi 28, Kylix 3, Lazarus. Для экспериментов использовалась Delphi 7.
  Не смотря на уже достаточно почтенный возраст PS, качественной справочной информации по нему немного. В дистрибутиве можно найти каталог 'help' с файлом 'Script.dox' в формате 'Doc-O-Matic' и каталог 'Articles' с двумя статьями на английском — 'Using RemObjects Pascal Script' и 'Using Classes with RemObjects PascalScript'. Их русский перевод:
  Как видно предлагается учиться на примерах, модернизируя при необходимости. Подход понятный, но иногда недостаточный, если нужна более полная информация о внутреннем устройстве кода.

Состав дистрибутива

В дистрибутив входят два компонента интерпретатора:
  •   PSScript — визуальный компонент обьединяющий в себе компилятор (uPSCompiler.pas), среду времени выполнения (uPSRuntime.pas), препроцессор (uPSPreProcessor.pas) и интерфейс плагинов (uPSComponent.pas, TPSPlugin);
      PSScriptDebugger — наследник PSScript, добавляет отладочные возможности (точки останова);
  • и девять компонентов плагинов, расширяющих возможности интерпретатора:
    •   PSDllPlugin — плагин библиотеки импорта для использования в Pascal Script функций внешних Dll;
    •   PSImport_Classes — плагин библиотеки импорта для простых классов;
    •   PSImport_Controls — плагин библиотеки импорта для простых контроллов (оконных классов);
    •   PSImport_StdCtrls — плагин библиотеки импорта для стандартных контроллов;
    •   PSImport_Forms — плагин библиотеки импорта для форм;
    •   PSImport_DB — плагин библиотеки импорта для баз данных;
    •   PSImport_ComObj — плагин библиотеки импорта для компонентов COM, Ole и ActiveX;
    •   PSImport_DateUtils — плагин библиотеки импорта для типа TDateTime;
    •   PSCustomPlugin — плагин-пустышка для легкой разработки собственных плагинов;
    Кроме этих компонентов в каталоге ./Source/ThirdParty лежит пакет с добавочными компонентами:
    • TPSImport_IBX — компоненты для работы с InterBase;
    • TPSImport_Mask — компоненты редактора по маске TMaskEdit;
    • TPSImport_JvMail — компоненты почтовой связи из компонентов JEDI;
    • TPSImport_Dialogs — компоненты со вкладки Dialogs палитры компонентов Delphi;
    • TPSImport_Registry — компоненты для работы с INI файдами и реестром Windows из Delphi;

    Препроцессор

      Компонент PSScript содержит отключаемую опцию препроцессора. Если PSScript.UsePreProcessor = True, перед компиляцией скрипта будет запущен его препроцессинг (поиск и выполнение директив компилятора). Директива заключается в фигурные скобки и начинается со знака $. Движок поддерживает несколько директив:
    • $I ($INCLUDE) — включение кода модуля.
    • $DEFINE, $UNDEF, $IFDEF, $IFNDEF, $ELSE, $ENDIF — условная компиляция.
      если препроцессор встречает директиву включения кода, он вызывает событие PSScript.OnNeedFile. По нему в программу передается имя требуемого файла и буфер для него. Программа должна заполнить буфер содержимым файла и вернуть True, если она вернет False, значит файл недоступен.  При условной компиляции можно пользоваться стандартным определением условия компиляции с помощью директивы $DEFINE. Однако компонент предлагает еще один способ определения — с помощью списка Defines самого компонента. Все определения из этого списка работают аналогично определенным в коде скрипта.

    Компилятор

      Архитектура интерпретатора Pascal Script (далее PS) написана максимально гибко. Правила, по которым будет интерпретатироваться код, сам интерпретатор узнает только во время компиляции скрипта в байт-код и его выполнения. Например для адаптации к языку Паскаль служат следующие функции:
    • UPSCompiler.TPSPascalCompiler.DefineStandardProcedures — для компилятора;
    • UPSRuntime.DefProc — для среды выполнения;

    • UPSRuntime.TPSExec.RegisterStandardProcs — для среды выполнения;
    Класс TPSPascalCompiler компилирует скрипт (программу на языке Паскаль) в промежуточный байт-код. Для сбора и использования при компиляции нужных данных он содержит шесть списков:

    • FTypes: TPSList;
    • FConstants: TPSList;
    • FVars: TPSList;
    • FProcs: TPSList;
    • FRegProcs: TPSList;
    • FClasses: TPSList;

    Простейшая программа-пустышка, которая ничего не делает и занимает в исходном виде 26 байт
    program Test;
    begin
    end.
    
    после компиляции превратится в следующий байт-код, размер 227 байт

      * Первая строчка с названием необязательна.




      Как видно, байт-код в самом начале имеет заголовок-сигнатуру, двойное слово 0x53504649 или 'IFPS' в ANSI (uPSUtils.PSValidHeader = 1397769801). Это позволяет программно идентифицировать скомпилированный интерпретатором скрипт. Операторы (зарезервированные слова) языка Паскаль в байт-коде не присутствуют, они представлены определенными константными байтами. Поэтому с ростом кол-ва исходного кода, размер байт-кода сначала сравняется с ним, а затем станет меньше исходного.
      Средсвами PS можно посмотреть, как видит свою программу сам интерпретатор (код дизассемблера в модуле uPSDisassembly):
    • [TYPES]
      Type [0]: Pointer
      Type [1]: U32
      Type [2]: Variant
      Type [3]: PChar
      Type [4]: Currency
      Type [5]: Extended
      Type [6]: Double
      Type [7]: Single
      Type [8]: S64
      Type [9]: String
      Type [10]: U32
      Type [11]: S32
      Type [12]: S16
      Type [13]: U16
      Type [14]: S8
      Type [15]: ProcPtr Export: ANYMETHOD
      Type [16]: String
      Type [17]: String
      Type [18]: String
      Type [19]: String
      Type [20]: UnicodeString
      Type [21]: WideString
      Type [22]: WideChar
      Type [23]: Char
      Type [24]: U8
      Type [25]: U16
      Type [26]: U32
      Type [27]: U8 Export: BOOLEAN
      Type [28]: U8
    • [VARS]
    • [PROCS]
      Proc [0] Export: !MAIN -1
        [0] RET

    Понять тут можно не много, так интерпретатор регистрирует в пустой программе 29 типов и экспортируемую процедуру !MAIN (uPSUtils.PSMainProcName = '!MAIN').

    Типы предварительно определенные в классе TPSPascalCompiler (список FTypes):

    • Byte = U8;
    • Boolean = Enum;
    • LongBool = Enum;
    • WordBool = Enum;
    • ByteBool = Enum;
    • Char = Char;
    • WideChar = WideChar;
    • WideString = WideString;
    • UnicodeString = UnicodeString;
    • AnsiString = String;
    • string = String;
    • NativeString = String;
    • AnyString = String;
    • AnyMethod = ProcPtr;
    • ShortInt = S8;
    • Word = U16;
    • SmallInt = S16;
    • LongInt = S32;
    • ___Pointer = Pointer;
    • LongWord = U32;
    • Integer = LongInt;
    • Cardinal = LongWord;
    • tbtString = String;
    • Int64 = S64;
    • Single = Single;
    • Double = Double;
    • Extended = Extended;
    • Currency = Currency;
    • PChar = PChar;
    • Variant = Variant;
    • !NotificationVariant = NotificationVariant;
    • TVariantArray = Array of Variant;
    • TVarType = Word;
    • TIFException = Enum;
    • IUnknown = Interface;
    • IInterface = Interface;
    • IDispatch = Interface;
    • !OPENARRAYOFVARIANT = Array of Variant;


    Константы предварительно определенные в классе TPSPascalCompiler (список FConstants):

    • False: Boolean = 0;
    • True: Boolean = 1;
    • varEmpty: Word = 0 (0x0);
    • varNull: Word = 1 (0x1);
    • varSmallInt: Word = 2 (0x2);
    • varInteger: Word = 3 (0x3);
    • varSingle: Word = 4 (0x4);
    • varDouble: Word = 5 (0x5);
    • varCurrency: Word = 6 (0x6);
    • varDate: Word = 7 (0x7);
    • varOleStr: Word = 8 (0x8);
    • varDispatch: Word = 9 (0x9);
    • varError: Word = 10 (0xA);
    • varBoolean: Word = 11 (0xB);
    • varVariant: Word = 12 (0xC);
    • varUnknown: Word = 13 (0xD);
    • varShortInt: Word = 16 (0x10);
    • varByte: Word = 17 (0x11);
    • varWord: Word = 18 (0x12);
    • varLongWord: Word = 19 (0x13);
    • varInt64: Word = 20 (0x14);
    • varStrArg: Word = 72 (0x48);
    • varAny: Word = 257 (0x101);
    • varString: Word = 256 (0x100);
    • varTypeMask: Word = 4095 (0xFFF);
    • varArray: Word = 8192 (0x2000);
    • varByRef: Word = 16384 (0x4000);
    • ErNoError: TIFException = 0;
    • erCannotImport: TIFException = 1;
    • erInvalidType: TIFException = 2;
    • ErInternalError: TIFException = 3;
    • erInvalidHeader: TIFException = 4;
    • erInvalidOpcode: TIFException = 5;
    • erInvalidOpcodeParameter: TIFException = 6;
    • erNoMainProc: TIFException = 7;
    • erOutOfGlobalVarsRange: TIFException = 8;
    • erOutOfProcRange: TIFException = 9;
    • ErOutOfRange: TIFException = 10;
    • erOutOfStackRange: TIFException = 11;
    • ErTypeMismatch: TIFException = 12;
    • erUnexpectedEof: TIFException = 13;
    • erVersionError: TIFException = 14;
    • ErDivideByZero: TIFException = 15;
    • ErMathError: TIFException = 16;
    • erCouldNotCallProc: TIFException = 17;
    • erOutofRecordRange: TIFException = 18;
    • erOutOfMemory: TIFException = 19;
    • erException: TIFException = 20;
    • erNullPointerException: TIFException = 21;
    • erNullVariantError: TIFException = 22;
    • erInterfaceNotSupported: TIFException = 23;
    • erCustomError: TIFException = 24;

    Константы начинающиеся на 'var' относятся к вариантной записи TVarType, начинающиеся на 'er' к номерам ошибок TIFException.

    Кроме того в процедуре DefineStandardProcedures компилятор регистрирует для применения следующие функции и процедуры, как стандартные для Delphi, так и необходимые интерпретатору (список FProcs):

    • function Assigned(I: LongInt): Boolean;
    • procedure _T(Name: tbtString; V: Variant);
    • function _T(Name: tbtString): Variant;
    • function IntToStr(I: Int64): string;
    • function StrToInt(S: string): LongInt;
    • function StrToIntDef(S: string; def: LongInt): LongInt;
    • function Copy(S: AnyString; iFrom: LongInt; iCount: LongInt): AnyString;
    • function Pos(SubStr: AnyString; S: AnyString): LongInt;
    • procedure Delete(var S: AnyString; iFrom: LongInt; iCount: LongInt);
    • procedure Insert(S: AnyString; var s2: AnyString; iPos: LongInt);
    • function GetArrayLength(Arr): LongInt;
    • procedure SetArrayLength(var arr; count: LongInt);
    • function StrGet(var S: string; I: LongInt): Char;
    • function StrGet2(S: string; I: LongInt): Char;
    • procedure StrSet(C: Char; I: LongInt; var S: string);
    • function WStrGet(var S: AnyString; I: LongInt): WideChar;
    • procedure WStrSet(C: AnyString; I: LongInt; var S: AnyString);
    • function VarArrayGet(var S: Variant; I: LongInt): Variant;
    • procedure VarArraySet(C: Variant; I: LongInt; var S: Variant);
    • function AnsiUpperCase(S: string): string;
    • function AnsiLowerCase(S: string): string;
    • function UpperCase(S: AnyString): AnyString;
    • function LowerCase(S: AnyString): AnyString;
    • function Trim(S: AnyString): AnyString;
    • function Length(S): LongInt;
    • procedure SetLength(var s; NewLength: LongInt);
    • function Low(X): Int64;
    • function High(X): Int64;
    • procedure Dec(var x);
    • procedure Inc(var x);
    • procedure Include(var s; m);
    • procedure Exclude(var s; m);
    • function Sin(E: Extended): Extended;
    • function Cos(E: Extended): Extended;
    • function Sqrt(E: Extended): Extended;
    • function Round(E: Extended): LongInt;
    • function Trunc(E: Extended): LongInt;
    • function Int(E: Extended): Extended;
    • function Pi: Extended;
    • function Abs(E: Extended): Extended;
    • function StrToFloat(S: string): Extended;
    • function FloatToStr(E: Extended): string;
    • function PadL(S: AnyString; I: LongInt): AnyString;
    • function PadR(S: AnyString; I: LongInt): AnyString;
    • function PadZ(S: AnyString; I: LongInt): AnyString;
    • function Replicate(C: Char; I: LongInt): string;
    • function StringOfChar(C: Char; I: LongInt): string;
    • function Unassigned: Variant;
    • function VarIsEmpty(V: Variant): Boolean;
    • function VarIsClear(V: Variant): Boolean;
    • function Null: Variant;
    • function VarIsNull(V: Variant): Boolean;
    • function VarType(V: Variant): Word;
    • procedure RaiseLastException;
    • procedure RaiseException(Ex: TIFException; Param: string);
    • function ExceptionType: TIFException;
    • function ExceptionParam: string;
    • function ExceptionProc: LongWord;
    • function ExceptionPos: LongWord;
    • function ExceptionToString(er: TIFException; Param: string): string;
    • function StrToInt64(S: string): Int64;
    • function Int64ToStr(I: Int64): string;
    • function StrToInt64Def(S: string; def: Int64): Int64;
    • function SizeOf(Data): LongInt;
    • function IdispatchInvoke(Self: IDispatch; PropertySet: Boolean; Name: AnsiString; Par: !OPENARRAYOFVARIANT): Variant;

    Кроме этого регистрируется основная процедура интерпретатора (список FRegProcs):
    • procedure Main Proc;
    Эта процедура и исполняет скрипт, вспомним наличие в скомпилированном коде-пустышке процедуры !MAIN (uPSUtils.PSMainProcNameOrg = 'Main Proc').

    Среда времени выполнения

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

    Интерфейс плагинов

      В компоненте PSScript интерфейс плагинов представлен списком Plugins, производным от TCollection. Каждый плагин, для того чтобы его можно было поместить в палитру компонентов, представляет собой наследника TComponent:
      TPSPlugin = class(TComponent)
      public
        procedure CompOnUses(CompExec: TPSScript); virtual;
        procedure CompileImport1(CompExec: TPSScript); virtual;
        procedure CompileImport2(CompExec: TPSScript); virtual;
        procedure ExecOnUses(CompExec: TPSScript); virtual;
        procedure ExecImport1(CompExec: TPSScript; const ri: TPSRuntimeClassImporter); virtual;
        procedure ExecImport2(CompExec: TPSScript; const ri: TPSRuntimeClassImporter); virtual;
      end;
    
    Первые три процедуры вызываются средой компилятора, три последующие средой времени выполнения.

      В компоненте TPSScript процессом компиляции занимается встроенный private метод ScriptUses, он на этапе создания компонента подключается к событию OnUses компилятора, компиляция начинается при вызове метода Compile, последовательность процесса компиляции следующая:
    1. До срабатывания события OnUses вызывается встроенные в компилятор методы DefineStandardTypes и DefineStandardProcedures, регистрирующие в среде компиляции простые стадартные типы языка Паскаль и наиболее употребительные процедуры и функции. Это происходит независимо от наличия или отсутствия подключения к компоненту TPSScript внешних плагинов. Далее срабатывает событие OnUses компилятора, т.е. процедура ScriptUses компонента TPSScript.
    2. Запускается перебор подключенных плагинов из списка Plugins компонента TPSScript. Для каждого подключенного плагина запускается процедура CompOnUses самого плагина.
    3. Еще раз запускается перебор подключенных плагинов из списка Plugins компонента TPSScript. Для каждого подключенного плагина запускается процедура CompileImport1 самого плагина.
    4. Срабатывает событие CompImport компонента TPSScript, а значит и внешний метод если он определен.
    5. И еще раз запускается перебор подключенных плагинов из списка Plugins компонента TPSScript. Для каждого подключенного плагина запускается процедура CompileImport2 самого плагина.
    6. Срабатывает событие OnCompile компонента TPSScript, а значит и внешний метод если он определен.
    7. Т.к. метод ScriptUses это функция, в конце него проверяется список сообщений компилятора, и в зависимости от наличия/присутствия ошибок зависит ее результат.
      Таким образом информация в среду компиляции компонентом TPSScript добавляется в следующих местах:
    1. Сначала компилятор добавляет информацию о простых стадартных типах языка Паскаль и наиболее употребительных процедурах и функциях (DefineStandardTypes и DefineStandardProcedures).
    2. Сам компонент TPSScript может добавить информацию во внешнем методе, подключенном к событию CompImport. Строго говоря добавить информацию можно и в OnCompile компонента, это не ошибка, но так обычно не делают. Событие OnCompile лучше использовать для контроля информации, или ее переноса т.к. он вызывается последним.
    3. Каждый подключенный плагин может может добавить информацию во внешних методах, подключенным к событиям CompOnUses, CompileImport1 и CompileImport2 плагина.
      Для правильной работы компилятора важен порядок размещения плагинов в списке компонента PSScript, т.е. в каком порядке они будут зарегистрированы компилятором. Плагины, использующие типы и классы которые в самом плагине не обьявлены, должны находится в списке позже плагинов в которых эти типы и классы обьявляются. Вы получите ошибку компилятора, если плагин не может зарегистрировать ваши типы и классы, т.к. они ссылаются на типы и классы объектов, которые еще не были зарегистрированы (и в самом плагине не обьявлены). К сожалению, система PS Plugin не имеет никакого способа автоматического разрешения зависимостей (предложенного автором), а сообщение об ошибке компилятора не скажет вам, какой тип он не смог найти и где это место в коде.  Покажу на примере, допустим вы разместили на форме компонент PSScript и подключили к нему плагин PSImport_Controls. Если теперь запустить компиляцию (PSScript.Compile) получите ошибку
    • [Error] (1:1): Unable to register type TMouseEvent
    как видно из сообщения понятно только что не удалось зарегистрировать тип TMouseEvent, а вот что это за тип и как он регистрируется можно понять только просмотрев исходники PSImport_Controls. Смотрим и находим строку
    • AddTypeS('TMouseEvent', 'procedure (Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);');
      Сразу-же видим ссылку на тип TObject, который в данном плагине не обьявлен, он обьявляется в плагине PSImport_Classes, который и должен быть зарегистрирован ранее плагина PSImport_Controls.

    PS.

    Другие статьи из цикла «ROPS (Rem Object Pascal Script)»:
  • +3
  • 05 октября 2022, 17:54
  • anakost

Комментарии (0)

RSS свернуть / развернуть
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.