Содержание


Guile - универсальный инструмент программирования

Часть 4. Взаимодействие с языком C (окончание)

Comments

Серия контента:

Этот контент является частью # из серии # статей: Guile - универсальный инструмент программирования

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Guile - универсальный инструмент программирования

Следите за выходом новых статей этой серии.

Надеюсь, что примеры из предыдущей статьи убедили читателя, насколько легко и просто вызывать Scheme-код из C-кода и наоборот. Программист полностью контролирует ситуацию: как и когда вызывать интерпретатор или отдельные процедуры, в каком формате передавать параметры и т.п. Кроме того, в приложения можно добавлять новые функции, типы данных, управляющие структуры и даже новые синтаксические конструкции в Guile, создавая особый, специализированный язык, ориентированный на конкретный класс задач, но при этом основанный на прочном фундаменте надёжной реализации Scheme.

1. Определение новых типов данных - малых объектов (smobs)

Термин "smob" придумал Обри Джаффер (Aubrey Jaffer) (автор ядра SCM - основы интерпретатора Guile) для обозначения механизма "малых объектов" (small objects), то есть, механизма добавления новых типов данных в систему Guile. Само название - малые объекты - говорит об относительно небольшом размере smob'ов: они могут содержать только лишь один указатель на некоторый блок памяти и ещё 16 дополнительных битов.

1.1. Что необходимо для создания smob

Для того, чтобы определить новый smob-тип, программист должен предоставить Guile набор характеристик этого типа - как должны выглядеть значения этого типа при выводе, каким образом данные этого типа подвергаются процедуре "сборки мусора" и т.п. В ответ на это Guile формирует новый уникальный тэг для создаваемого типа. После завершения всех формальных процедур программист может воспользоваться функцией scm_c_define_gsubr() для определения набора C-функций, доступных в Guile-коде и предназначенных для создания и обработки объектов этого вновь созданного типа.

1.2. Описание нового типа

Полное определение нового типа подразумевает, что программист непременно должен написать четыре функции, управляющие экземплярами этого типа: mark, free, print и equalp.

Функцию mark Guile будет применять к каждому экземпляру этого нового типа для количественного учёта, необходимого для корректного выполнения процедуры "сборки мусора". В обязанности этой функции входит оповещение "сборщика мусора" о любом другом SCM-значении, которое содержит данный объект. Тем не менее, по умолчанию функция mark не делает ничего, и программист сам должен позаботиться обо всех необходимых оповещениях.

Guile применяет функцию free к каждому удаляемому экземпляру данного нового типа, следовательно, функция должна освобождать все ресурсы, захваченные этим объектом. Можно сравнить free с Java-методом finalization - он тоже вызывается в неопределённое время (когда начинается очередной сеанс "сборки мусора") после того, как объект был удалён. По умолчанию функция free освобождает память, занятую smob-данными (если структура, переданная в scm_make_smob_type(), имеет ненулевой размер) с помощью вызова scm_gc_free().

Функция print используется в Guile для вывода значения любого экземпляра нового определяемого типа точно так же, как при вызовах display или write. По умолчанию вывод print имеет следующий формат #<NAME ADDRESS>, где NAME - это первый аргумент, переданный в функцию scm_make_smob_type().

По имени функции equalp понятно, что используется она для сравнения значений двух экземпляров этого нового smob-типа. Вызывается equalp в тех случаях, когда в Scheme-коде встречается функция equal?. Если в соответствии с форматом сравнения equal? оба экземпляра должны квалифицироваться, как "равные" (имеющие одинаковое содержимое), то функция equalp должна возвращать SCM_BOOL_T, а в противном случае - SCM_BOOL_F. Если equalp возвращает NULL, то функция equal? будет считать, что любые два экземпляра данного типа никогда не будут "равными" (в смысле содержимого), за исключением того случая, когда они являются ссылками на один и тот же объект (это определяется функцией eq?).

1.3. Регистрация нового smob-типа

После того, как новый тип всесторонне описан, можно переходить к его регистрации. Для этого вызывается функция scm_make_smob_type(), возвращающая значение типа scm_t_bits, которое служит идентификатором нашего нового smob-типа.

Кстати, описанные выше четыре функции тоже нужно зарегистрировать с помощью вызовов scm_set_smob_mark(), scm_set_smob_free(), scm_set_smob_print и scm_set_smob_equalp() соответственно. Для каждого создаваемого типа эти функции должны вызываться не более одного раза, а в исходном коде они должны размещаться сразу же после вызова функции регистрации scm_make_smob_type().

В Guile-системе может быть создано до 256 различных smob-типов. Впрочем, не следует увлекаться чрезмерным "размножением" smob'ов - лозунг "каждой структуре данных в C-коде - отдельный smob-тип" далеко не всегда уместен. Зачастую лучше зарегистрировать один обобщающий smob-тип, а затем реализовать второй уровень - подтип, управляемый верхним уровнем. Этот подтип второго уровня, например, может использовать упомянутые выше 16 дополительных битов для расширения общего типа.

1.4. Пример создания нового smob-типа

В приведённом ниже примере показано, как может быть объявлен, описан и зарегистрирован новый smob-тип, представляющий 8-битовые данные изображения в оттенках серого цвета.

#include <libguile.h>

struct image
{
  int width;
  int height;
  char *pixels;
  SCM name;    /* имя изображения */
  SCM update_func; /* процедура, вызываемая при изменении изображения,
                      например, при обновлении экрана. Возвращает 
                      SCM_BOOL_F, если обновление не требуется  */
};

static scm_t_bits image_tag;

void init_image_type( void )
{
  image_tag = scm_make_smob_type( "image", sizeof(struct image) );
  scm_set_smob_mark( image_tag, mark_image );
  scm_set_smob_free( image_tag, free_image );
  scm_set_smob_print( image_tag, print_image );
  /* функцию equalp можно не определять - сравнивать внутреннее содержимое
     изображений требуется достаточно редко. Хотя... кто знает... */
}

2. Практическое использование малых объектов

2.1. Создание экземпляров

Обычно сам по себе smob непосредственно содержит одно слово данных. Это либо указатель на блок памяти, в котором хранятся собственно данные, либо сами данные, если им хватает места в этом слове. Если выражаться точнее, одного слова достаточно для SCM-значения, или для указателя на void, или для целого числа, соответствующего типу size_t или ssize_t.

Кроме того, вы можете создать smob-типы, содержащие два или три непосредственно адресуемых слова, если этого достаточно для хранения ваших данных. Использование таких smob-объектов "увеличенного размера" более эффективно, чем создание обычного smob'а с указателем на крошечный блок памяти. В Guile имеются функции управления памятью, весьма полезные при эксплуатации smob-объектов.

Для извлечения содержимого этого самого слова памяти smob'а можно воспользоваться макрокомандой SCM_SMOB_DATA, а изменить содержимое позволяет макро SCM_SET_SMOB_DATA. Доступ к 16 дополнительным битам обеспечивают SCM_SMOB_FLAGS и SCM_SET_SMOB_FLAGS. Макро SCM_SMOB_DATA и SCM_SET_SMOB_DATA интерпретируют значение smob-слова, как unsigned int - вполне достаточно для хранения указателя на void, что позволяет манипулировать указателями на экземпляры объектов.

Если нужно сохранить SCM-значение прямо в слово памяти smob'а, то для этого предназначены макро SCM_SMOB_OBJECT и SCM_SET_SMOB_OBJECT.

В целом при создании экземпляра smob-типа рекоменуется придерживаться последовательности шагов, описанных ниже:

  1. Выделить блок памяти, необходимый для хранения данных, с помощью функции scm_gc_malloc().
  2. Корректно инициализировать этот блок памяти, избегая при этом вызовов функций, которые могут привести к нелокальному выходу из программы. То есть, указатель лучше инициализировать NULL-значением, а если предполагается хранение SCM-значения, то для инициализации используйте SCM_BOOL_F.
  3. Создать smob, используя SCM_NEWSMOB, с передачей ему указателя на корректно инициализированный блок памяти.
  4. После этого можно начинать работу с блоком памяти, связанным с smob, "в штатном режиме".

Описанная процедура гарантирует, что smob будет находиться в корректном состоянии сразу после его создания, а также то, что все выделенные ему ресурсы размещены надлежащим образом и могут быть освобождены без "утечек памяти".

2.2. Снова пример

Вернёмся к примеру, приведённому выше. Его промежуточным результатом является тот факт, что глобальной статической переменной image_tag присвоен тэг, который был возращён функцией scm_make_smob_type(). Теперь можно создать smob-объект, слово памяти которого будет содержать указатель на конкретный экземпляр структуры image.

SCM make_image( SCM scm_name, SCM scm_width, SCM scm_height )
{
  SCM img_smob;
  struct image *img_data;
  int width = scm_to_int( scm_width );
  int height = scm_to_int( scm_height );

  /* a) Выделение блока памяти */
  img_data = (struct image *)scm_gc_malloc( sizeof(struct image), "img_data" );
  
  /* b) Инициализация выделенного блока памяти */
  img_data->width = width;
  img_data->height = height;
  img_data->pixels = NULL;
  img_data->name = SCM_BOOL_F;
  img_data->update_func = SCM_BOOL_F;

  /* c) Создание smob */
  SCM_NEWSMOB( img_smob, image_tag, img_data );

  /* d) Завершение инициализации связанного блока памяти */
  img_data->name = scm_name;
  img_data->pixels = scm_gc_malloc( (width * height), "image pixels" );

  /* новый smob-тип готов к употреблению */
  return img_smob;
}

Для преобразования SCM-значений ширины и высоты в int вызывается функция scm_to_int(), внутри которой из-за ошибок может произойти нелокальный выход из программы. Но в текущий момент это не так страшно, поскольку ресурсы пока ещё не распределялись.

На шаге a) при выделении блока памяти может возникнуть ошибка, но здесь она не обрабатывается (чтобы упростить код).

На шаге b) возможность нелокального выхода исключена. Структура img_data приводится в корректное состояние.

Шаг с) также исключает нелокальный выход, только теперь ответственность за это на себя принимает Guile. После завершения этого шага img_smob содержит "правильный" smob, который корректно инициализирован и защищён и в свою очередь способен защитить свои SCM-значения, хранящиеся в структуре img_data. Тем не менее, записывать SCM-значения в поля структуры необходимо после окончательного завершения создания smob'а, поскольку процедура "сборки мусора" может быть вызвана несколько раньше, и SCM-значения, пока ещё "невидимые" для Guile, будут уничтожены. Именно поэтому присваивание SCM-значений выполняется на отдельном шаге d).

2.3. Контроль типов

При работе с экземплярами smob-типов функции обработки должны проверять соответствие передаваемого им SCM-значения требуемому smob-типу, прежде чем разрешить доступ к его внутренним данным. Обычно это делается с помощью функции scm_assert_smob_type().

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

SCM clear_image( SCM image_smob )
{
  int img_area;
  struct image *img_data;

  scm_assert_smob_type( image_tag, image_smob );

  img_data = (struct image *)SCM_SMOB_DATA( image_smob );
  img_area = img_data->width * img_data->height;
  memset( img_data->pixels, 0, img_area );
  /* Вызов внутренней функции обновления изображения */
  if( scm_is_true( img_data->update_func ) )
    scm_call_0( img_data->update_func );

  scm_remember_upto_here_1( image_smob );

  return SCM_UNSPECIFIED;
}

В приведённом примере функция scm_assert_smob_type( image_tag, image_smob ) проверяет, является ли SCM-значение image_smob smob-типом, идентифицируемым тэгом image_tag. Если тип совпал, то функция ничего не делает, позволяя продолжить выполнение программы. Если image_smob является объектом другого типа, то генерируется состояние ошибки.

Функция scm_remember_upto_here_1( image_smob ) создаёт ссылку на заданный объект image_smob, чтобы гарантировать сохранение его в стеке или в регистре и не допустить его уничтожение процедурой "сборки мусора". Следует отметить, что эта функция применима только к обычным локальным C-переменным. Глобальные и/или статические переменные, а также выделенные блоки памяти не могут быть защищены с помощью этого механизма.

Поскольку все данные о самом изображении (img_data->pixels) были обнулены (изображение "стёрто"), функция возвращает константу SCM_UNSPECIFIED, то есть, сообщает о том, что данный объект не содержит изображения, соответствующего требуемой спецификации данных.

2.4. "Сборка мусора" для smob-объекта

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

Когда "сборщик мусора" встречает smob-объект, он устанавливает mark-бит и по тэгу этого smob'а ищет соответствующую функцию mark, вызывает её и передаёт данный smob, как единственный аргумент.

Функция mark отвечает за установку mark-бита во всех Scheme-объектах, на которые ссылается данный smob. Если этого не сделать, то данные, необходимые для работы smob-объекта, будут удалены "сборщиком мусора". Чтобы установить mark-бит заданного Scheme-объекта, необходимо вызвать функцию scm_gc_mark().

Для нашего примера mark-функция может выглядеть приблизительно так:

SCM mark_image( SCM image_smob )
{
  struct image *img_data = (struct image *)SCM_SMOB_DATA( image_smob );
  /* установка mark-битов всех SCM-объектов, хранящихся в этой структуре */
  scm_gc_mark( img_data->name );
  scm_gc_mark( img_data->update_func );
  return SCM_BOOL_F;
}

Ну, и для завершения картины требуется free-функция, которая освобождает все ресурсы, используемые нашим smob-объектом.

size_t free_image( SCM image_smob )
{
  struct image *img_data = (struct image *)SCM_SMOB_DATA( image_smob );
  scm_gc_free( img_data->pixels, (img_data->width * img_data->height), "image pixels" );
  scm_gc_free( img_data, sizeof(struct image), "img_data" );
  return 0;
}

Здесь следует отметить, что по историческим причинам в качестве типа возвращаемого значения принят size_t, беззнаковое целое число, хотя free-функция всегда должна возвращать нуль.

3. Заключение

Механизм малых объектов smobs позволяет существенно расширить возможности связки C-Guile. Программист действительно может создать "расширенную версию" Guile с новыми типами данных и даже с новыми синтаксическими конструкциями.

В первой статье, открывающей цикл, был представлен общий обзор Guile. Во второй статье рассматривалось практическое применение интерпретатора. Третья и четвёртая статьи посвящены взаимодействию Guile с языком программирования C. В пятой, заключительной статье цикла будут описаны дополнительные средства Guile.


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=680628
ArticleTitle=Guile - универсальный инструмент программирования: Часть 4. Взаимодействие с языком C (окончание)
publish-date=06142011