Версия:

Практикум / Практическое задание на C
Практикум / Практическое задание на C

Практическое задание на C

Практическое задание на C

Ниже приводится практическое занятие на языке C: Хранимые процедуры на языке C.

Хранимые процедуры на языке C

Tarantool может вызывать код на языке C с помощью модулей, ffi или хранимых процедур на C. В данном практическом задании рассматривается только третий метод, хранимые процедуры на языке C. На самом деле, программы всегда представляют собой функции на языке C, но исторически сложилось так, что широко используется фраза «хранимая процедура».

Данное практическое задание могут выполнить те, у кого есть пакет программ для разработки Tarantool’а и компилятор языка программирования C. Оно состоит из пяти задач:

  1. easy.c — выводит «hello world»;
  2. harder.c — декодирует переданное значение параметра;
  3. hardest.c — использует API для языка C для вставки в базу данных;
  4. read.c — использует API для языка C для выборки из базы данных;
  5. write.c — использует API для языка C для замены в базе данных.

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

Подготовка

Проверьте наличие следующих элементов на компьютере:

  • Tarantool 1.10
  • Компилятор GCC, подойдет любая современная версия
  • module.h и включенные в него файлы
  • msgpuck.h
  • libmsgpuck.a (только для некоторых последних версий msgpuck)

The module.h file will exist if Tarantool was installed from source. Otherwise Tarantool’s «developer» package must be installed. For example on Ubuntu say:

$ sudo apt-get install tarantool-dev

или на Fedora введите команду:

$ dnf -y install tarantool-devel

The msgpuck.h file will exist if Tarantool was installed from source. Otherwise the «msgpuck» package must be installed from https://github.com/rtsisyk/msgpuck.

Чтобы компилятор C увидел файлы module.h и msgpuck.h, путь к ним следует сохранить в переменной. Например, если адрес файла module.h/usr/local/include/tarantool/module.h, а адрес файла msgpuck.h/usr/local/include/msgpuck/msgpuck.h, введите команду:

$ export CPATH=/usr/local/include/tarantool:/usr/local/include/msgpuck

Статическая библиотека libmsgpuck.a нужна для версий msgpuck старше февраля 2017 года. Только в том случае, если встречаются проблемы соединения при использовании операторов GCC в примерах данного практического задания, в пути следует указывать libmsgpuck.a (libmsgpuck.a создан из исходных файлов загрузки msgpuck и Tarantool, поэтому его легко найти). Например, вместо «gcc -shared -o harder.so -fPIC harder.c» во втором примере ниже, необходимо ввести «gcc -shared -o harder.so -fPIC harder.c libmsgpuck.a».

Tarantool выполняет запросы в качестве клиента. Запустите Tarantool и введите эти запросы.

box.cfg{listen=3306}
    box.schema.space.create('capi_test')
    box.space.capi_test:create_index('primary')
    net_box = require('net.box')
    capi_connection = net_box:new(3306)

Проще говоря: создайте спейс под названием capi_test, и выполните соединение с одноименным capi_connection.

Не закрывайте клиент. Он понадобится для последующих запросов.

easy.c

Запустите еще один терминал. Измените директорию (cd), чтобы она совпадала с директорией, где запущен клиент.

Создайте файл. Назовите его easy.c. Запишите в него следующие шесть строк.

#include "module.h"
    int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      printf("hello world\n");
      return 0;
    }
    int easy2(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      printf("hello world -- easy2\n");
      return 0;
    }

Скомпилируйте программу, что создаст файл библиотеки под названием easy.so:

$ gcc -shared -o easy.so -fPIC easy.c

Теперь вернитесь в клиент и выполните следующие запросы:

box.schema.func.create('easy', {language = 'C'})
    box.schema.user.grant('guest', 'execute', 'function', 'easy')
    capi_connection:call('easy')

Если эти запросы вам незнакомы, перечитайте описание box.schema.func.create(), box.schema.user.grant() и conn:call().

Важна функция capi_connection:call('easy').

Во-первых, она ищет функцию easy, что должно быть легко, потому что по умолчанию Tarantool ищет в текущей директории файл под названием easy.so.

Во-вторых, она вызывает функцию easy. Поскольку функция easy() в easy.c начинается с printf("hello world\n"), слова «hello world» появятся на экране.

В-третьих, она проверяет, что вызов прошел успешно. Поскольку функция easy() в easy.c оканчивается на return 0, сообщение об ошибке отсутствует, и запрос выполнен.

Результат должен выглядеть следующим образом:

tarantool> capi_connection:call('easy')
    hello world
    ---
    - []
    ...

Теперь вызовем другую функцию в easy.c — easy2(). Она практически совпадает с функцией easy(), но есть небольшое отличие: если имя файла не совпадет с именем функции, нужно будет указать file-name.function-name.

box.schema.func.create('easy.easy2', {language = 'C'})
    box.schema.user.grant('guest', 'execute', 'function', 'easy.easy2')
    capi_connection:call('easy.easy2')

… и на этот раз результатом будет: «hello world – easy2».

Вывод: вызвать C-функцию легко.

harder.c

Вернитесь в терминал, где была создана программа easy.c.

Создайте файл. Назовите его harder.c. Запишите в него следующие 17 строк:

#include "module.h"
    #include "msgpuck.h"
    int harder(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      uint32_t arg_count = mp_decode_array(&args);
      printf("arg_count = %d\n", arg_count);
      uint32_t field_count = mp_decode_array(&args);
      printf("field_count = %d\n", field_count);
      uint32_t val;
      int i;
      for (i = 0; i < field_count; ++i)
      {
        val = mp_decode_uint(&args);
        printf("val=%d.\n", val);
      }
      return 0;
    }

Скомпилируйте программу, что создаст файл библиотеки под названием harder.so:

$ gcc -shared -o harder.so -fPIC harder.c

Теперь вернитесь в клиент и выполните следующие запросы:

box.schema.func.create('harder', {language = 'C'})
    box.schema.user.grant('guest', 'execute', 'function', 'harder')
    passable_table = {}
    table.insert(passable_table, 1)
    table.insert(passable_table, 2)
    table.insert(passable_table, 3)
    capi_connection:call('harder', passable_table)

На этот раз вызов передает Lua-таблицу (passable_table) в функцию harder(). Функция``harder()`` увидит это, как указано в параметре char *args.

На данный момент функция harder() начнет использовать функции, определенные в msgpuck.h. Процедуры, которые начинаются с «mp» — это функции msgpuck, которые обрабатывают данные в формате MsgPack. Передача и возврат всегда осуществляются в этом формате, поэтому следует ознакомиться с msgpuck для того, чтобы овладеть навыками работы с API для языка C.

Однако, пока достаточно понимать, что функция mp_decode_array() возвращает количество элементов в массиве, а функция mp_decode_uint возвращает целое число без знака из args. Есть также побочный эффект: по окончании декодирования args изменился и теперь указывает на следующий элемент.

Таким образом, первой будет отображена строка «arg_count = 1», поскольку был передан только один элемент: passable_table.
Второй будет отображена строка «field_count = 3», потому что в таблице находятся три элемента.
Следующие три строки будут «1», «2» и «3», потому что это значения элементов в таблице.

Теперь вывод на экране выглядит следующим образом:

tarantool> capi_connection:call('harder', passable_table)
    arg_count = 1
    field_count = 3
    val=1.
    val=2.
    val=3.
    ---
    - []
    ...

Вывод: на первый взгляд, декодирование значений параметров, переданных в C-функцию непросто, но существуют документированные процедуры для этих целей, и их не так много.

hardest.c

Вернитесь в терминал, где были созданы программы easy.c и harder.c.

Создайте файл. Назовите его `hardest.c. Запишите в него следующие 13 строк:

#include "module.h"
    #include "msgpuck.h"
    int hardest(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      uint32_t space_id = box_space_id_by_name("capi_test", strlen("capi_test"));
      char tuple[1024]; /* Must be big enough for mp_encode results */
      char *tuple_pointer = tuple;
      tuple_pointer = mp_encode_array(tuple_pointer, 2);
      tuple_pointer = mp_encode_uint(tuple_pointer, 10000);
      tuple_pointer = mp_encode_str(tuple_pointer, "String 2", 8);
      int n = box_insert(space_id, tuple, tuple_pointer, NULL);
      return n;
    }

Скомпилируйте программу, что создаст файл библиотеки под названием hardest.so:

$ gcc -shared -o hardest.so -fPIC hardest.c

Теперь вернитесь в клиент и выполните следующие запросы:

box.schema.func.create('hardest', {language = "C"})
    box.schema.user.grant('guest', 'execute', 'function', 'hardest')
    box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
    capi_connection:call('hardest')

На этот раз C-функция выполняет три действия:

  1. найдет числовой идентификатор спейса capi_test путем вызова box_space_id_by_name();
  2. форматирует кортеж, используя другие функции msgpuck.h;
  3. вставит кортеж с помощью box_insert().

Предупреждение

char tuple[1024]; используется здесь просто в качестве быстрого способа ввода команды «выделить байтов с запасом». В серьезных программах разработчику следует обратить внимание на то, чтобы выделить достаточно места, которое будут использовать процедуры mp_encode.

Затем всё еще в клиенте выполните следующий запрос:

box.space.capi_test:select()

Результат должен выглядеть следующим образом:

tarantool> box.space.capi_test:select()
    ---
    - - [10000, 'String 2']
    ...

Это доказывает, что функция hardest() была успешно выполнена, но откуда взялись box_space_id_by_name() и box_insert()? Ответ: API для языка C.

read.c

Вернитесь в терминал, где были созданы программы easy.c, harder.c и hardest.c.

Создайте файл. Назовите его read.c. Запишите в него следующие 43 строки:

#include "module.h"
    #include <msgpuck.h>
    int read(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      char tuple_buf[1024];      /* where the raw MsgPack tuple will be stored */
      uint32_t space_id = box_space_id_by_name("capi_test", strlen("capi_test"));
      uint32_t index_id = 0;     /* The number of the space's first index */
      uint32_t key = 10000;      /* The key value that box_insert() used */
      mp_encode_array(tuple_buf, 0); /* clear */
      box_tuple_format_t *fmt = box_tuple_format_default();
      box_tuple_t *tuple = box_tuple_new(fmt, tuple_buf, tuple_buf+512);
      assert(tuple != NULL);
      char key_buf[16];          /* Pass key_buf = encoded key = 1000 */
      char *key_end = key_buf;
      key_end = mp_encode_array(key_end, 1);
      key_end = mp_encode_uint(key_end, key);
      assert(key_end < key_buf + sizeof(key_buf));
      /* Get the tuple. There's no box_select() but there's this. */
      int r = box_index_get(space_id, index_id, key_buf, key_end, &tuple);
      assert(r == 0);
      assert(tuple != NULL);
      /* Get each field of the tuple + display what you get. */
      int field_no;             /* The first field number is 0. */
      for (field_no = 0; field_no < 2; ++field_no)
      {
        const char *field = box_tuple_field(tuple, field_no);
        assert(field != NULL);
        assert(mp_typeof(*field) == MP_STR || mp_typeof(*field) == MP_UINT);
        if (mp_typeof(*field) == MP_UINT)
        {
          uint32_t uint_value = mp_decode_uint(&field);
          printf("uint value=%u.\n", uint_value);
        }
        else /* if (mp_typeof(*field) == MP_STR) */
        {
          const char *str_value;
          uint32_t str_value_length;
          str_value = mp_decode_str(&field, &str_value_length);
          printf("string value=%.*s.\n", str_value_length, str_value);
        }
      }
      return 0;
    }

Скомпилируйте программу, что создаст файл библиотеки под названием read.so:

$ gcc -shared -o read.so -fPIC read.c

Теперь вернитесь в клиент и выполните следующие запросы:

box.schema.func.create('read', {language = "C"})
    box.schema.user.grant('guest', 'execute', 'function', 'read')
    box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
    capi_connection:call('read')

На этот раз C-функция выполняет четыре действия:

  1. снова найдет числовой идентификатор спейса capi_test путем вызова box_space_id_by_name();
  2. форматирует ключ поиска = 10 000, используя другие функции msgpuck.h;
  3. получает кортеж с помощью box_index_get();
  4. проходит по полям каждого кортежа с помощью box_tuple_get(). а затем декодирует каждое поле в зависимости от его типа. В данном случае, поскольку мы получаем кортеж, который сами вставили с помощью hardest.c, мы знаем заранее, что его тип будет MP_UINT или MP_STR. Однако, весьма часто здесь употребляется оператор выбора case с одной опцией для каждого возможного типа.

В результате вызова capi_connection:call('read') должны получить:

tarantool> capi_connection:call('read')
    uint value=10000.
    string value=String 2.
    ---
    - []
    ...

Это доказывает, что функция read() была успешно выполнена. И снова важные функции, которые начинаются с boxbox_index_get() и box_tuple_field() — пришли из :ref:``API для языка C <index-c_api_reference>`.

write.c

Вернитесь в терминал, где были созданы программы easy.c, harder.c, hardest.c и read.c.

Создайте файл. Назовите его write.c. Запишите в него следующие 24 строки:

#include "module.h"
    #include <msgpuck.h>
    int write(box_function_ctx_t *ctx, const char *args, const char *args_end)
    {
      static const char *space = "capi_test";
      char tuple_buf[1024]; /* Must be big enough for mp_encode results */
      uint32_t space_id = box_space_id_by_name(space, strlen(space));
      if (space_id == BOX_ID_NIL) {
        return box_error_set(__FILE__, __LINE__, ER_PROC_C,
        "Can't find space %s", "capi_test");
      }
      char *tuple_end = tuple_buf;
      tuple_end = mp_encode_array(tuple_end, 2);
      tuple_end = mp_encode_uint(tuple_end, 1);
      tuple_end = mp_encode_uint(tuple_end, 22);
      box_txn_begin();
      if (box_replace(space_id, tuple_buf, tuple_end, NULL) != 0)
        return -1;
      box_txn_commit();
      fiber_sleep(0.001);
      struct tuple *tuple = box_tuple_new(box_tuple_format_default(),
                                          tuple_buf, tuple_end);
      return box_return_tuple(ctx, tuple);
    }

Скомпилируйте программу, что создаст файл библиотеки под названием write.so:

$ gcc -shared -o write.so -fPIC write.c

Теперь вернитесь в клиент и выполните следующие запросы:

box.schema.func.create('write', {language = "C"})
    box.schema.user.grant('guest', 'execute', 'function', 'write')
    box.schema.user.grant('guest', 'read,write', 'space', 'capi_test')
    capi_connection:call('write')

На этот раз C-функция выполняет шесть действий:

  1. снова найдет числовой идентификатор спейса capi_test путем вызова box_space_id_by_name();
  2. создает новый кортеж;
  3. начинает транзакцию;
  4. заменяет кортеж в box.space.capi_test
  5. заканчивает транзакцию;
  6. последняя строка заменяет цикл read.c — вместо получения и вывода каждого поля, использует функцию box_return_tuple(...) для возврата всего кортежа вызывающему клиенту, чтобы вывести его на экран.

В результате вызова capi_connection:call('write') должны получить:

tarantool> capi_connection:call('write')
    ---
    - [[1, 22]]
    ...

Это доказывает, что функция write() была успешно выполнена. И снова важные функции, которые начинаются с boxbox_txn_begin(), box_txn_commit() и box_return_tuple() — пришли из :ref:``API для языка C <index-c_api_reference>`.

Вывод: длинное описание всего API для языка C необходимо в силу весомых причин. Все функции можно вызвать из C-функций, которые вызываются из Lua. Таким образом, хранимые процедуры на языке C получают полный доступ к базе данных.

Очистка данных

  • Удалите все кортежы с функцией с помощью box.schema.func.drop.
  • Удалите спейс capi_test с помощью box.schema.capi_test:drop().
  • Удалите файлы с разрешением .c и .so, созданные для данного практического задания.

Пример из набора тестов

Скачайте исходный код Tarantool’а. Откройте поддиректорию test/box. Проверьте наличие файла под названием tuple_bench.test.lua и еще одного файла под названием tuple_bench.c. Изучите Lua-файл на предмет вызова функции в C-файле с использованием методов, описанных в данном практическом задании.

Вывод: некоторые тесты из стандартного набора используют хранимые процедуры на языке C, а они должны работать, поскольку мы не можем выпустить Tarantool, если он не прошел тестирование.