Tarantool CE/EE Documentation portal logo
Помощь
Обновлена 15 сентября 2026 г. в 08:55

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

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

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

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

(1) easy.c – выводит "hello world"; (2) harder.c – декодирует переданное значение параметра; (3) hardest.c – использует C API для выполнения вставки в БД; (4) read.c – использует C API для выполнения выборки из БД; (5) write.c – использует C API для выполнения замены в БД.

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

Подготовка

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

  • Tarantool 2.1 или более поздней версии
  • Компилятор gcc, подойдет любая современная версия
  • module.h и файлы, подключаемые в нём (#included)
  • msgpuck.h
  • libmsgpuck.a (только для некоторых последних версий msgpuck)

Файл module.h есть в системе, если Tarantool был установлен из исходных файлов. В противном случае, следует установить пакет Tarantool "developer". Например, на Ubuntu введите команду:

$ sudo apt-get install tarantool-dev

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

$ dnf -y install tarantool-devel

Файл msgpuck.h будет доступен, если Tarantool был установлен из исходных файлов. В противном случае необходимо установить пакет "msgpuck" из https://github.com/tarantool/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.ceasy2(). Она практически совпадает с функцией easy(), но есть небольшое отличие: если имя файла не совпадет с именем функции, нужно будет указать {имя-файла}.{имя-функции}.

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. Передача и возврат данных всегда осуществляются в этом формате, поэтому для уверенного владения C API необходимо ознакомиться с msgpuck.

Однако, пока достаточно понимать, что функция 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 = 1field_count = 3val=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().

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

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 = 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) форматирование ключа поиска = 10000 с помощью дополнительных функций 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() – пришли из API для языка C.

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]; /* Должен быть достаточно большим, чтобы вместить результат mp_encode */  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() – пришли из API для языка C.

Вывод: длинное описание всего 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, если он не прошел тестирование.