Данный файл является частью Руководства по TADS для авторов игр.
Copyright © 1987 - 2002 Майкл Дж. Робертс (Michael J. Roberts). Все права защищены.

Руководство было преобразовано в формат HTML Н. К. Гайем (N. K. Guy), компания tela design.

Перевод руководства на русский язык - Валентин Коптельцев


Приложение G


Внешние функции

В (R)TADS имеется средство, позволяющее вызывать из игры функции/подпрограммы, определенные при помощи другого языка программирования (Си или Ассемблера). Это может понадобиться, например, для считывания информации из файла на диске, для получения доступа к драйверам устройств на компьютере и т. п. Эти функции мы в дальнейшем будем называть "внешними функциями" или "подпрограммами пользователя". В данном приложении описывается процесс создания и использования внешних функций.

Прежде, чем продолжить, необходимо предупредить вас о том, что написание подпрограммы пользователя - дело сложное. Мы постарались максимально упростить создание и вызов внешних функций, однако вам всё равно потребуется изучить значительное количество неочевидных моментов, чтобы пользоваться этим механизмом. Кроме того, по причинам ограниченности объема Руководства и отсутствия у автора соответствующих педагогических способностей здесь не будет информации о программировании на Си, Ассемблере или других языках, а также об использовании компилятора, компоновщика и других программистских инструментов. Таким образом, предполагается, что вы уже знакомы с работой в среде разработки, в которой вы предполагаете создавать внешние функции.


Объявление внешних функций и обращение к ним

С точки зрения программы на (R)TADS внешняя функция практически ничем не отличается от встроенной. Единственная разница состоит в том, что компилятору необходимо сообщить о том, что функция является пользовательской. Это делается при помощи специальной инструкции external function. Эта инструкция аналогична предварительному объявлению для обычных функций; например, для объявления внешней функции с именем myfunc инструкция выглядит следующим образом:

  myfunc: external function; 

После такого объявления к функции myfunc можно обращаться точно так же, как к любой другой встроенной или определенной пользователем функции. Обращение может быть, например, таким:

  x := myfunc(10, 'hello'); 


Пример внешней функции

Использование готовой внешней функции не составляет труда, однако ее написание - несколько более сложная задача. Оставшаяся часть этого приложения как раз посвящена написанию пользовательских подпрограмм. Обратите внимание, что в состав дистрибутива TADS входит пример внешней функции на Си, а также небольшая программа на TADS, содержащая обращение к ней. Сама функция определена в файле TESTUX.C, а программа TADS, вызывающая ее - в файле TESTUX.T. Кроме того, вам может оказаться полезным ознакомиться с содержанием файла TADSEXIT.H, который следует включать в текст ваших внешних функций на Си. Этот файл определяет интерфейс между TADS и внешними функциями на Си.

Обратите внимание, что процесс компиляции и связывания внешней функции зависит от операционной системы и среды разработки. В файле TESTUX.C имеется информация по компиляции и связывания для большого количества систем. Внешнюю функцию можно написать на любом языке, поддерживающем интерфейс в стиле Си; в частности, для обращения из внешней функции к драйверам устройств компьютера предпочтительным может оказаться язык Ассембелера. Однако в дистрибутив TADS входит только интерфейсный файл для функций на Си (TADSEXIT.H), и инструкции в TESTUX.C тоже относятся только к языку Си. Если вы хотите использовать другой язык, вы должны быть достаточно уверенным пользователем своей среды разработки и операционной системы для того, чтобы соответствующим образом скорректировать эти инструкции.


Написание внешних функций

В общем и целом внешняя функция TADS пишется, как обычная функция на Си. Ограничения следующие. Запрещено использовать статические и глобальные переменные, при этом можно использовать автоматические переменные. Не допускается вызов функций библиотеки времени исполнения (например, printf), которые могут использовать глобальные переменные. Если ваша функция или любая функция из тех, к которым она обращается, использует глобальные переменные, результаты исполнения будут непредсказуемыми, вплоть до вылета программы с ошибкой.

(На самом деле утверждение о запрете на использование статических и глобальных переменных - это некоторое упрощение. Для ряда операционных систем, в частности, MS-DOS, Atari ST, Macintosh, допустимо использование глобальных переменных при условии, что для них используется относительная адресация - иначе говоря, они должны располагаться в том же программном сегменте, что и объектный код вашей внешней функции, а их адрес должен формироваться с использованием счетчика команд. Если вы достаточно хорошо знакомы с используемым вами компилятором, чтобы формировать статические переменные с относительной адресацией (большинство компиляторов умеют это, если задать им правильную комбинацию "секретных" опций), то сможете использовать статические переменные без всяких проблем. Обсуждение того, как именно добиться этого на вашем конкретном компиляторе, очевидно, выходит за рамки настоящего Руководства).

Первой в программе на Си, содержащей вашу внешнюю функцию, должна идти директива #include следующего вида:

  #include <tadsexit.h> 

Данная директива вставляет заголовочный файл tadsexit.h (входящий в состав дистрибутива TADS) в текущий компилируемый модуль. Этот файл определяет интерфейс между TADS и вашей внешней функцией с использованием набора простых в использовании макрокоманд. Благодаря этим макрокомандам вам не требуется разбираться в запутанных тонкостях структур данных и последовательностей вызовов, которые использует TADS для обращения к вашей внешней функции и обмена информацией с ней.

Вне зависимости от имени внешней функции, использованной в инструкции external function вашей программы на TADS, название функции в программе на Си всегда будет main. Объявление этой функции будет выглядеть следующим образом:

  int main(ctx)
  tadsuxdef far *ctx;
  {

У вас на этом месте могут возникнуть два вопроса. Во-первых, если в программе на Си функция объявлена как main, то как TADS будет знать, что при обращении к функции с именем, скажем, myexit потребуется вызвать именно ее? Во-вторых, если в игре определено более одной внешней функции, то как все они могут иметь одно и то же имя main?

Ответ на оба этих вопроса звучит так: TADS "узнает" имя внешней функции не от компилятора Си (для которого ее имя всегда main), а из других источников. Для MS DOS, Atari ST и Macintosh таким источником будет так называемый "менеджер ресурсов". Версия этого менеджера для MS DOS- и Windows-совместимых систем входит в дистрибутив TADS (файл TADSRSC32.EXE). В файле TESTUX.C описывается порядок компиляции и последующего использования редактора ресурсов для компиляторов Turbo C и Microsoft C версии 6.00 под MS-DOS, а также для компилятора Mark Williams C для Atari ST. На системах под Macintosh можно использовать стандартный редактор ресурсов (например, ResEdit), который должен входить в дистрибутив среды разработки Си. Для более-менее современных 32-битных версий Windows (начиная с Win 95 и NT) внешняя функция компилируется в динамическую библиотеку (dll), которую следует разместить в одном каталоге с файлом игры; имя файла этой библиотеки (без расширения) и будет восприниматься TADS в качестве имени внешней функции.

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

Аргумент, указанный в заголовке функции main, не соответствует аргументам, передающимся игровой программой внешней функции при обращении к ней. Это, скорее, некий "контекст", используемый для обмена информацией с TADS. Используя этот контекст, можно получить доступ к аргументам, изначально переданным внешней функции.

Все ваши аргументы размещаются в "стеке", что означает, что вы можете извлекать их по одному. Сначала необходимо вызвать функцию tads_tostyp(ctx), чтобы определить тип первого аргумента. Она вернет одно из следующих значений (определенных в TADSEXIT.H): TADS_NUMBER, означающее, что аргумент является длинным целым; TADS_STRING для аргумента строкового типа; наконец, TADS_NIL или TADS_TRUE для одного из логических (булевских) значений. Аргументы иных типов внешним функциям передавать нельзя, поскольку те не смогут с ними работать.

Далее необходимо извлечь из стека первый аргумент, для чего требуется вызвать одну из функций в зависимости от полученного на предыдущем шаге типа: tads_popnum(ctx) для чисел, tads_popstr(ctx) для строк и tads_pop(ctx) для обоих булевских значений. Последняя функция не возвращает никакого значения, поскольку значение аргумента ("истина" или nil) в этом случае целиком определяется полученным ранее типом, но вызвать ее требуется, чтобы удалить аргумент из стека (и таким образом открыть доступ к последюущим аргументам). Две другие функции возвращают соответственно значение типа "длинное целое" и дескриптор строки.

Возвращаемый функцией tads_popstr(ctx) дескриптор строки - это не указатель на строку в том смысле, как это понимается в языке Си. Он представляет собой специальную структуру данных, которая содержит длину строки и текст (содержимое) строки. Именно в таком виде хранятся строковые значения в TADS версии 2 - строки с завершающим нулем в стиле Си здесь не используются. Напрямую использовать дескриптор строки нельзя, однако имеются две функции, позволяющие разложить его на значения, с которыми вы сможете работать. Для получения длины строки осуществите вызов tads_strlen(ctx, str) (здесь str - это полученный при помощи функции tads_popstr(ctx) дескриптор строки); функция вернет длинное целое, которое и будет соответствовать искомой длине. Вызов tads_strptr(ctx, str) позволит получить доступ к содержимому строки; функция вернет указатель на символ с адресом, по которому записан первый байт текста строки. Обратите внимание, что завершающего нуля здесь нет, и использовать функции Си, работающие со строками с завершающим нулем, в данном случае нельзя. Вместо этого перед считыванием необходимо определить размер текстового буфера, используя функцию tads_strlen.

Для хранения значения, возвращаемого функцией tads_strpop(), необходимо использовать специальный тип данных tads_strdesc. Этот тип определен в файле TADSEXIT.H наряду с интерфейсными функциями.

Никогда не осуществляйте запись в текстовый буфер, полученный посредством функции tads_strptr(). TADS исходит из того, что эти области памяти не будут изменяться пользовательской программой; если все-таки осуществить такую модификацию, результаты могут быть непредсказуемыми.

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

Если ваша внешняя функция должна возвращать значение, вам необходимо использовать одну из функций семейства tads_pushxxx(ctx, value). Для числовых значений используйте tads_pushnum(ctx, value), где value - целое число. Для возвращения булевского значения используйте tads_pushtrue(ctx) (для значения "истина") либо tads_pushnil(ctx) (для nil). Для строки с завершающим нулем в стиле Си подойдет функция tads_pushcstr(ctx, pointer), где pointer - это адрес первого байта такой строки.

Вы также можете выделить в памяти пространство под строку и сформировать возвращаемые данные в этом пространстве. Вызовите для этого функцию tads_stralo(ctx, len), где len - целочисленное значение, соответствующее количеству байт (символов) в возвращаемом строковом значении. Эта функция вернет указатель на на первый байт выделенной области памяти, куда и можно будет записать возвращаемую строку. Строка не должна завершаться нулем, а ее длина должна совпадать с размером выделенной области. Закончив формирование (запись) строки, необходимо осуществить вызов функции tads_pushastr(ctx, pointer), где pointer - значение, изначально возвращенное функцией tads_stralo(). TADS запоминает размер выделенной по вашему запросу области памяти, и передавать ее размер повторно при возврате строки не требуется.





Знаток языков обоих.
ГОРАЦИЙ, Оды (23 до н. э.)


Приложение F Содержание Приложение H