Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
621 lines (505 sloc) 37.4 KB

Пакет ppRuby

Пакет ppRuby предоставляет доступ к Ruby API для языка Free Pascal. Использовать его можно для двух основных задач:

  • Написание библиотек расширения для Ruby;
  • Встраивание Ruby в программы на Free Pascal / Lazarus в качестве скриптового языка.

В первом случае понадобится только ruby.pas скомпилированный в статическом режиме. Во втором будет лучше (хотя и необязательно) использовать прочие модули пакета, которые предоставляют разнообразные утилиты для удобной работы, а также механизм конвертации pascal-объектов в Ruby, автоматически транслирующий published-свойства.

Введение

Состав пакета

  • Каталог demo — содержит небольшой демонстрационный проект.
    • Подкаталог rb — скрипты (rb/*.rb), предназначенные для запуска в демо-проекте. По мере расширения возможностей пакета этот подкаталог также будет дополняться.
    • Файлы: aboutform.{lfm,pas}, demo.{ico,lpi,lpr}, mainform.{lfm,pas} — собственно исходные файлы демо-проекта.
  • Каталог doc и файлы doc/*.md — настоящая документация.
  • Каталог img и файлы img/logo.{png,xcf} — логотип проекта.
  • Каталог src — собственно исходные файлы пакета.
    • rbopts.inc — включаемый файл, определяющий существенные для компиляции параметры. Включается во все модули пакета.
    • rbdefs.inc — включаемый файл, доопределяющий переменные условной компиляции в зависимости от уже заданных. Требуется только для ruby.pas, вынесен в отдельную сущность ради логического разделения.
    • ruby.pas — определяет необходимые типы и предоставляет доступ к API. В дополнение к этому определяет процедуры инициализации, финализации и управление выбором версии в случае динамической загрузки библиотеки. Для удобства большая часть кода вынесена во включаемые файлы:
      • rbtypes.inc — определения типов, используемых функциями API.
      • rbdynintf.inc — интерфейсная часть для динамической загрузки.
      • rbstatintf.inc — интерфейс статической загрузки.
      • rbmacrointf.inc — интерфейс для inline-функций, представленных в оригинальном API (на языке C) как макросы.
      • rbdynimpl.inc — реализация вспомогательных функций, необходмых для динамической загрузки.
      • rbmacroimpl.inc — реализация функций-«макросов».
      • rbdyninit.inc — инициализация механизма динамической загрузки.
    • rbtools.pas — базовый вспомогательный модуль, предоставляющий функции конвертации данных основных типов языка Pascal в объекты Ruby, а также «обертки» над некоторыми функциями API, необходимые для удобного и безопасного использования в pascal-контексте (см. ниже подраздел «Исключения и контекст»).
    • rbobjects.pas — определяет конвертацию в Ruby объектов и классов Pascal. При конвертации классов автоматически создаются ruby-атрибуты, соответствующие published-свойствам.
    • rbclasses.pas, rbdialogs.pas, rbforms.pas — определяют ruby-методы для public-свойств и методов классов, расположенных в соостветствующих стандартных модулях (т.е. Classes, Dialogs и Forms). В настоящий момент RbClasses реализован практически полностью, тогда как два других — по минимуму. Эти модули будут однозначно расширяться в дальнейшем, а также дополняться аналогами для других стандартных модулей FPC и Lazarus.
    • rubyconnection.pas — определяет компонент, позволяющий задать параметры подключения к библиотеке в design-time;
      • rubyconnection_icon.lrs — значок к нему.
    • pprubydsgn.pas — регистрация компонентов в IDE. Пока только TRubyConnection, в дальнейшем вся регистрация будет складываться туда, чтобы не замусоривать модули, используемые и в run-time.
    • ppruby.lpk и ppruby.pas — собственно пакет.

Режимы условной компиляции

  • RUBY_STATIC — устанавливает статический режим. Т.е. используется раннее связывание — функции и переменные Ruby API определяются как external. В этом режиме имеют смысл также следующие определения:

    • RUBY19, RUBY20 и RUBY21, определяющие версию библиотеки. Последний вариант введен, но пока не реализован (скорее всего, работа с версией Ruby 2.1 начнется, когда она появится в репозиториях Gentoo).

    • RUBY_LIB — макрос, определяющий имя библиотеки, автоматически устанавливается в зависимости от версии и операционной системы (согласно определениям DARWIN, UNIX кроме DARWIN и WINDOWS).

      Имя библиотеки для UNIX соостветствует именованию в дистрибутиве Debian и производных, но можно переключиться на именование Gentoo, задав GENTOO соответственно. Если вы используете другой дистрибутив с отличающимся вариантом имени файла библиотеки (или другую unix-based систему, например, FreeBSD), пожалуйста, сообщите. Корректно задать имя библиотеки можно в файле rbdefs.inc.

  • RUBY_DYNAMIC — включается по умолчанию. В этом случае функции API определяются как процедурные переменные и инициализируются вместе с прочими переменными при загрузке. Кроме того, включается система выбора версий.

    Надо заметить, что в разных версиях Ruby некоторые константы API имеют разное значение. Поэтому в динамическом режиме они являются переменными, что может принести некоторые неудобства и замедление.

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

Использование

Создание расширений

Создание расширений для Ruby не было основной задачей данного пакета, поэтому все, что он дает — доступ к API. Как им воспользоваться — см. документацию Ruby. Специфически же для FPC и ppRuby нужно добавить следующее:

  • Обязательна компиляция в статическом варианте, поскольку загрузкой будет управлять сторона Ruby.
  • Модули для конвертации классов и объектов в принципе применимы, но следует учитывать, что время жизни объектов так же должно управляться со стороны Ruby.
  • Крайне важно обеспечить «непересечение» обработки исключений Pascal и Ruby. См. ниже подраздел «Исключения и контекст».

Встраиваемый скриптинг

Настоящий пакет предназначен для того, чтобы максимально упростить именно добавление Ruby как скриптового движка в проекты на Lazarus. Достаточно передать каким-то образом, например, задав глобальную переменную, объект в Ruby, а затем подать текст скрипта на выполнение. Например, перебор компонентов формы можно организовать так:

uses
  ..., RbTools, RbObjects, RbClasses;

...
rb_gv_set('form', Obj2Val(frmMain));
pp_eval(script);

Где в переменной script находится что-то типа:

$form.each do |cmp|
  if cmp.respond_to? :caption
    cmp.caption = cmp.name
  end
end

Подключив же еще и модуль RbForms получим «общую точку доступа» — Pascal::Forms.application. Тогда перебор форм приложения может выглядеть так:

# метод application объявлен как module_function,
# чтобы не писать каждый раз имя модуля, подмешаем
# его в глобальный объект
extend Pascal::Forms

application.each do |form|
  form.caption = "Form: #{form.name}"
end

Дополнительно возможен перехват вывода, через установку глобальной переменной $stdout (и, возможно, $stderr) в специальное значение, получаемое конвертацией интерфейса IOutput, определенного в RbObjects. Этот интерфейс, например, реализует TRubyConnection, перенаправляя вывод в выбранный компонент. Подменив таким образом стандартный вывод, можно спокойно использовать в скриптах привычные puts, p и так далее. Результат можно видеть в демо.

Исключения и контекст

Важный момент. Ruby и Free Pascal используют схожие механизмы обработки исключений, но, естественно, полностью независимые. Когда в блоке try...end возникает ruby-исключение, оно свободно пролетает его границу и стек исключений оказвается несбалансированным. То же самое происходит при возникновении pascal-исключения в блоке обработки Ruby. Несбалансированный стек исключений, в свою очередь, с большой вероятностью приводит к ошибке сегментации — иногда сразу, а при возникновении следующего исключения — почти всегда.

Соответственно, необходимо постоянно понимать, в каком контексте мы находимся, и разделять эти контексты. Т.е. если в определяемом нами ruby-методе потенциально может возникнуть pascal-исключение, опасное место следует обернуть в try...except...end и в блоке except сформировать уже ruby-исключение.

Кроме явных блоков обработки исключений, Free Pascal организует также неявные, в которых производится финализация локальных динамических переменных. Поскольку неявные блоки по простому развести не получится, в rbopts.inc они отключены — за это отвечает директива {$implicitexceptions off}. К сожалению, это решение не идеально — финализацию, в т.ч. опять же неявных, переменных нужно теперь производить вручную и явно... Полный аудит кода на этот предмет пока не проводился, но будет проведен в ближайших версиях.

Справочная информация

Модуль Ruby

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

Базовые типы

  • VALUE — представляет любое значение в Ruby. В оригинальном API это беззнаковое целое, соответствующее по размерности указателю. Однако в ruby.pas вместо простого PtrUInt использован он же, «завернутый» в запись. Это сделано для того, чтобы уменьшить вероятность ошибок от неявного приведения типов (записи неявному приведению не подвержены). Есть и недостаток такого подхода — невозможность объявить «истинную», т.е. не являющуюся инициализированной переменной, константу. Но, см. выше, в динамическом режиме это попросту не имеет значения.

  • ID — это то значение, на которое по сути ссылается тип Symbol. В оригинале — так же как и VALUE — соответствует PtrUInt. Аналогично завернуто в запись, таким образом нельзя случайно использовать ID там где требуется VALUE типа Symbol, и наоборот.

К типу VALUE относятся следующие специальные значения:

  • Qfalse,
  • Qtrue,
  • Qnil,
  • Qundef.

Впрочем, последнее можно забыть сразу — в норме оно появиться не может, нормально обозначение отсутствия значения — это Qnil.

Для удобства использования на обоих типах определен оператор =.

operator = (x, y : VALUE) : Boolean; inline;
operator = (x, y : ID) : Boolean; inline;

Загрузка и инициализация библиотеки

В случае динамической загрузки в первую очередь нужно задать пути поиска библиотеки — «зарегистрировать версию».

procedure pp_reg_ruby_version (const version : array of cint;
                               const info : RbInfo;
                               const libs : array of UnicodeString);
  • Здесь version — открытый массив целых (количеством от нуля до четырех), например — [1,9,3] или [2,0,0] — четвертая цифра будет означать «patchlevel», который не влияет на совместимость, поэтому ее использование малоосмысленно, хотя и возможно.

  • info — запись с информацией об особенностях версии библиотеки. В настоящее время такая особенность одна — в Ruby 2.0 введено непосредственное представление чисел с плавающей точкой на 64-битных системах, в связи с чем поменялись некоторые базовые константы. В дальнейшем не исключено расширение этого типа данных.

    type
      RbInfo = record
        check_flonum : Boolean;
      end;
    
    const
      Rb19 : RbInfo = ( check_flonum : False );
      Rb20 : RbInfo = ( check_flonum : True  );
  • libs — список строк — имен библиотеки, по которым программа будет пытаться ее загрузить.

По умолчанию модуль сам регистрирует две версии — 1.9 и 2.0 соответственно с известными автору именами библиотеки, аналогично тем, которые устанавливаются для статической загрузки. Так что если у вас (и у целевого пользователя) стандартная установка Ruby на Mac OS X, Windows, Debian-based Linux или Gentoo, регистрировать библиотеку вручную не требуется.

Собственно загрузка производится совместно с инициализацией процедурой pp_ruby_init()

procedure pp_ruby_init (const version : array of cint);
procedure pp_ruby_init;

При этом происходит просмотр зарегистрированных версий с последней к первой, проверяется соответствие версий — неуказанные цифры соответствуют любым, т.е. [1,9] ~ [1,9,3] и [1,9] ~ [1,9,1], [2] ~ [2,0,0] и [2] ~ [2,1,42] и т.д. Вызов без параметров аналогичен pp_ruby_init([]), т.е. подходящей считается любая версия.

Если версия подходит, процедура пытается загрузить библиотеку, пробуя имена с первого до последнего (если ни по одному имени загрузка не удалась, продолжается цикл по версиям).

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

После того, как работа с интерпретатором Ruby завершена, следует вызвать pp_ruby_done().

procedure pp_ruby_done (check_errinfo : Boolean = True);

Необязательный параметр указывает, нужно ли проверять код ошибки и генерировать исключение в случае чего. Если повторная загрузка библиотеки не планируется, можно выставить его в false.

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

Как явствует из вышесказанного, загрузка и выгрузка могут производитья неоднократно, при этом в промежутке между ними интерпретатор Ruby как бы не существует. Может возникнуть потребность выполнять какие-то действия, скажем, при каждой загрузке — определить какие-то методы модулей, константы и т.д. Для этого предусмотрены процедуры-хуки.

type
  pp_init_hook = procedure;
  pp_done_hook = procedure;

procedure pp_reg_init_hook (hook : pp_init_hook);
procedure pp_reg_done_hook (hook : pp_done_hook);

При этом init-хуки будут выполнены сразу после инициализации интерпретатора, а done-хуки непосредственно перед финализацией, т.е. и то и другое производится при рабочем состоянии интерпретатора. Может быть существенным, что вызов этих хуков происходит в pascal-контексте, таким образом pascal-исключения в них допустимы, а ruby-исключения нет — следует использовать rb_protect().

Ну, и наконец, текущее состояние библиотеки можно определить функцией pp_ruby_active().

function pp_ruby_active : Boolean; inline;

Примечание: В версиях Ruby 1.9 и 2.0 для преобразования вещественного числа в VALUE используются разные функции API — rb_float_new() в 1.9 и rb_float_new_in_heap() в 2.0. У них полностью одинаковые прототипы и сущность выполняемого действия...

Для кроссверсионной работы в модуле Ruby определена процедурная переменная:

var
  p_rb_float_new : function (x : Double) : VALUE; cdecl;

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

RbTools и RbObjects

Эти два модуля — ядро пакета, ради которого все и затевалось.

Модуль RbTools

Контекст

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

type
  TWrapper = procedure is nested;

procedure pp_protect (wrapper : TWrapper);
procedure pp_try (wrapper : TWrapper);

Первая заключает переданный ей код в rb_protect() и генерирует pascal-исключение в случае чего. Вторая, соотственно, в try...except...end и генерирует ruby-исключение посредством rb_raise(). Реализация через вложенно-процедурный тип параметра позволяет не заморачиваться с разными прототипами реальных функций.

Преобразования

Далее — набор стандартных преобразований для использования в pascal-контексте:

function Int2Val  (const x : PtrInt       ) : VALUE;
function UInt2Val (const x : PtrUInt      ) : VALUE;
function Bool2Val (const x : Boolean      ) : VALUE;
function Dbl2Val  (const x : Double       ) : VALUE;
function Str2Val  (const x : UTF8String   ) : VALUE;
function UStr2Val (const x : UnicodeString) : VALUE;
function Str2Sym  (const x : UTF8String   ) : VALUE;
function UStr2Sym (const x : UnicodeString) : VALUE;
function Val2Int  (v : VALUE) : PtrInt;
function Val2UInt (v : VALUE) : PtrUInt;
function Val2Bool (v : VALUE) : Boolean;
function Val2Dbl  (v : VALUE) : Double;
function Val2Str  (v : VALUE) : UTF8String;
function Val2UStr (v : VALUE) : UnicodeString;
function Sym2Str  (v : VALUE) : UTF8String;
function Sym2UStr (v : VALUE) : UnicodeString;

function Str2ID (const x : UTF8String) : ID;
function UStr2ID (const x : UnicodeString) : ID;
function ID2Str (id : ID) : UTF8String;
function ID2UStr (id : ID) : UnicodeString;

... и аналогично для Ruby-контекста:

function IV (x : PtrInt ) : VALUE; inline;
function UV (x : PtrUInt) : VALUE; inline;
function BV (x : Boolean) : VALUE; inline;
function DV (x : Double ) : VALUE; inline;
function SV (x : PChar)   : VALUE; inline;
function SY (x : PChar)   : VALUE; inline;
function VI (x : VALUE) : PtrInt;  inline;
function VU (x : VALUE) : PtrUInt; inline;
function VB (x : VALUE) : Boolean; inline;
function VD (x : VALUE) : Double;  inline;
function VP (x : VALUE) : PChar;   inline;
function YP (x : VALUE) : PChar;   inline;

function SD (x : PChar) : ID; inline;
function DP (x : ID) : PChar; inline;

С учетом inline это практически набор синонимов для функций API.

Важно: Строковые функции (и PChar'овые) всегда подразумевают кодировку UTF-8. Для работы с другими кодировками придется обращаться непосредственно к функциям API из модуля Ruby.

Хелперы

Ряд полезных оберток над API для pascal-контекста:

function pp_inspect (v : VALUE) : UTF8String;
function pp_eval (const code : UTF8String) : VALUE;
function pp_call (obj : VALUE; const method : UTF8String;
                          const args : array of VALUE) : VALUE;
function pp_send (obj : VALUE; const method : UTF8String;
                          const args : array of VALUE) : VALUE;
function pp_to_s (obj : VALUE) : UTF8String;

pp_call() может вызывать только public-методы, тогда как pp_send() — любые.

И три коротких вспомогательных для ruby-контекста:

function C (obj : VALUE; method : PChar; const args : array of VALUE) : VALUE;
function S (v : VALUE) : PChar; inline;
function XS (v : VALUE) : PChar; inline;

Тут, пожалуй, не обойтись без пояснений:

  • C() — это аналог pp_send(), хотя сокращение от «call», конечно.
  • S() — это аналог pp_inspect(), а
  • XS()pp_to_s(). Понять это нельзя, запомнить сложно, но всегда можно заглянуть в реализацию, на то и открытость.
Хелперы для определений

Для удобства определения атрибутов и доступа по индексу (с дополнительным плюсом в виде контроля типов):

type
  pp_getter = function (obj : VALUE) : VALUE; cdecl;
  pp_setter = function (obj, value : VALUE) : VALUE; cdecl;
procedure pp_define_attr (klass : VALUE; name : PChar;
                          getter : pp_getter; setter : pp_setter);

type
  pp_b_getter = function (obj, idx : VALUE) : VALUE; cdecl;
  pp_b_setter = function (obj, idx, value : VALUE) : VALUE; cdecl;
procedure pp_define_braces (klass : VALUE;
                          getter : pp_b_getter; setter : pp_b_setter);
Хуки и данные

Модуль RbTools добавляет init-хук, который определяет в Ruby:

  • модуль Pascal — пространство имен для дальнейшей работы, чтобы гарантированно не смешивать идентификаторы;
  • и класс исключений Pascal::Error.

Значения этих модуля и класса помещаются в переменные pp_mPascal и pp_ePascalError соответственно.

Кроме того, хук инициализации устанавливает интерпретатору UTF-8 как внешнюю и внутреннюю кодировку по умолчанию.

Модуль RbObjects

Преобразования

Для преобразования объектов и классов Object Pascal в Ruby и получения их потом обратно служат следующие функции (для pascal-контекста):

function Obj2Val (const x : TObject) : VALUE;
function New2Val (const x : TObject) : VALUE;
function Cls2Val (const x : TClass ) : VALUE;
function Val2Obj (v : VALUE; nn : Boolean = False) : TObject;
function Val2Cls (v : VALUE) : TClass;

New2Val() отличается от Obj2Val() тем, что задает процедуру-финализатор, когда сборщик мусора Ruby доберется до соответствующего значения, будет освобожден и pascal-объект. Т.е. время жизни объекта управляется со стороны Ruby, тогда как объектами, преобразованных посредством Obj2Val(), управляет сторона Pascal.

Класс при преобразовании объекта преобразуется автоматически, иерархия классов при этом сохраняется с корнем Pascal::System::TObject, который в свою очередь унаследован от Data. Все классы в Ruby попадают как Pascal::<unit>::<class>, что опять же страхует от возможного конфликта имен — в Pascal элементы разных модулей вполне могут иметь совпадающие имена.

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

При этом у Pascal::System::TObject «разопределен» метод new, т.е. создать новый объект на стороне Ruby невозможно. Для тех классов, которым это разрешено, следует определить его заново с использованием NV() (см. ниже).

Параметр nn у функции Val2Obj() означает «not null», если его установить в true, то nil из Ruby будет создавать исключение, в противном случае (по умолчанию), он преобразуется в паскалевский nil.

Для использования в ruby-контексте предоставлены аналоги:

function OV (x : TObject) : VALUE;
function NV (x : TObject) : VALUE;
function CV (x : TClass ) : VALUE;
function VO (x : VALUE) : TObject;
function VJ (x : VALUE) : TObject;
function VC (x : VALUE) : TClass;

Здесь NV() — это, как нетрудно догадаться, аналог New2Val(), а VJ(), как догадаться, вероятно, труднее, — Val2Obj() с параметром nn, установленным в true, т.е. выдает гарантированно валидный TObject.

Для преобразованных объектов сразу определена операция == — через вызов метода Equals(). Это важно, поскольку значения VALUE для одного и того же объекта, преобразованного два раза, совпадать не будут, а попытки использовать кэширование соответствий не увенчались успехом из за повышенной агрессивности сборщика мусора в Ruby.

published-свойства

При преобразовании классов методы для работы с published-свойствами создаются автоматически. Поддерживаются следующие типы свойств:

  • Целые и вещественные числа,
  • Перечисления и множества (конвертируются в тип Symbol и Set<Symbol>),
  • Строки (однобайтные трактуются как UTF-8),
  • Символы (конвертируются как строки),
  • Булевы значения,
  • Объекты.

Имена свойств при этом приводятся к нижнему регистру (целиком, а не только первые буквы).

Перехват вывода

Для организации перехвата вывода создан потомок ruby-класса IO — Pascal::IO, экземпляров которого так просто создать не получится, только из pascal-кода фунцией Out2Val() (или WV()). Собственно, объявления:

var
  pp_cPascalIO : VALUE;

type
  IOutput = interface(IUnknown)
  ['{83A91283-5CD0-4408-BC2A-BE02274D8902}']
    function Write (val : VALUE) : VALUE;
  end;

function Out2Val (const x : IOutput) : VALUE;
function Val2Out (v : VALUE) : IOutput;

function WV (x : Pointer) : VALUE;
function VW (x : VALUE) : Pointer;

Таким образом, достаточно реализовать IOutput и можно устанавливать свой объект вывода. Класс IO устроен так, что все методы типа printf, puts и p будут работать через этот Write(). При его реализации автоматическое завершение строки добавлять не следует, т.е. это именно Write(), а не WriteLn().

Хуки

Для ручного определения методов объектов (классов) и функций модулей следует использовать следующие хуки:

type
  pp_unit_hook = procedure (v : VALUE);
  pp_class_hook = procedure (v : VALUE);

procedure pp_reg_unit_hook (const name : AnsiString; hook : pp_unit_hook);
procedure pp_reg_class_hook (cls : TClass; hook : pp_class_hook);

В хуки будет передан соответствующий класс или модуль Ruby, и выполняться они будут в ruby-контексте.

Модули RbClasses, RbDialogs, RbForms...

В этих модулях задаются Ruby методы классам из Classes, Dialogs и Forms соответственно. Более подробная информация воспоследует...

TODO...

Компонент TRubyConnection

TODO...

You can’t perform that action at this time.