В данном докладе я расскажу о том, как Lua помогает расширять функционал Rspamd, позволяя людям без особых знаний С писать эффективные правила фильтрации спама. Также будут рассмотрены особенности внедрения Lua в C код и основные приемы, применяемые при написании API для Lua приложений. Отдельное внимание будет уделено документации к Lua API, которая является одним из необходимых компонентов для opensource приложения.
Кроме этого, отдельная часть доклада посвящена анализу производительности Lua: использованию LuaJIT, сравнению вызовов C функций через FFI с традиционным вызовом, оптимизации строковых операций и таблиц в Lua.
В заключение будут рассмотрены некоторые открытые вопросы: будущее языка, наличие нескольких диалектов, статический анализ Lua стека, а также вопросы безопасности при JIT компиляции.
Практика совместного использования Lua и C в opensource спам-фильтре Rspamd / Всеволод Стахов (University of Cambridge, Mimecast)
1.
2. Что такое Rspamd
• Система фильтрации спама с ориентацией на производительность
• OpenSource проект
• Плагины и правила написаны на языке Lua
• Ядро написано на plain (old) C
6. Что написано на Lua
• Правила, а также комбинации регулярных выражений
• Большая часть плагинов:
• Проверка DNS списков
• Взаимодействие с Redis (репутация, “серые” списки, динамические настройки, список ответов,
динамические лимиты)
• Работа с внешними сервисами (антивирусы, запись в ClickHouse итд)
• Контент-фильтрация
• Работа с нейросетями и классификацией текстов
• Работа с SpamAssassin правилами
14. Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
15. Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
• Скорость работы (Python, Guile, TCL)
16. Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
• Скорость работы (Python, Guile, TCL)
• Скорость переключения между C и встраиваемым языком
18. Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
19. Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
20. Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
• Текущий подход к разработке: писать Lua биндинги на Си, а основную
логику на Lua
21. Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
• Текущий подход к разработке: писать Lua биндинги на Си, а основную
логику на Lua
• Упростилось написание утилит командной строки
24. Обучение Lua
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
25. Обучение Lua
• Плагин опроса внешнего сервиса DCC:
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
26. Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
27. Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
28. Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
• еще 30 минут заняла адаптация биндингов на C
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
29. Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
• еще 30 минут заняла адаптация биндингов на C
• аналог на перле - больше 1k строк кода
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
32. Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
33. Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
34. Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
• Стандартная библиотека содержит только необходимый минимум
функций
35. Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
• Стандартная библиотека содержит только необходимый минимум
функций
• Динамическая строгая типизация
38. Синтаксис Lua
Основные элементы
• Переменные: local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
39. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
40. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
41. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
42. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
43. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
44. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
45. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
• Функции:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
46. Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
• Функции:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
local function something(task) -- Normal definition
local cb = function(data) -- Functions can be nested
…
end
end
50. Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
51. Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
• Ключи - строки и числа:
52. Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
local t = {
1, -- 1
[3] = {1,2},
test = function() end,
['spaces in key'] = 'abc',
[1.2] = 3,
[-1] = 4,
}
• Ключи - строки и числа:
53. Таблицы в Lua
table
local table = {
a = 1,
b = 2,
[1] = 3,
[2] = function() end,
[0] = true
}
3
function() end
true
1
2
1
2
0
‘a’
‘b’
Массив Хеш таблица
!
54. Таблицы в Lua
Итерация
local t = {
a = 1,
b = 2,
[1] = 3,
[2] = function() end,
[0] = true
}
Массив
Таблица
целиком
1..2
55. Метатаблицы
Когда просто таблиц недостаточно
• Задают общие свойства для других таблиц
• Примерно соответствуют Prototype в JavaScript
• Позволяют задавать функции-методы для таблиц (через __newindex или __index)
• Могут также задавать операторы над таблицами (например, через __eq или
__sum)
• Можно задавать метатаблицы для стандартных Lua типов (например, строк)
• Используются в C API для определения методов для типа userdata
59. Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
60. Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
• Могут быть аргументами и
возвращаться из функций:
61. Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
local function f(cb) -- accepts callback function
return function(args) -- accepts some args
return cb(args) -- return callback function from args (decorator like)
end
end
• Могут быть аргументами и
возвращаться из функций:
62. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
63. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
64. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
65. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
66. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Можно возвращать функцию
67. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Функция-замыкание
Можно возвращать функцию
68. Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Функция-замыкание
Непосредственно вызов
Можно возвращать функцию
69. Функции в Lua
• Позволяют писать в функциональном стиле (библиотека lua-fun)
• Замыкания работают также для C функций
• Передача по ссылке несколько необычна, но эффективна
• Время жизни замыкания ассоциируется с переменной, в которой оно
хранится
71. Строки в Lua
Передача строки из C
Глобальный хеш
строк
s1
s2
s3
lua_pushstring (L, "hello");
hello
Копия
Хеш
hash(hello)
Поиск
72. Строки в Lua
Передача строки из C
Глобальный хеш
строк
s1
s2
s3
lua_pushstring (L, "hello");
hello
Вставка
hello
73. Строки в Lua
• Строки в Lua неизменяемы
• Сравнение строк - просто сравнение указателей: O(1)
• Создание и передача строк - дорогая операция
• Если нужно делать строку из кусков, то нужно использовать таблицу
и table.concat
82. Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
83. Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
84. Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
85. Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
• Крайне неочевидный код
86. Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
• Крайне неочевидный код
• Есть ряд сложных моментов (итерация по таблице)
87. Пример из практики
Итерация по таблице
lua_pushvalue (L, i); /* Push table on top */
lua_pushnil (L); /* Push nil to start iterate */
while (lua_next (L, -2)) {
lua_pushvalue (L, -2); /* Copy key as it is special */
key = luaL_checkstring (L, -1);
value = luaL_checkstring (L, -2);
lua_pop (L, 2); /* Remove key and value leaving original key */
}
lua_pop (L, 1); /* Remove table */
90. Пример из практики
Итерация по таблице
table -2
nil -1
while (lua_next (L, -2))
table -3
key -2
value -1
Предыдущий ключ
Новый ключ
91. Пример из практики
Итерация по таблице
lua_pushvalue (L, -2);
key = luaL_checkstring (L, -1);
value = luaL_checkstring (L, -2);
table
-2
key
-1
value
key copy
-3
-4
92. Пример из практики
Итерация по таблице
lua_pop (L, 2);
table
-2
key
-1
value
key copy
-3
-4
table
key -1
-2
while (lua_next (L, -2))
Используется для следующей итерации
94. Плюсы FFI
• Очень быстро работают для простых типов (десяток циклов)
• Поддерживают все конструкции C99
• Поддерживают типы-обертки (boxed types), например 64-х битные
целые
• Проще поддерживать
96. Пример из практики
Оптимизация узких мест
local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Проверка
LuaJIT
97. local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Определение С функций
Пример из практики
Оптимизация узких мест
98. local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
FFI call
Userdata to C pointer
bool -> int
Пример из практики
Оптимизация узких мест
99. local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Plain call
Пример из практики
Оптимизация узких мест
103. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
104. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
105. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
106. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
107. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
• Не всегда быстрее
108. Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
• Не всегда быстрее
• Очень сложно сделать sandbox