4: Введение в программирование скриптов на C

Bash CGI

4: Введение в программирование скриптов на C

Информация

Язык программирования C — это традиционный инструмент разработки программного обеспечения, используемый на протяжении последних 25 лет (с момента появления Unix). С учетом того, что Unix в настоящее время является основной серверной средой, умение программировать CGI-скрипты на C является одним из необходимых условий успешной работы Web-инженера.

Вникнув повнимательнее в спецификацию CGI, любой программист поймет, что спецификация создавалась с расчетом на Unix и С. Работа с переменными окружения и потоками стандартного ввода/вывода построена с учетом особенностей среды и средств программирования. При адаптации спецификации CGI в других средах, например, MS-Windows, программирование многих механизмов обмена приходится модифицировать.

Большинство задач при разработке скриптов можно решить средствами Perl, но есть ряд задач, которые требуют использования C-программ. Наиболее распространенным применением C являются скрипты-интерфейсы к базам данных. В качестве примера такого интерфейса воспользуемся библиотекой PostgreSQL.

В данном разделе речь пойдет о программировании CGI-скриптов на классическом C без объектно-ориентрованных особенностей C++.

Общая структура C-скрипта

Скрипт на языке C ничем не отличается от обычной C-программы. Собственно, это набор процедур, среди которых есть главная процедура. Этой главной процедуре передается управление при загрузке программы в оперативную память. Главная процедура контролирует вызов других процедур и весь ход выполнения программы. Простой скрипт — это одна главная процедура.

При разработке С-скрипта следует всегда помнить, что в отличие от скрипта на Bash и Perl, С-скрипт, прежде чем выполнить, нужно еще и откомпилировать, т.е. превратить в исполняемый компьютером код. Если в системе нет компилятора для C, то программировать С-скрипты будет довольно сложно.

Синтаксически главная процедура выглядит следующим образом:

#include <stdlib.h>
#include <stdio.h>>
void main(argc,argv,env)
int *argc;
char *argv[];
char *env[];
{
/* тело программы */
}

В этом фрагменте представлены все основные элементы программирования CGI-скриптов на С. Строки в начале программы (#include ... ) позволяют включить в текст программы декларации (описатели) стандартных функций. Строка "void main ..." — это объявление главной процедуры. В качестве параметров в данную процедуру (функцию) передаются:

  • число аргументов командной строки — argc;
  • указатель на массив аргументов командной строки — argv;
  • указатель на массив переменных окружения — env.

Само тело программы помещается между символами фигурных скобок "{...}". Фраза "тело программы" размещена между парой "/* ... */". Это комментарий. Сама программа на С состоит из операторов. Операторы могут быть простые и составные. Простые операторы – это, например, оператор присваивания. Составной оператор — это блок. Блок представляет собой последовательность операторов, заключенную в фигурные скобки "{...}". В конце простого оператора должен стоять символ ";". В нашем примере объявление (декларирование) переменных перед блоком тела программы — это последовательность простых операторов.

Про С часто говорят, что в языке только пять операторов, а все остальное — это библиотека стандартных функций. Операторов в нем, конечно, больше, но такая характеристика в целом верна.

Стандартный поток вывода

Стандартный поток вывода в С ассоциируется с дескриптором STDOUT. Самым распространенным способом записи данных в этот поток является функция форматного вывода printf. Если скрипт должен что-то передать браузеру пользователя, то первое, что нужно сделать — это применить printf для формирования HTTP-заголовка:

main()
{
printf("Content-type: text/html\n\n");
printf("<H1>C и CGI</H1>");
}

Первый вызов printf формирует заголовок — определяет тип тела HTTP-отклика, а второй вызов формирует заглавие первого уровня в HTML-документе. В общем случае у функции printf три аргумента: printf(FILE,"format",VARS_LIST); FILE — дескриптор файла, "format" — формат вывода данных, VARS_LIST — список переменных, чьи значения подлежат выводу. Если дескриптор файла опущен, то вывод направляется в поток стандартного вывода. Список переменных указывается в том случае, если в формате вывода есть шаблоны вывода для переменных из этого списка.

Для каждого типа данных в С существует свой шаблон вывода. Перечислим только некоторые из них:

%d — вывод целого числа;
%s — вывод массива символов (строки);
%f — вывод вещественного числа;
%x — вывод целого числа в шестнадцатеричном
     виде.

Для того, чтобы распечатать аргументы командной строки, можно применить следующий формат:

int i;
...
for(i=0;i<argc;i++)
{
printf("arg[%d]=%s\n",i,argv[i]);
}

В данном случае переменная цикла i — это целая константа, поэтому в квадратных скобках указано [%d]. Второй аргумент списка переменных — указатель на массив символов (строка, содержащая значение аргумента командной строки), поэтому после знака равенства ("=") применен шаблон вывода массива символов %s.

Переменные окружения

Третий аргумент главной процедуры — указатель на массив переменных окружения, каждый элемент которого представляет собой строковую константу вида "имя-значение". Неудобство работы с этим массивом заключается в том, что заранее не известно, сколько элементов он содержит. Список переменных окружения кончается в тот момент, когда при его переборе встречается указатель NULL. Пример такого перебора представлен ниже:

#include <stdlib.h>
#include <stdio.h>
void main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
int i;
i=0;
while(env[i])
{
printf("%d:%s\n",i,env[i]);
i++;
}

}

В данном случае перебор идет до тех пор, пока значение указателя больше 0. При этом переменные окружения выдаются в виде "имя:значение". Для того, чтобы воспользоваться этими значениями, придется разбирать каждую строку, выделяя имя и значение переменной при помощи манипуляций с адресами внутри строки. Любые адресные операции — это потенциальные ошибки (хотя они и позволяют писать быстрые программы).

К счастью, в С есть функция getenv(). В качестве аргумента этой функции достаточно указать имя переменной окружения, и система вернет указатель на его значение. Например, необходимо знать, сколько символов нужно считать со стандартного ввода скрипта:

int i,n;
char *query;
...
n = atoi(getenv("CONTENT_LENGTH"));
query = (char *) malloc(n+1);
for(i=0;i<n;i++)
{
query[i] = getc();
}
...
free(query);

В этом примере при помощи последовательного применения getenv и atoi (ascii to integer) из переменной окружения в переменную целого типа помещается значение переменной окружения — CONTENT_LENGTH. Последовательное применение функций здесь необходимо, т.к. значение переменной окружения — строка, а мы хотим использовать число, следовательно, строку символов следует не только получить, но еще и преобразовать в число.

Для преобразования строк в числа часто используют функцию форматного ввода sscanf. В нашем случае это выглядело примерно следующим образом:

char *length;
int n;
...
length = getenv("CONTENT_LENGTH");
sscanf(length,"%d",&n);
...

Следует заметить, что sscanf — это довольно сложная функция. Она предназначена для ввода любой информации и ее преобразования. Как показывает практика, иногда sscanf работает не так, как предполагает программист. Поэтому незачем стрелять из пушки по воробьям — применяйте лучше специализированные функции преобразования, например, atoi, если это возможно.

Аргументы командной строки

Аргументы командной строки передаются в С-скрипт через второй параметр главной процедуры. Второй параметр — массив указателей на строковые константы, которые и есть аргументы командной строки. Число таких аргументов определяется первым параметром главной процедуры. В этом смысле просмотреть все аргументы командной строки можно в цикле for:

#include <stdlib.h>
#include <stdio.h>
main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
int i;
printf("Content-type: text/plain\n\n");
for(i=0;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}

}

В данном случае скрипт генерирует простую текстовую страницу, на которой в столбик распечатываются аргументы командной строки скрипта. Такие аргументы появляются только у запроса типа ISINDEX. При работе с числовыми аргументами нужно помнить, что передаются они в программу как строки, и их следует преобразовывать в числа. Лучше всего это делать при помощи функций atoi.

Стандартный поток ввода

Cчитывать данные в программу на C принято из файлов. При этом файлы ассоциируются с потоками данных. Поток данных — это последовательность октетов (8 бит, или более привычно — байт). Если считывается текстовый файл, то мы имеем дело с последовательностью символов. Если считываем двоичный файл — имеем дело с октетами (байтами). Все функции С, которые работают с файлами ориентированы на эту модель — на потоки данных.

С каждым файлом при открытии потока данных связан дескриптор файла, который, являясь совокупностью данных о потоке, описывает поток. Со стандартным потоком ввода в С связывают дескриптор с именем STDIN. Во многих случаях это имя указывать не надо, т.к. оно предполагается по умолчанию.

Самый простой способ чтения потока стандартного ввода обеспечивает функция посимвольного чтения getc():

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
void main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
char *query;
int length;
length = atoi(getenv("CONTENT_LENGTH"));
query = (char *) malloc(length+1);
memset(query,'\000',length+1);
for(int i=0;i<length;i++)
{
query[i] = getc();
}
free(query);
}

Функция getc() доставляет по одному символу из потока стандартного ввода. Не следует думать, что это медленный способ чтения. Во-первых, сервер передает данные через канал (pipe), а во-вторых, поток буферизуется. Поэтому даже посимвольное чтение данных происходит достаточно быстро.

Можно прочитать и все данные сразу. Для этого пользуются функцией fread():

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
void main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
char *query;
int length;
length = atoi(getenv("CONTENT_LENGTH"));
query = (char *) malloc(length+1);
memset(query,'\000',length+1);
fread(query,length,1,STDIN);
free(query);
}

В данном случае мы читаем из потока стандартного ввода STDIN в буфер query ровно length символов.

Последнее замечание. В наших примерах используется функция malloc(). Эта функция отводит память под данные, которые мы считываем со стандартного ввода. После завершения скрипта нужно обязательно освободить эту память, не уповая на то, что после завершения программы вся память все равно освободится. Здесь учитывается два момента. Во-первых, использование такого способа размещения данных связано с попыткой избежать переполнения при использовании областей памяти фиксированной длины, которое приводит к аварийному завершению программы. Во-вторых, при переходе от CGI-скриптов к FastCGI-скриптам это позволяет избежать "утечки" памяти. В FastCGI скрипт не завершается после ответа клиенту, а остается "висеть" в памяти в ожидании следующего запроса. Если память не освободить, то при новом обращении произойдет резервирование новой области памяти. В конечном итоге свободной памяти может и не оказаться.

Типы данных и переменные

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

Любая переменная в С должна быть продекларирована. В противном случае при выполнении многих операций можно получить неожиданные результаты. Для каждого типа данных используется своя форма декларации переменной. Для аккуратной работы с типами данных в С следует использовать операцию преобразования данных. Последнее особенно актуально при работе с указателями и числовыми данными, в которых можно потерять точность.

Целые числа

Целое число декларируется как:

int a;
unsigned int au;
short b;
long c;

Нас интересуют только первые две строки. Последняя строка декларирует длинное целое число. Короткое число попадает в интервал -214<a<214. Если у числа указан модификатор unsigned, то оно попадает в интервал 0<a<215.

Одновременно с декларированием число можно проинициализировать, что вообще-то рекомендуется делать всегда:

int a=0,b=0;

Как видно из этого примера, в одном операторе декларирования (объявления) переменных можно указать сразу несколько переменных одного типа и при этом их можно инициализировать. Переменные целого типа необходимы в CGI-программировании при обработке обращений по методу POST. Для того, чтобы считать данные из потока стандартного ввода, нужно указать скрипту, сколько байтов оттуда следует считать. При этом сначала текстовую константу из переменной окружения CONTENT_LENGTH следует преобразовать в число, а затем использовать в операторах чтения или цикла:

#include <stdio.h>
#include <string.h>
void main()
{
char *length,*buf;
int n,i;
length = (char *) getenv("CONTENT_LENGTH");
n = atoi(length);
buf = (char *) malloc(n+1);
memset(buf,'\000',n+1);
for(i=0;i<n;i++)
{
buf[i] = getc();
}
printf("Content-type: text/plain\n\n%s\n",
        buf);
free(buf);

Функция getenv() позволяет получить значение переменной CONTENT_LENGTH, а функция atoi() — преобразовать это значение в целое число, которое потом используется в качестве границы при посимвольном чтении данных из стандартного потока ввода.

Строки символов

В С нет специального типа данных, который позволял бы работать со строками символов. Для этой цели используются массивы символов. Одиночный символ или массив символов можно объявить через оператор char:

char a='\000';
char buf[];
char buf[20];

В первом случае переменная a — это просто одиночный символ. В зависимости от реализации компилятора на него будет отводиться разное число байтов. В наиболее экономичном варианте — 1 байт, если символ отображается на короткое целое — 2 байта, если в архитектуре аппаратной платформы нет числа меньше четырех байтов — на четыре байта. Одним словом, не следует думать, что под символ всегда отводится 1 байт.

Символы можно использовать в арифметических операциях:

#include <stdio.h>
#include <stdlib.h>
void main()
{
unsigned char a='a';
a++;
printf("%c\n",a);
}

В результате этих нехитрых операций вместо "a" будет напечатано "b". Код символа рассматривается как целое число и увеличивается на 1. Этот принцип можно использовать при преобразовании строчных латинских букв в заглавные (с буквами русского алфавита так не получится).

Определив массив символов buf[20] в 20 символов длиной, мы зарезервировали под него место, в которое можем разместить 20 символов. При объявлении buf[] мы только обозначаем, что будем использовать переменную buf для обращений к массиву символов. Места под сам массив мы не отводим. То есть место мы отвели, но под указатель — переменную, которая будет хранить адрес массива символов. Следовательно, buf[20] отводит место под массив и под указатель на него.

Однако, как же быть со строками символов? Ведь все переменные окружения — это строки символов. Для работы с ними используются функции, которые описаны в include-файле string.h:

strcmp() — сравнение строк
strcpy() — копирование строк
strstr() — поиск подстроки
и т.д.

При этом строка распознается по символу '\000' в конце строки, т.е. все разряды байта или совокупности байтов, в которой расположен символ, равны нулю. Приведем пример копирования QUERY_STRING в массив символов query:

#include <stdio.h>
#include <string.h>
void main()
{
char query[1024];
strcpy(query,getenv("CONTENT_LENGTH"));
printf("Content-type: text/plain\n\n%s\n",
        query);
}

Функция strcpy() копирует строку запроса из переменной окружения QUERY_STRING в переменную query и после нее дописывает символ '\000'. Мы заранее отвели побольше символов под массив query, т.к. strcpy не проверяет границ массива, и при такой операции можно запросто "наехать" на область памяти, не отведенной под наш скрипт, что приведет к его аварийному завершению (segmentation violation — как это знакомо). Поэтому лучше контролировать размеры буферов и использовать указатели.

Указатели

Указатели — это наиболее мощное и одновременно опасное средство программирования в С. В современных языках, таких как Java, например, указатели уничтожают как класс, т.к. именно они — основной источник множества ошибок, а, точнее, манипуляции с ними.

С другой стороны, нельзя написать эффективной программы (быстро исполняется и занимает мало памяти), если не использовать указатели и адресную арифметику, которая позволяет манипулировать указателями.

Указатель на переменную определенного типа данных объявляется путем ввода символа "*" перед именем переменной:

int *n;
char *query;

При этом место резервируется под адрес соответствующего значения. В Intel-платформах существуют модификации указателей в зависимости от модели памяти (т.е. какой длины адрес должен использоваться — 16 бит, 24 бита или более). В 64-разрядных архитектурах просто указывается опция компилятора (например, в Irix 6.4 "-64" — длинные адреса для всей программы или "-32" — короткие адреса).

С указателями при программировании CGI-скриптов приходится сталкиваться постоянно. Указатель на переменную окружения возвращает функция getenv(). Другими словами, она возвращает адрес начала значения переменной окружения:

char *length;
length = (char *) getenv("CONTENT_LENGTH");

Другой пример — указатель на массив переменных окружения и массив аргументов командной строки:

#include <stdlib.h>
#include <stdio.h>
void main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
/* тело программы */
}

В данном случае конструкция типа *argv[] — это массив указателей, которые указывают на символы, а точнее, на символьные переменные. В данном контексте совсем по-другому смотрится конструкция char a[] — это просто иная форма записи указателя. Переменная a — это указатель на массив. Есть, правда, один нюанс. Он заключается в том, что, обращаясь к элементам массива, перемещаться мы будем на длину элемента массива, а это уже зависит от типа данных.

Управление потоком вычислений

С представляет собой универсальный язык программирования со всеми присущими подобным языкам атрибутами. Он родился в период увлечения структурным программированием, поэтому в нем есть операторы, которые позволяют построить программу без использования goto.

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

  • оператор if;
  • оператор goto ;
  • оператор while ;
  • оператор for ;
  • оператор switch ;
  • оператор break.

В целом этого набора должно хватить для программирования CGI-скриптов.

Оператор if

Условное исполнение части кода программы в С определяется оператором if. В общем случае он имеет синтаксис:

if(условие) оператор; [else оператор;]

или

if(условие) { тело_блока } 
  [else { тело_блока }]

В этой записи "условие" — это логическое выражение, которое возвращает значения "истина" или "ложь", например, "x>y". Оператор в данном контексте — это простой оператор С, например, "x=1". Блок — это совокупность простых операторов и/или блоков. Самый простой пример применения if — определение метода доступа к CGI-скрипту:

char *query;
int n;
...
if(strcmp(getenv("REQUEST_METHOD"),"GET"))
{
query = getenv("QUERY_STRING");
}
else
{
n = atoi(getenv("CONTENT_LENGTH"));
query = (char *) malloc(n+1);
memset(query,'\000',n+1);
fread(query,n,1,STDIN);
}

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

Оператор goto

Сколько было возражений против использования goto, но он до сих пор существует в большинстве языков программирования. В простых программах, по большому счету, без него можно обойтись, но в ряде случаев он необходим. Оператор goto — это принудительный переход на другой фрагмент кода программы. Например, при разных алгоритмах обработки выхода из цикла:

...
for(i=0;i<n;i++)
{
buf[i]=getc();
if(buf=='\n') goto STRING;
}
printf("В потоке ввода одна строка\n");
STRING:printf(
  "Одна строка считана из потока ввода\n");
...

В данном случае переход осуществляется при обнаружении символа конца строки во входном потоке.

Оператор while

Оператор while позволяет исполнять тело цикла до тех пор, пока верно условие. Например, при распечатке переменных окружения:

#include <stdlib.h>
#include <stdio.h>
void main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
int i;
i=0;
while(env[i])
{
printf("%d:%s\n",i,env[i]);
i++;
}

}

В данном случае программа распечатывает переменные окружения до тех пор, пока указатель env[i] не примет пустое значение.

Оператор for

Оператор for — это детализация общего случая оператора цикла. Он состоит из блока инициализации переменной цикла, условия исполнения тела цикла и блока операторов конца цикла. Самой простой и наиболее распространенной его формой является случай одной переменной цикла:

for(i=0;i<n;i++)
{
/* тело цикла */
}

При программировании CGI-скриптов с оператором цикла можно познакомиться при распечатке аргументов командной строки в запросах типа ISINDEX:

#include <stdlib.h>
#include <stdio.h>
main(argc,argv,env)
int argc;
char *argv[];
char *env[];
{
int i;
printf("Content-type: text/plain\n\n");
for(i=0;i<argc;i++)
{
printf("argv[%d]=%s\n",i,argv[i]);
}

}

В данном случае переменная цикла пробегает значения от 0 до значения переменной argc, которая содержит число аргументов командной строки.

Оператор switch

Оператор switch — это переключатель на несколько положений, если пользоваться терминологией электротехники. Аргумент может принимать некоторые значения, например целочисленные или символьные, а выполняться будет тот фрагмент кода, который описан для этого значения:

switch(x)
{
case 'a': x='A'; break;
...
case 'z': x='Z'; break;
}

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

Оператор break

Оператор break применяется для досрочного завершения цикла, в котором он указан. В некотором смысле он заменяет оператор goto. В примере с оператором switch оператор break использовался для выхода этого оператора из блока. Если бы break там не применялся, то операторы блока исполнялись бы последовательно с точки входа в блок до конца блока.

Оператор continue

Оператор continue применяется для пропуска операторов цикла, непосредственно следующих за оператором continue до конца цикла. В программировании CGI-скриптов оператор можно применять для обхода операторов вывода по условию, например, по IP-адресу удаленного хоста:

while(buf = fgets())
{
if(!strncmp("144.206.160.32",
  getenv("REMOTE_ADDR"),14)) continue;
...
}

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

Файлы

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

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

Для начала работы файл нужно открыть:

FILE *IN;
...
IN = open("text.txt","r");

Объявление (декларация) FILE *IN; определяет переменную IN как указатель на структуру дескриптора файла. Функция open() присваивает указателю IN значение адреса дескриптора файла с именем text.txt. При этом файл открыт только для посимвольного чтения. Если нужно открыть файл для записи, то вместо r следует указать w. Если требуется и чтение, и запись в файл, то его открывают со значением второго аргумента (вместо r) равным r+.

После работы с файлом его нужно закрыть:

FILE *IN;
...
IN = open("text.txt","r");
...
close(IN);

Закрывается файл функцией close(). При этом происходит освобождение памяти из-под структуры дескриптора файла и буферов ввода/вывода, которые были созданы при открытии файла. Желательно закрывать файлы в обратной последовательности (последним закрывается файл, который был открыт первым). Это позволяет избежать фрагментации памяти.

Для чтения данных из файла можно использовать функцию fread():

FILE *IN;
char query[1024];
...
IN = open("text.txt","r");
fread(query,1024,1,IN);
...
close(IN);

В данном случае в массив символов query считывается один блок данных размером 1024 символа из файла, связанного с дескриптором IN. Если числа 1024 и 1 поменять местами, то функция read считает 1024 блока по одному символу.

Для записи в файл применяется другая функция — fwrite():

FILE *OUT;
...
OUT = open("text.txt","w");
fwrite(query,1024,1,OUT);
...
close(OUT);

Значения параметров в этой функции те же, что и в функции fread().

Функции fread() и fwrite() — это функции неформатного ввода/вывода. В них не происходит никакого преобразования данных. Данные записываются в файлы в том виде, в котором они хранятся в переменных. Следует отметить, что в С различают два типа потоков данных: символьные и двоичные. Если fread()/fwrite() применять для двоичного потока, то, действительно, никаких преобразований происходить не будет. Если применять эти функции к символьным потокам данных, то преобразования производятся. Например, выполняется обработка конца строки.

Кроме неформатного ввода/вывода, в С применяют форматный ввод/вывод. Он реализуется через функции fscanf() и fprintf(). Первая функция служит для считывания и преобразования данных, а вторая — для преобразования и записи. Примером применения этих функций может служить счетчик посещения страницы:

#include <stdlib.h>
#include <stdio.h>
void main()
{
FILE *IN;
int n;
IN = open("text.txt","r");
fscanf(IN,"%d",&n);
close(IN);
IN = open("text.txt","w");
n++;
printf("%d",n);
fprintf(IN,"%d",n);
close(IN);
}

В данном случае целое число, записанное ASCII-символами, считывается из файла text.txt, преобразуется в формат целого числа и помещается в переменную n. После этого оно увеличивается на единицу, записывается на старое место и распечатывается в поток стандартного вывода. Если в HTML-странице разместить подстановку (server side include) результатов исполнения этого скрипта, то мы реализуем счетчик посещения страниц.

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

Директивы препроцессора позволяют собрать программу на языке С из готовых блоков кода. Кроме того, можно реализовать управление процессом компиляции, например, разработать процедуру условной компиляции для разных операционных систем.

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

Если необходимо задействовать функции форматного ввода/вывода, а их мы применяем для печати в стандартный вывод, то следует использовать инструкцию #include <stdio.h>:

#include <stdio.h>
void main()
{
printf("Content-type: text/html\n\n");
printf("<HTML>");
printf("<HEAD>");
printf("</HEAD>");
printf("<BODY>");
printf("<H1>Привет от-CGI</H1>");
printf("</BODY>");
printf("</HTML>");
}

Инструкция препроцессора начинается с символа "#". При использовании инструкций включения различают локальные файлы и стандартные файлы включения. Когда применяются стандартные файлы включения, имя файла заключают в "<имя_файла>". При использовании локального файла имя файла заключают в обычные двойные кавычки — "имя_файла". В наших примерах применяются только стандартные файлы включений.

Мы используем файлы включения только для ввода в код программы описаний стандартных функций и констант, с этими функциями связанных. Для наиболее распространенных функций существует файл /usr/include/stdlib.h. Его включения в программу достаточно для того, например, чтобы использовать функции ввода/вывода и сравнения строк:

#include <stdlib.h>
void main()
{
printf("Content-type: text/plain\n\n");
if(strcmp("GET",getenv("REQUEST_METHOD"))
{
printf("Нет даты в потоке STDIN");
}

}

В данном случае в stdlib.h определены шаблоны для функций strcmp() и getenv().

При программировании в среде Unix программист всегда может применить команду man, которая позволяет получить подсказку по использованию той или иной функции C.

Компиляция

Программа на С — это текстовый файл, из которого программа-компилятор создает исполняемый файл. CGI-скрипт — это исполняемый файл. Для компиляции используется компилятор с языка С. В большинстве Unix-платформ этот компилятор носит название cc.

Предположим, что нужно создать программу с именем hello.cgi. Код на С расположен в файле hello.c. В этом случае достаточно выполнить:

bash%cc -o hello.cgi hello.c

Опция "" в этой записи определяет имя исполняемого файла. Он задается сразу вслед за ней. Имя файла исходного текста С указывается просто в качестве параметра.

Если в скрипте использовать функции из внешней библиотеки, то компилятору необходимо указать ее адрес:

bash%cc -o test.cgi test.c -lpq

В данном случае мы используем внешнюю библиотеку pq. Опция -l определяет имя библиотеки. Сама процедура сборки программы называется linking (связывание). Отсюда и буква "l" перед именем библиотеки.