
Tarantool – это in-memory СУБД с открытым исходным кодом, разрабатываемая VK Tech. Существует два способа разработки приложений для Tarantool. Как и к большинству СУБД, к Tarantool можно подключаться из внешнего приложения по TCP/IP. С этой целью для многих популярных языков программирования (включая Go, Python, C#, С++, Java и др.) разработаны соответствующие коннекторы. Это – первый способ.
Кроме этого, Tarantool обладает замечательной особенностью: он позволяет запускать бизнес-логику на встроенном сервере приложений. В этом случае пользовательский код исполняется в одном адресном пространстве с данными, что обеспечивает высокое быстродействие. Это – второй способ.
Если мы ведем разработку для встроенного сервера приложени�� Tarantool, то выбор языков программирования более ограничен. Основным языком программирования, в этом случае, является Lua. Также, часть логики может быть реализована на C/C++. Но набор доступных языков расширяется. Например, с некоторого времени, поддерживается Rust. Помимо этого, Tarantool может выполнять Wasm-приложения. Скоро код библиотеки для разработки на Wasm будет открыт.
Tarantool – высокопроизводительная СУБД часто используемая для построения highload-систем. Какой из вариантов разработки позволяет достичь максимального быстродействия (которое в таких системах будет не лишним)? В настоящей статье я хотел бы рассмотреть возможные способы вызова C-кода из Lua/LuaJIT/Tarantool (в режиме сервера приложений) и то, как это может ускорить и без того быструю СУБД.
Итак, основным средством разработки для Tarantool является Lua. А именно, его реализация, поддерживающая возможность динамической компиляции – LuaJIT. Среди основных преимуществ Lua можно выделить следующие:
простота изучения (человеку с опытом разработки достаточно 15 минут (!), чтобы начать программировать);
высокая скорость разработки приложений (динамическая типизация, «утиная» типизация, функции первого класса, высокоуровневые библиотеки для решения большинства основных задач);
высокая производительность: Lua быстр сам по себе, а использование LuaJIT делает его недосягаемым для других скриптовых языков;
простота взаимодействия с кодом, написанным на C.
Дополнительные аргументы в пользу Lua рассмотрены здесь: Why choose Lua?
Lua покрывает большинство потребностей разработчика и, если вы разрабатываете приложение для Tarantool с нуля, то, чаще всего, Lua – оптимальный выбор. Lua хорош и быстр. Но что, если хочется еще быстрее?
Каким бы быстрым ни был динамический язык программирования, даже при наличии JIT ему зачастую сложно сравниться со статически-типизированным языком, обладающим полноценным AOT-компилятором. Особенно когда речь идет о C.
Очевидно, что общая производительность системы зависит не только от языка программирования. Большое влияние оказывает выбранная архитектура, а также применяемые структуры данных и алгоритмы. Говоря об оптимизации за счет разработки на C, будем исходить из предположения, что приложение спроектировано правильно, код написан оптимально, произведено его профилирование и установлено, что та или иная его часть, реализованная на Lua, является узким местом.
Заметим, что при желании, можно, также, вести разработку на C++. Чтобы вызвать код на C++ из сервера приложений Tarantool, достаточно сделать для него обертку на C. Кроме производительности, есть как минимум еще одна причина, по которой вы можете захотеть использовать логику написанную на С/C++ в сервере приложений Tarantool – наличие готовой библиотеки, реализующей необходимую функциональность.
Как обратиться к коду на C из сервера приложений Tarantool? Есть три основных способа:
Lua C API;
LuaJIT FFI;
хранимые процедуры/пользовательские функции.

Ниже мы рассмотрим эти способы подробнее.
Подготовка к запуску примеров
Независимо от того, какой способ вы выберите, для того, чтобы из кода на C работать с данными, хранящимися в базе, вам потребуется Tarantool C API.
Для запуска приведенных ниже примеров в системе должны быть установлен компилятор C/C++ (в примерах я использую GCC, но подойдет и любой другой) и Tarantool. Кроме этого, понадобятся Make и CMake. Если Tarantool установлен не из исходных кодов, то необходимо, также, установить пакеты разработчика.
Для Ubuntu пакеты разработчика Tarantool устанавливаются так:
sudo apt install tarantool-dev
Для RedHat-совместимых дистрибутивов:
sudo dnf install tarantool-devel
Реализации на LuaJIT можно сравнить и со стандартным Lua. Для этого потребуется установить интерпретатор Lua и пакеты разработчика для него.
В Ubuntu это можно сделать так:
sudo apt install lua5.3 sudo apt install liblua5.3-dev
В RedHat-совместимых дистрибутивах:
sudo dnf install lua sudo dnf config-manager --set-enabled powertools sudo dnf install lua-devel
Для примера вызова C-функции из SQL потребуется библиотека MsgPuck:
git clone https://github.com/tarantool/msgpuck.git cd msgpuck mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make sudo make install
Полный код примеров, рассмотренных в статье, можно найти на github:
git clone https://github.com/msiomkin/tnt-c-bench.git
Компиляция примеров с использованием GCC:
cd tnt-c-bench make
Для LuaJIT-версий внутри Makefile выполняются примерно следующее:
gcc -O3 \ -shared -o lib_name.so \ -fPIC \ -I/usr/include/tarantool \ -I/usr/local/include/tarantool src_file_name.c
Тестирование с замерами времени производилось на сервере следующей конфигурации:
16 CPU Intel Xeon Ice Lake 2 GHz (virtual cores)
64 Gb RAM
High-IOPS SSD (10 000 IOPS read, 5 000 IOPS write)
Все тесты запускались многократно, указанные ниже значения времени – усредненные. Замеры затраченного (процессорного) времени выполнялись с помощью стандартной функции times.
Примеры на Lua запускались на LuaJIT с включенным JIT-компилятором (jit.on()), с отключенным (jit.off() jit.flush()), а также на стандартном интерпретаторе Lua версии 5.3.4.
Lua C API
Lua C API – набор функций, который позволяет коду на C взаимодействовать с Lua. С одной стороны, Lua является встраиваемым языком – его можно использовать из C подобно библиотеке. С другой стороны, Lua является расширяемым языком – он может вызывать библиотеку написанную на C. В обоих случаях код на C использует один и тот же программный интерфейс для обращения к Lua – Lua C API.
Рассмотрим как регистрировать C-функции для последующего их вызова из Lua на примере задачи подсчета строк в файле. Сначала реализуем функцию на Lua.
Для работы с файлами в Tarantool существует собственный (неблокирующий) модуль fio, рекомендованный к использованию. Для примера мы будем использовать стандартный модуль Lua io, т.к. он позволяет нам написать код более похожий на тот, который будет в следующем примере на C. Функция подсчета строк будет выглядеть следующим образом:
local function count_lines(file_name) local buf_size = 65536 local f = io.input(file_name) local counter = 0 while true do local buf = f:read(buf_size) if buf == nil then break end local i = 0 while true do i = buf:find("\n", i + 1) if i == nil then break end counter = counter + 1 end end f:close() return counter end
Полный код примера находится в файле lines_luaj.lua.
Сгенерировать файл с большим количеством строк можно с помощью random_csv_luaс.lua.
Генерируем файл для теста, содержащий 10 млн строк (около 1,5 Гб):
tarantool random_csv_luac.lua 10000000
Запускаем пример:
tarantool lines_luaj.lua
Таким способом сгенерированный CSV-файл обрабатывается за 0,7-0,8 с.
Реализуем аналогичную функциональность на C.
#include <stdio.h> #include <string.h> static int count_lines(const char* filename) { FILE* file = fopen(filename, "r"); if (file == NULL) return -1; #define BUF_SIZE 65536 char buf[BUF_SIZE]; int counter = 0; for(;;) { ssize_t n = fread(buf, 1, BUF_SIZE, file); if (ferror(file)) { counter = -1; break; } char* p = buf; while (n > 0) { char* q = (char*)memchr(p, '\n', n); if (q == 0) break; counter++; q++; n -= (ssize_t)(q - p); p = q; } if (feof(file)) break; } fclose(file); return counter; }
Чтобы вызвать функцию используя Lua C API, нам потребуется сделать обертку. Добавляем следующие заголовочные файлы:
#include <lua.h> #include <lauxlib.h>
Lua C API использует стек для передачи параметров и получение результата. Функция-обертка выглядит следующим образом:
static int count_lines_L(lua_State* L) { const char* filename = lua_tostring(L, 1); /* читаем первый аргумент */ int n = count_lines(filename); /* подсчитываем количество строк */ lua_pushnumber(L, n); /* возвращаем количество строк */ return 1; /* один результат (возможен multireturn) */ }
Регистрируем функцию, как доступную для вызова через Lua C API:
/* массив функций для библиотеки */ static const struct luaL_Reg mylib[] = { /* в Lua функция будет называться count_lines */ { "count_lines", count_lines_L }, /* метка окончания массива регистрируемых функций */ { NULL, NULL } }; /* точка входа в Lua C библиотеку */ LUA_API int luaopen_lineslc(lua_State *L) { /* регистрируем функции библиотеки */ luaL_register(L, "lineslc", mylib); return 1; }
Полный код реализации на C будет выглядеть так: lines_luac.c.
Вызвать функцию из Tarantool-приложения довольно просто:
local mylib = require("lineslc") local n = mylib.count_lines("test.csv")
Полный код примера вызова реализации на C из Lua находится в файле lines_luac.lua.
Запускаем пример:
./lines_luac.lua
Сравним, также, c другими версиями - c отключенным JIT и на стандартном Lua:
tarantool lines_luaj_off.lua lua lines_luav.lua
Замеры времени на тестовом стенде:

Обработка на C происходит примерно в два раза быстрее, чем в первом примере (см. первые две строки на рис. 2). Последние два варианта еще медленнее.
Если посмотреть реализацию модуля io, то мы увидим, что он реализован на C (в частности, в примере выше f:read вызовет io_file_readlen, которая, в свою очередь, вызовет fread). Таким образом, мы сравниваем большое количество вызовов C-функций из Lua в цикле и один вызов C-функции с циклом внутри. Понятно, что в первом случае будет больше накладных расходов. Справедливо ли такое сравнение? Скорее – да. Так устроен Lua: большинство стандартных библиотек LuaJIT (и Lua) написаны на C. Поэтому каждый раз, когда мы просим сделать LuaJIT/Lua что-либо «полезное» мы, скорее всего, вызываем C-код.
Но что, если все-таки хочется сделать сравнительный бенчмарк C-кода и чистого Lua-кода без (C-вызовов)? Тогда можно реализовать, например, быструю сортировку.
Реализация на Lua:
local function partition(arr, left, right) local pivot_value = arr[right] local pivot = left - 1 for i = left, right - 1 do if arr[i] < pivot_value then pivot = pivot + 1 arr[i], arr[pivot] = arr[pivot], arr[i] end end pivot = pivot + 1 arr[pivot], arr[right] = arr[right], arr[pivot] return pivot end local function quick_sort(arr, left, right) if left > 0 and left < right then local pivot = partition(arr, left, right) quick_sort(arr, left, pivot - 1) quick_sort(arr, pivot + 1, right) end end
Проверяем байткод и видим, что в нем нет никаких вызовов функций, кроме наших собственных (т.е. C-вызовы не используются):
Proto #0
0000 FUNCV 2 0001 FNEW 0 0 ; <main.lua>:1 0002 FNEW 1 1 ; <main.lua>:18 0003 UCLO 0 => 0004 0004 RET0 0 1
Proto #1
0000 FUNCF 11 0001 TGETV 3 0 2 0002 SUBVN 4 1 0 ; 1 0003 MOV 5 1 0004 SUBVN 6 2 0 ; 1 0005 KSHORT 7 1 0006 FORI 5 => 0016 0007 TGETV 9 0 8 0008 ISGE 9 3 0009 JMP 9 => 0015 0010 ADDVN 4 4 0 ; 1 0011 TGETV 9 0 4 0012 TGETV 10 0 8 0013 TSETV 10 0 4 0014 TSETV 9 0 8 0015 FORL 5 => 0007 0016 ADDVN 4 4 0 ; 1 0017 TGETV 5 0 2 0018 TGETV 6 0 4 0019 TSETV 6 0 2 0020 TSETV 5 0 4 0021 RET1 4 2
Proto #2
0000 FUNCF 9 0001 KSHORT 3 0 0002 ISGE 3 1 0003 JMP 3 => 0021 0004 ISGE 1 2 0005 JMP 3 => 0021 0006 UGET 3 0 ; partition 0007 MOV 5 0 0008 MOV 6 1 0009 MOV 7 2 0010 CALL 3 2 4 0011 UGET 4 1 ; quick_sort 0012 MOV 6 0 0013 MOV 7 1 0014 SUBVN 8 3 0 ; 1 0015 CALL 4 1 4 0016 UGET 4 1 ; quick_sort 0017 MOV 6 0 0018 ADDVN 7 3 0 ; 1 0019 MOV 8 2 0020 CALL 4 1 4 0021 RET0 0 1
Полный код примера: qsort_luaj.lua.
Реализация на C:
#define SWAP_INT(a, b) \ { \ int tmp = a; \ a = b; \ b = tmp; \ } static int partition(int* arr, int left, int right) { int pivot_value = arr[right]; int pivot_pos = left - 1; for (int i = left; i < right; i++) if (arr[i] < pivot_value) { pivot_pos++; SWAP_INT(arr[i], arr[pivot_pos]); } pivot_pos++; SWAP_INT(arr[right], arr[pivot_pos]); return pivot_pos; } static void quick_sort(int* arr, int left, int right) { if (left < right) { int pivot_pos = partition(arr, left, right); quick_sort(arr, left, pivot_pos - 1); quick_sort(arr, pivot_pos + 1, right); } }
Полный код реализации быстрой сортировки на C: qsort_luac.c. Вызов из Lua: qsort_luac.lua.
Примеры можно запускать так:
tarantool qsort_luaj.lua 10000000 # LuaJIT tarantool qsort_luac.lua 10000000 # Lua C API tarantool qsort_luaj_off.lua 10000000 # LuaJIT с отключенным JIT lua qsort_luav.lua 10000000 # стандартный интерпретатор Lua tarantool qsort_lua.lua 50000000 # LuaJIT tarantool qsort_luac.lua 50000000 # Lua C API tarantool qsort_lua.lua 50000000 # LuaJIT с отключенным JIT lua qsort_luac.lua 50000000 # стандартный интерпретатор Lua
Замеры времени выполнения на тестовом стенде:


Все равно догнать реализацию на C не получается – вновь получаем разницу с LuaJIT примерно в два раза (см. первые две строки на рис. 3 и рис. 4).
Тем не менее LuaJIT очень хорош: исполнение кода стандартным интерпретатором Lua безнадежно отстает по времени (см. последнюю строку на рис. 3 и рис. 4).
В качестве примера библиотеки из реальной жизни, использующей Lua C API, можно обратиться к Tarantool SMTP client.
Плюсы Lua C:
Механизм интегрирован в стандартный Lua, описан в официальной документации и является наиболее универсальным. При желании библиотеку можно будет использовать не только в Tarantool, но и из любого Lua-приложения.
После добавления Lua C API обертки для функции на стороне C функцию можно «прозрачно» вызывать из программы на Lua. Дополнительная обертка на стороне Lua не требуется. Это позволяет в последующем использовать библиотеку Lua-разработчику не обладающему опытом разработки на C.
При необходимости внутри C-функции можно вызвать код на Lua (см. lua_pcall).
Поддерживается возможность возврата нескольких результатов из функции (multireturn), что привычно для Lua-разработчиков.
Минусы Lua C:
В случае работы с готовой C-библиотекой ее придется доработать – добавить ко всем используемым функциям Lua C API обертки;
Для передачи параметров функций используется Lua-стек – не самый быстрый способ передачи параметров.
LuaJIT FFI
LuaJIT FFI – интегрированный в LuaJIT механизм вызова внешних С-функций и использования C-структур данных из кода на Lua. LuaJIT FFI извлекает информацию для взаимодействия с библиотекой из стандартных C-объявлений. Поэтому нет необходимости дорабатывать саму библиотеку. Достаточно скопировать в Lua-скрипт определения нужных функций и типов.
Для доступа к C-структурам LuaJIT может генерировать код аналогичный тому, который выдает компилятор C, а передача параметров функций при работе через FFI может быть выполнена через C-стек или регистры процессора по ABI (Lua-стек не используется). Это должно положительно сказаться на быстродействии. Рассмотрим пример вызова C-кода через FFI и сравним время выполнения с версией на Lua C API.
Вызовем C-реализации подсчета строк в файле и быстрой сортировки через FFI. На этот раз код на C не должен содержать никаких оберток. Достаточно только реализации самой вызываемой функции: см. lines_ffi.c и qsort_ffi.c. Напротив, вызовы из Lua теперь будет несколько сложнее. Рассмотрим вызов через FFI на примере подсчета строк в файле.
Нам потребуется модуль ffi:
local ffi = require("ffi")
Добавляем в Lua-код C-определение функции:
ffi.cdef([[ int count_lines(const char* filename); ]])
Подгружаем C-библиотеку с реализацией функции быстрой сортировки:
local mylib = ffi.load("./linesf.so")
Вызываем подсчет строк через FFI:
local n = mylib.count_lines("test.csv")
Полный код примера: lines_ffi.lua.
Аналогичным образом можно вызвать и C-реализацию быстрой сортировки. Полный код примера: qsort_ffi.lua.
Запуск примеров:
tarantool lines_ffi.lua tarantool qsort_ffi.lua 10000000 tarantool qsort_ffi.lua 50000000
Замеры показывают такое же время, как и в примерах с Lua C API (см. рис. 2 – 4). Это ожидаемо, т.к. мы производим однократный вызов одних и тех же C-функций. Потенциальный выигрыш от использования FFI в этом случае лежит в пределах погрешности измерений.
Проверим, будет ли отличаться по времени большое количество вызовов функций. В качестве примера возьмем стандартную функцию isalpha и будем вызывать ее в цикле. Тем самым мы, во-первых, выполним большое количество вызовов. Во-вторых, дадим компилятору возможность разместить вызов на трассе.
Для вызова c использованием Lua C API нам вновь потребуется обертка – isalpha_luac.c.
Для вызова через FFI обертки не потребуется. А т.к. isalpha – стандартная функция, то и никакой собственной библиотеки реализовывать также не надо. Все делается на стороне LuaJIT:
local ffi = require("ffi") ffi.cdef([[ int isalpha(int ch); ]]) local C = ffi.C local counter = 0 local n = 100 * 1000 * 1000 for i = 1, n do local c = i % 100 if C.isalpha(c) ~= 0 then counter = counter + 1 end end
Полный код вызовов с использованием Lua C API и FFI: isalpa_luac.lua и isalpa_ffi.lua соответственно.
Запуск примеров:
tarantool isalpha_ffi.lua # FFI, LuaJIT tarantool isalpha_luac.lua # Lua C API, LuaJIT tarantool isalpha_luac_joff.lua # Lua C API, LuaJIT с отключенным JIT lua isalpha_luav.lua # Lua C API, Lua tarantool isalpha_ffi_joff.lua # FFI, LuaJIT с отключенным JIT
Замеры времени выполнения на тестовом стенде:

И вот тут уже разница очень заметна. Трассы в LuaJIT позволяют делать эффективные вызовы – разница с Lua C API почти на порядок (см. две первые строки на рис. 5).
Если посмотреть результирующий код вызова через FFI, то мы увидим, что JIT использовал регистры CPU для передачи параметров (отсюда и максимальная скорость):
... mov esi, 0x64 mov edi, ebp call 0x7f0941d5bd80 ; modulus operator mov edi, eax call 0x7f0941de93b7 ; isalpha test eax, eax jnz 0x7f7750c80020 ...
Сравните с кодом, который генерирует gcc для версии на C++:
#include <ctype.h> int main() { int counter = 0; for (int i = 0; i < 100000000; i++) { int c = i % 100; if (isalpha(c)) counter++; } return 0; }
... ; modulus operator logic is inlined mov eax, DWORD PTR [rbp-12] mov edi, eax call isalpha test eax, eax je .L3 ...
Предпоследние три строки, относящиеся непосредственно к вызову isalpha и получению результата совпадают.
На рис. 5 также можно увидеть и другие интересные факты. Для вызовов через Lua C API выбор режима jit.on/jit.off не критичен – время почти одинаковое (см. вторую и третью строки). А вот вызов через FFI без JIT работает крайне медленно (см. последнюю строку): инетпретатору приходится конвертировать аргументы в объекты cdata, затем значения из cdata помещать в регистры или C-стек. Для возвращения результата необходимо делать обратные преобразования. Также, объекты cdata должны обрабатываться GC. Все это требует дополнительного времени.
Здесь можно посмотреть примеры библиотек, использующих LuaJIT FFI: cron-parser, sqlparser.
Плюсы LuaJIT FFI:
Объем предварительной работы значительно сокращается: скопировать нужные определения из заголовочных файлов на C проще, чем писать Lua C API обертку для каждой функции.
Нет необходимости дорабатывать библиотеку на C.
Можно работать с библиотекой, даже если она заранее скомпилирована и нет доступа к исходникам. Достаточно взять описание функций из документации или заголовочных файлов.
Передача параметров функций может осуществляться по ABI (через регистры или C-стек), что является наиболее эффективным способом.
Минусы LuaJIT FFI:
Этот механизм интегрирован в LuaJIT, из стандартного Lua его можно использовать только с помощью сторонних библиотек.
Возрастает ответственность: разработчик на Lua теперь обязан уметь программировать и на C – неосторожное действие в Lua коде может вызвать ошибку сегментации (англ. segmentation fault). См. пример ниже.
FFI-вызовы из интерпретатора (с отключенным JIT или если вызов не на трассе) работают крайне медленно.
Внутри C-функции нельзя вызывать код на Lua.
Multireturn не поддерживается – из функции нельзя вернуть несколько результатов.
Пример неосторожного обращения с указателями при использовании FFI:
local ffi = require("ffi") ffi.cdef[[ size_t strlen(const char *s); ]] len = ffi.C.strlen(str) -- пара пара пам...

Хранимые процедуры / пользовательские функции
Как и многие СУБД, Tarantool позволяет использовать хранимые процедуры (англ. stored procedures). Хранимые процедуры создаются с помощью функции box.schema.func.create. После этого их можно вызывать как из встроенного сервера приложений так и из внешнего приложения.
Хранимые процедуры могут быть реализованы на Lua и C. При вызове хранимых процедур на C из встроенного сервера приложений используется механизм Lua C API, рассмотренный выше. Поэтому дополнительные замеры времени для этого варианта приводить не будем.
Вызов хранимых процедур из внешнего приложения лежит за рамками настоящей статьи. Отметим только, что замеры производительности в случае внешнего приложения могут отличаться в зависимости от выбранного языка программирования и коннектора.
Функции созданные через box.schema.func.create можно вызывать из SQL-запросов. В этом случае, их более правильно называть «пользовательские функции» (англ. user-defined functions). Рассмотрим пример вызова пользовательских функций из SQL-запросов и сравним скорость работы функций, реализованных на C и Lua.
Допустим, мы хотим разработать функцию для SQL, которая подсчитывает сумму цифр целого положительного числа. Начнем с варианта на Lua:
function digit_sum(n) local s = 0 while n > 0 do local r = n % 10 s = s + r n = (n - r) / 10 end return s end
Для того, чтобы Lua-функцию можно было вызвать из SQL ее необходимо создать в БД следующим образом:
box.schema.func.create("DIGIT_SUM", { language = "LUA", exports = { "SQL" }, param_list = { "unsigned" }, body = [[ function(n) local s = 0 while n > 0 do local r = n % 10 s = s + r n = (n - r) / 10 end return s end ]], returns = "unsigned", is_deterministic = true })
Подробнее о указанных во втором параметре опциях можно прочитать здесь.
Если мы хотим, чтобы скрипт можно было выполнить многократно, перед созданием функции необходимо обработать ситуацию, когда функция уже существует. Удалим функцию в таком случае:
box.schema.func.drop("DIGIT_SUM", { if_exists = true })
Далее необходимо задать права на запуск функции:
box.schema.role.grant("public", "execute", "function", "DIGIT_SUM")
Для проверки работы функции нам потребуется создать space (аналог таблиц в других СУБД), в который можно поместить случайные числа, подаваемые на вход функции:
test_space = box.schema.create_space("test", { format = { { name = "id", type = "unsigned" }, { name = "value", type = "unsigned" } }, temporary = true }) test_space:create_index("primary")
Заполняем space случайными числами:
local max_value = 2 ^ 32 for i = 1, n do local value = math.random(max_value) test_space:insert({ i, value }) end
Теперь все готово к запуску SQL-запроса, использующего созданную нами функцию. Для замера времени возьмем агрегирующий запрос:
SELECT COUNT(*) FROM "test" WHERE DIGIT_SUM("value") < 10
Т.к. сканировать space в Tarantool запрещено, чтобы запрос заработал добавим в него условие (фиктивное) по первичному индексу:
SELECT COUNT(*) FROM "test" WHERE "id" > 0 AND DIGIT_SUM("value") < 10
Для запуска SQL-запроса необходимо вызывать box.execute:
local res, err = box.execute([[ SELECT COUNT(*) FROM "test" WHERE "id" > 0 AND DIGIT_SUM("value") < 10 ]])
Полный код примера можно найти в файле digsum_lua.lua.
Рассмотрим реализацию на C.
Функция суммирования цифр полностью аналогична версии на Lua:
static uint64_t digit_sum(uint64_t n) { uint64_t s = 0; while (n > 0) { int r = n % 10; s += r; n = n / 10; } return s; }
Для того чтобы функцию можно было использовать в SQL-запросе, необходимо добавить обертку.
Нам потребуется заголовочный файл для работы с Tarantool C API:
#include <module.h>
Функция-обертка на C должна иметь следующие типы параметров и результата:
int digit_sum(box_function_ctx_t* ctx, const char* args, const char* args_end) { ... }
Функция принимает некий контекст (который Tarantool передает в нее автоматически), а также указатели на начало и конец буфера, содержащего передаваемые аргументы. В качестве результата функция возвращает код (0 – в случае успеха).
Буфер, на который указывает второй параметр содержит аргументы вызова в виде MessagePack-массива. Для работы с MessagePack Tarantool использует библиотеку MsgPuck. Заголовочный файл MsgPuck:
#include <msgpuck.h>
Для десериализации аргументов сначала необходимо считать флаг массива и количество его элементов:
mp_decode_array(&args);
Tarantool проверяет количество элементов при вызове функций из SQL, поэтому дополнительная проверка не обязательна. Однако, типы аргументов не проверяются, поэтому добавим проверку типа первого (и единственного) аргумента:
enum mp_type value_type = mp_typeof(*args); if (value_type != MP_UINT) { return box_error_raise(-1, "Invalid argument type, expecting an unsigned integer"); }
Далее считываем значение аргумента:
uint64_t n = mp_decode_uint(&args);
Вызываем функцию суммирования цифр, которую мы описали выше:
uint64_t s = digit_sum(n);
Результат необходимо снова поместить в MessagePack-массив (который мы потом преобразуем в кортеж). Создаем буфер размером 9 байт (1 байт под длину массива + 8 байт под результат):
char res_buf[1 + sizeof(uint64_t)];

Указатель на конец буфера:
char* res_end = res_buf;
Записываем размер массива, который равен количеству возвращаемых значений, т.е. единице:
res_end = mp_encode_array(res_end, 1);
Записываем результат:
res_end = mp_encode_uint(res_end, s);
Преобразуем массив в кортеж (англ. tuple – аналог записи в других СУБД):
struct tuple* res = box_tuple_new(box_tuple_format_default(), res_buf, res_end);
Помещаем кортеж в контекст и возвращаем результат этой операции в качестве кода результата функции:
return box_return_tuple(ctx, res);
Полный пример: digsum_udf.c.
Код вызова пользовательской C-функции схож с кодом вызова вызова пользовательской Lua-функции, но имеет некоторые отличия. При регистрации функции в БД теперь не нужно указывать тело функции:
box.schema.func.create("mylib.tnt_digit_sum", { language = "C", exports = { "SQL" }, param_list = { "unsigned" }, returns = "unsigned", is_deterministic = true })
Наименование функции в SQL-запросе должно содержать имя библиотеки:
SELECT COUNT(*) FROM "test" WHERE "id" > 0 AND "myudf.digit_sum"("value") < 10
В остальном код на Lua не имеет отличий от предыдущего примера. Полный код: digsum_c.lua.
Запуск примеров:
tarantool digsum_lua.lua tarantool digsum_c.lua
Результаты на 10 млн. строк: 12-13 с. для версии на Lua против 4 с. для версии на C. Разница – в три раза.

Здесь можно найти примеры использования Tarantool C API для обращения к БД из хранимых процедур / пользовательских функций.
Плюсы использования хранимых процедур / пользовательских функций:
Можно вызывать из SQL-запросов.
Можно вызывать из внешнего приложения.
Минусы использования хранимых процедур / пользовательских функций:
В случае вызова непосредственно из Lua-приложения встроенного сервера используется Lua C API. Такой способ не лишен указанных выше недостатков Lua C API, при этом требует дополнительного кода на стороне Lua (вызовов
box.schema.func.createи т.д.).
Заключение
Мы рассмотрели основные способы вызова кода на C из Lua/LuaJIT/Tarantool. Это может быть полезным как в целях оптимизации, так и в целях расширения функциональности приложения с помощью внешних библиотек. Tarantool за счет своих архитектурных особенностей обеспечивает высокую скорость работы приложений, разработанных на его основе. Если же вы хотите достичь экстремальной производительности, вы можете реализовать часть своего приложения на C/C++.