Содержание


Guile — универсальный инструмент программирования. Часть 3. Взаимодействие с языком C (начало)

Comments

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

Этот контент является частью # из серии # статей:

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

Этот контент является частью серии:

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

Пришло время вспомнить о том, что Guile заявлен не только, как интерактивная оболочка-интерпретатор, но и как встраиваемый язык расширений. Поэтому в данной статье мы рассмотрим, каким образом можно заставить совместно работать Guile и компилируемый язык программирования — на примере C.

1. Общие положения использования Guile в C-программах

Нужно ли вообще привязывать к нормальным полноценным компилируемым языкам "костыли и подпорки" в виде встраиваемых языков расширений, представителем которых является Guile? При программировании на C весьма часто в первую очередь прорабатываются структуры данных и интерфейсы к ним, затем пишется код обработки. Тем не менее, даже самым тщательным образом составленные структуры данных могут потребовать внесения изменений в них в процессе практической работы. Вообразите, например, приложение, в котором время отсчитывается в секундах, начиная с определённой заданной отсечки. При предварительном проектировании кажется, что ничего лучшего, чем unsigned long для хранения данных времени и придумать невозможно. А в ходе разработки выясняется, что необходимо учитывать время с точностью до десятых секунды, то есть изменить тип данных на double или выдумать что-то ещё, а кроме того, скорректировать все интерфейсы, все вызовы функций и все инструкции вывода, которые работают с данными времени.

В этой ситуации Guile может оказаться полезным. Во-первых, поскольку Guile (точнее, Scheme) является языком со слабой типизацией, многие из его математических процедур и процедур ввода/вывода менее чувствительны к изменениям структуры данных, передаваемых в них в качестве параметров. Во-вторых, составные типы данных Guile, такие как список (list), могут иметь переменную длину и содержать элементы различных типов. Функции, работающие со списками, могут быть написаны без жёсткой зависимости от конкретных типов данных, что позволяет отложить окончательное решение по структурам данных "на потом" с возможностью уточнений в процессе разработки.

1.1. С чего начать

Разумеется, с инициализации среды Guile в контексте приложения. Это можно сделать несколькими способами, которые мы рассмотрим подробно.

В общем случае каждый поток (thread) приложения, которому нужно воспользоваться функцией из Guile API-интерфейса, должен переключиться в guile-режим либо с помощью вызова scm_with_guile(), либо с помощью вызова scm_init_guile(). Глобальное состояние среды Guile инициализируется автоматически в тот момент, когда самый первый поток приложения переключается в guile-режим.

Функция Guile API-интерфейса

void *scm_with_guile( void *(*function)(void *), void *data )

вызывает функцию function, передаёт в неё данные data и возвращает то значение, которое вернула function. Во время выполнении функции function текущий поток находится в guile-режиме, следовательно, может использовать Guile API-интерфейс. Если это самый первый поток, который входит в guile-режим, то глобальное состояние среды Guile инициализируется перед вызовом функции function. Сама функция function вызывается через scm_with_continuation_barrier(), гарантирующую, что возврат из scm_with_guile() будет происходить один и только один раз.

После возврата из scm_with_guile() поток выходит из guile-режима (за исключением тех случаев, когда функция scm_with_guile() вызывалась уже из guile-режима). Следовательно, только внутри функции function SCM-переменные могут быть сохранены в стеке и защищенны от процедуры "сборки мусора".

Поток может выйти из guile-режима, вызвав scm_without_guile(), — временнно, то есть, с возможностью возврата, или постоянно.

Другой подход к инициализации Guile — вызов функции

void scm_init_guile(),

при котором описанные выше ограничения не накладываются. Данная функция устанавливает режим, в котором весь код в текущем потоке выполняется так, как если бы он выполнялся "внутри" вызова scm_with_guile(). Таким образом, для всех функций, вызываемых текущим потоком, обеспечивается защита SCM-значений в их фреймах стека от процедуры "сборки мусора" (разумеется, за исключением того случая, когда данный поток явно вышел из guile-режима).

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

1.2. От теории — к практике

А теперь — пример. Для упрощения в примерах будут рассматриваться только однопоточные приложения.

Допустим, у нас в текущем каталоге есть файл environ.scm, в котором содержится исходный код процедуры set-editor, которая проверяет и при необходимости устанавливает переменную среды EDITOR:

(define myeditor (getenv "EDITOR"))
(define set-editor
  (lambda ()
    (if (equal? myeditor #f)
      (setenv "EDITOR" "vim"))
    (set! myeditor (getenv "EDITOR"))
    (display (string-append "EDITOR=" myeditor))
    (newline)))

Сначала с переменной myeditor связывается значение, возвращаемое функцией getenv. Если эта функция обнаружит переменную среды EDITOR, то вернёт её значение в виде строки. В противном случае возвращается значение #f (ложь).

Далее мы определяем свою функцию set-editor и сразу же проверяем значение переменной myeditor. Если переменная содержит не строку с именем редактора, а логическое значение #f, то необходимо определить переменную среды EDITOR с заданным значением. Это делается с помощью функции setenv (те, кто "не любит vim", могут задать значение "emacs" или имя другого предпочитаемого редактора). После этого значение переменной myeditor корректируется (вообще говоря, эта инструкция является избыточной и нужна только в случае изменения переменной среды, но в целях упрощения кода я не стал вводить дополнительные проверки). Завершает процедуру контрольный вывод значения переменной среды EDITOR.

Теперь посмотрим, как можно воспользоваться этой процедурой в коде C-программы. Для этого напишем и сохраним в файле env_info.c следующий исходный код:

#include <libguile.h>

int main( int argc, char **argv )
{
  SCM func_symbol;
  SCM func;

  scm_init_guile();
  scm_c_primitive_load( "environ.scm" );
  func_symbol = scm_c_lookup( "set-editor" );
  func = scm_variable_ref( func_symbol );
  scm_call_0( func );
  exit(0);
}

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

Так как Guile (Scheme) является языком с динамической типизацией, во время компиляции невозможно точно определить типы переменных и выражений. Для решения этой проблемы введено специальное универсальное представление всех типов Scheme-значений — тип SCM. При этом надо запомнить важное правило: никаких операций непосредственно с SCM-значениями в C-коде; эти значения предназначены только для передачи в Guile-функции. В нашей программе объявлены две SCM-переменные: func_symbol и func.

Инициализация среды Guile выполняется с помощью функции scm_init_guile(), описанной выше. Функция scm_c_primitive_load() загружает файл с заданным именем ("environ.scm") и интерпретирует его содержимое на самом верхнем уровне среды. Поиск по путям загрузки не выполняется; имя файла должно быть либо полным путевым именем файла, либо путевым именем, указанным относительно текущего каталога.

Следует принять к сведению, что многие функции, связанные с передачей строк в Guile-функции, реализованы в двух вариантах — в данном случае у scm_c_primitive_load() есть аналог scm_primitive_load(). Работают оба варианта одинаково, а разница лишь в том, что для scm_c_primitive_load() имя загружаемого файла задаётся в виде C-строки (то есть, с завершающим NULL-символом); часто такой вариант удобнее и проще. Для варианта "без _c_" требуется передача SCM-значения.

Функция scm_c_lookup() возвращает SCM-переменную, связанную с символом, заданным передаваемым фактическим параметром (здесь — "set-editor"), в текущем загруженном модуле. То есть, переменная func-symbol получает своего рода "указатель" на Guile-процедуру с именем set-editor (напомню, что в Guile функция или процедура — это тоже тип данных).

С помощью scm_variable_ref() выполняется "разыменование" (dereference) символа func_symbol и возвращается его значение, которое связывается с переменной func. Таким образом, func фактически получает код процедуры set-editor.

И, наконец, происходит вызов нашей Guile-процедуры посредством функции scm_call_0(), где суффикс "_0" означает отсутствие передаваемых параметров. После этого программа завершается.

Теперь мы можем скомпилировать

gcc -o env_info env_info.c -lguile

и выполнить программу, чтобы убедиться в том, что она работает (см.рис.3.1).

Рис.3.1. Результат вызова Guile-процедуры
Рис.3.1. Результат вызова Guile-процедуры
Рис.3.1. Результат вызова Guile-процедуры

1.3. Другие способы инициализации Guile

Альтернативным способом переключения в guile-режим является функция

void scm_boot_guile( int argc, char **argv,
                     void (*main_func)( void *data, int argc, char **argv),
					 void *data )

После входа в guile-режим выполняется вызов функции main_func с передачей ей данных data, количества аргументов argc и указателя на аргументы argv, как показано выше. Главной особенностью scm_boot_guile() является то, что после возврата из main_func автоматически вызывается exit(0), то есть, возврат из scm_boot_guile() никогда не происходит. При необходимости вернуть другой код завершения программы придётся вызывать exit(код) из самой функции main_func. Если выход после возврата из main_func нежелателен, то... используйте scm_with_guile() (см.выше).

Ещё одна функция

void scm_shell( int argc, char **argv )

обеспечивает запуск интерпретатора Guile в интерактивном режиме, обеспечивая взаимодействие с пользователем.

Применение этих функций демонстрирует следующий пример (исходный код в файле call_guile.c):

#include <stdlib.h>
#include <libguile.h>

static SCM exec_path( void )
{
  char *str = getenv( "PATH" );
  if( str == NULL )
    return SCM_BOOL_F;
  else
    return scm_from_locale_string( str );
}

static void call_guile( void *data, int argc, char **argv )
{
  scm_c_define_gsubr( "exec-path", 0, 0, 0, exec_path );
  scm_shell( argc, argv );
}

int main( int argc, char **argv )
{
  scm_boot_guile( argc, argv, call_guile, 0 );
  return 0; /* а эта инструкция не будет выполнена никогда */
}

Конечная цель этой программы — вызов интерпретатора Guile в интерактивном режиме. Кроме того, здесь показана возможность написания пользовательской Guile-функции exec-path на языке C. Эта функция просто получает значение переменной среды PATH и возвращает полученное значение. Разумеется, функция, предназначенная для Guile, должна возвращать SCM-значение. В данном случае это либо SCM_BOOL_F (#f), если переменная PATH не определена, либо результат преобразования C-строки str в Scheme-строку с помощью функции scm_from_locale_string(), которая возвращает содержимое str при её интерпретации в кодировке текущей системной локали.

В функции call_guile выполняется регистрация C-функции exec_path при помощи API-вызова scm_c_define_gsubr() и назначение ей имени "exec-path" в среде Guile. Аргументы в эту процедуру не передаются, поэтому значения количества обязательных (req), необязательных (opt) и прочих (rst) параметров равны 0. Кроме регистрации создаётся Scheme-связывание самого верхнего уровня для этой процедуры в текущей среде, то есть exec-path становится глобальной общедоступной процедурой. Далее инициализируется интерактивная среда Guile.

В корневой функции main выполняется вызов нашей функции call_guile посредством API-функции scm_boot_guile(), описанной в начале данного раздела. Результат компиляции и выполнения данной программы — на рис.3.2.

Рис.3.2. Вызов интерактивной оболочки Guile с включением новой процедуры
Рис.3.2. Вызов интерактивной оболочки Guile с включением новой процедуры
Рис.3.2. Вызов интерактивной оболочки Guile с включением новой процедуры

Комбинируя описанные выше методики, вы можете эффективно использовать среду интерпретатора Guile, дополняя его собственными функциями и настраивая в соответствии с текущими требованиями.

2. Библиотека libguile

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

Следует отметить, что кроме стандартных функциональных возможностей Scheme-интерпретатора библиотека libguile также предлагает динамическое преобразование типов, механизм "сборки мусора", поддержку "продолжений" (continuations) и т.д.

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

Менее очевидной, а потому и более важной является необходимость постоянно помнить о том, что нельзя непосредственно проверять значения SCM-переменных на истинность/ложность. В Scheme значение #f обозначает "ложь", и любая SCM-переменная может содержать это значение. Но это не означает, что SCM-представление #f в C-коде также даст результат "ложь". Для того, чтобы корректно проверить значение SCM-переменной на истинность или на ложность, следует пользоваться функциями scm_is_true() или scm_is_false() соответственно.

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

Единственной допускаемой операцией является непосредственное присваивание значения типа SCM переменной типа SCM с помощью оператора = языка C (такое присваивание было выполнено в нашем первом примере в разделе 1.2).

3. Примеры использования функций из библиотеки libguile

Небольшие примеры, приведённые ниже, не являются самостоятельными программами, а лишь служат иллюстрациями, показывающими корректную работу с SCM-значениями в C-коде с помощью функций из библиотеки libguile. Следующий пример состоит из одной функции, выполняющей инкрементирование только при условии истинности специального флага.

SCM cond_increment_func( SCM count, SCM flag )
{
  SCM result;
  if( scm_is_true( flag ) )
    result = scm_sum( count, scm_from_int(1) );
  else
    result = a;
  return result;
}

Обратите внимание на необходимость преобразования C-значения целого числа 1 в SCM-представление для того, чтобы выполнить его сложение с содержимым SCM-переменной count опять-таки с помощью специальной функции scm_sum(). Учитывая ограничения, описанные в предыдущем разделе, мы не обращаемся непосредственно к содержимому SCM-переменных — только через функции библиотеки libguile.

Здесь уместно сказать, что libguile предоставляет множество функций преобразования как из C-представления значений в SCM-представление, так и наоборот — из SCM в C. Общая схема такова: формы scm_from_тип() выполняют преобразования C-значений в SCM, а формы scm_to_тип() — обратное преобразование.

Логические значения Scheme доступны в C-коде без необходимости их преобразования в следующей форме: Scheme-значению #f соответствует C-представление SCM_BOOL_F, а значению #t — представление SCM_BOOL_T.

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

Guile (Scheme), как встраиваемый язык расширений для C-программ предоставляет интересную перспективу развития в направлении объединения функционального и императивного подходов при создании программ.

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


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


Комментарии

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

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