Dicas Principais: Identificando Conceitos Únicos e Armadilhas em Lua
API7.ai
October 12, 2022
No artigo anterior, aprendemos sobre as funções da biblioteca relacionadas a tabelas no LuaJIT. Além dessas funções comuns, hoje vou apresentar alguns conceitos únicos ou menos comuns do Lua e armadilhas comuns do Lua no OpenResty.
Tabela Fraca
Primeiro, temos a tabela fraca, um conceito único no Lua, que está relacionado à coleta de lixo. Como outras linguagens de alto nível, o Lua possui coleta de lixo automática, você não precisa se preocupar com a implementação e nem precisa fazer a coleta de lixo explicitamente. O coletor de lixo irá automaticamente coletar o espaço que não está sendo referenciado.
Mas a simples contagem de referências não é suficiente, e às vezes precisamos de um mecanismo mais flexível. Por exemplo, se inserirmos um objeto Lua Foo
(table ou função) na tabela tb
, isso cria uma referência a esse objeto Foo
. Mesmo que não haja outras referências a Foo
, a referência a ele em tb
sempre existirá, então não há como o coletor de lixo recuperar a memória ocupada por Foo
. Nesse ponto, temos apenas duas opções.
- Uma é liberar
Foo
manualmente. - A segunda é mantê-lo residente na memória.
Por exemplo, o seguinte código.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
print(#tb) -- 2
collectgarbage()
print(#tb) -- 2
table.remove(tb, 1)
print(#tb) -- 1
No entanto, acredito que você não queira manter a memória ocupada por objetos que não está usando, especialmente porque o LuaJIT tem um limite de 2G de memória. O momento de liberar manualmente não é fácil e adiciona complexidade ao seu código.
Então é hora da tabela fraca entrar em ação. Olhe para o nome, tabela fraca. Primeiro, é uma tabela, e então todos os elementos nessa tabela são referências fracas. O conceito é sempre abstrato, então vamos começar olhando para um código ligeiramente modificado.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "v"})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 0
'
Como você pode ver, os objetos que não estão sendo usados são liberados. O mais importante aqui é a seguinte linha de código.
setmetatable(tb, {__mode = "v"})
Parece familiar? Não é essa a operação de uma metatable? Sim, uma tabela é uma tabela fraca quando possui um campo __mode
em sua metatable.
- Se o valor de
__mode
fork
, a chave da tabela é uma referência fraca. - Se o valor de
__mode
forv
, então o valor da tabela é uma referência fraca. - Claro, você também pode configurá-lo como
kv
, indicando que tanto as chaves quanto os valores dessa tabela são referências fracas.
Qualquer uma dessas três tabelas fracas terá seu par chave-valor inteiro recuperado assim que sua chave ou valor for recuperado.
No exemplo de código acima, o valor de __mode
é v
, tb
é um array, e o valor do array é a tabela e o objeto de função, para que possa ser reciclado automaticamente. No entanto, se você mudar o valor de __mode
para k
, ele não será liberado, por exemplo, se você olhar para o seguinte código.
$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "k"})
print(#tb) -- 2
collectgarbage()
print(#tb) -- 2
'
Só demonstramos tabelas fracas onde o valor é uma referência fraca, ou seja, tabelas fracas do tipo array. Naturalmente, você também pode construir uma tabela fraca do tipo hash table usando um objeto como chave, por exemplo, como a seguir.
$ resty -e 'local tb = {}
tb[{color = red}] = "red"
local fc = function() print("func") end
tb[fc] = "func"
fc = nil
setmetatable(tb, {__mode = "k"})
for k,v in pairs(tb) do
print(v)
end
collectgarbage()
print("----------")
for k,v in pairs(tb) do
print(v)
end
'
Após chamar manualmente collectgarbage()
para forçar a coleta de lixo, todos os elementos na tabela tb
terão sido liberados. Claro, no código real, não precisamos chamar collectgarbage()
manualmente, ele será executado automaticamente em segundo plano, e não precisamos nos preocupar com isso.
No entanto, como mencionamos a função collectgarbage()
, vou falar mais algumas palavras sobre ela. Esta função pode receber várias opções diferentes e tem como padrão collect
, que é uma coleta de lixo completa. Outra útil é count
, que retorna a quantidade de espaço de memória ocupado pelo Lua. Essa estatística é útil para ver se há um vazamento de memória e nos lembra de não nos aproximarmos do limite superior de 2G.
O código relacionado a tabelas fracas é mais complicado de escrever na prática, menos fácil de entender e, consequentemente, tem mais bugs ocultos. Não precisa se apressar. Mais tarde, vou apresentar um projeto de código aberto, usando tabelas fracas que causaram problemas de vazamento de memória.
Closure e upvalue
Passando para closures e upvalues, como enfatizei anteriormente, todos os valores são cidadãos de primeira classe no Lua, assim como as funções incluídas. Isso significa que as funções podem ser armazenadas em variáveis, passadas como argumentos e retornadas como valores de outra função. Por exemplo, este código de exemplo aparece na tabela fraca acima.
tb[2] = function() print("func") end
É uma função anônima que é armazenada como o valor de uma tabela.
No Lua, a definição das duas funções no código a seguir é equivalente. No entanto, observe que a última atribui uma função a uma variável, um método que usamos frequentemente.
local function foo() print("foo") end
local foo = fuction() print("foo") end
Além disso, o Lua suporta escrever uma função dentro de outra função, ou seja, funções aninhadas, como no seguinte exemplo de código.
$ resty -e '
local function foo()
local i = 1
local function bar()
i = i + 1
print(i)
end
return bar
end
local fn = foo()
print(fn()) -- 2
'
Você pode ver que a função bar
pode ler a variável local i
dentro da função foo
e modificar seu valor, mesmo que a variável não seja definida dentro de bar
. Essa característica é chamada de escopo léxico.
Essas características do Lua são a base para closures. Um closure é simplesmente uma função que acessa uma variável no escopo léxico de outra função.
Por definição, todas as funções no Lua são, na verdade, closures, mesmo que você não as aninhe. Isso ocorre porque o compilador do Lua pega fora do script Lua e o envolve com outra camada da função principal. Por exemplo, as seguintes linhas simples de código.
local foo, bar
local function fn()
foo = 1
bar = 2
end
Após a compilação, ficará assim.
function main(...)
local foo, bar
local function fn()
foo = 1
bar = 2
end
end
E a função fn
captura duas variáveis locais da função principal, então ela também é um closure.
Claro, sabemos que o conceito de closures existe em muitas linguagens, e não é exclusivo do Lua, então você pode comparar e contrastar para entender melhor. Somente quando você entender closures poderá entender o que vamos dizer sobre upvalue
.
upvalue
é um conceito que é exclusivo do Lua, que é a variável fora do escopo léxico capturada no closure. Vamos continuar com o código acima.
local foo, bar
local function fn()
foo = 1
bar = 2
end
Você pode ver que a função fn
captura duas variáveis locais, foo
e bar
, que não estão em seu próprio escopo léxico e que essas duas variáveis são, na verdade, o upvalue
da função fn
.
Armadilhas Comuns
Após introduzir alguns conceitos no Lua, vou falar sobre as armadilhas relacionadas ao Lua que encontrei no desenvolvimento do OpenResty.
Na seção anterior, mencionamos algumas das diferenças entre o Lua e outras linguagens de desenvolvimento, como o índice começando em 1
, variáveis globais por padrão, etc. No desenvolvimento de código real do OpenResty, encontraremos mais problemas relacionados ao Lua e ao LuaJIT, e vou falar sobre alguns dos mais comuns abaixo.
Aqui está um lembrete: mesmo que você conheça todas as armadilhas, inevitavelmente terá que passar por elas sozinho para se impressionar. A diferença, claro, é que você será capaz de sair do buraco e encontrar o cerne do problema de uma maneira muito melhor.
O índice começa em 0 ou 1?
A primeira armadilha é que o índice do Lua começa em 1
, como mencionamos repetidamente antes.
Mas tenho que dizer que isso não é toda a verdade. Porque no LuaJIT, arrays criados com ffi.new
são indexados a partir de 0
novamente:
local buf = ffi_new("char[?]", 128)
Então, se você quiser acessar o buf
cdata
no código acima, lembre-se de que o índice começa em 0
, não 1
. Certifique-se de prestar atenção especial a este lugar quando usar FFI para interagir com C.
Correspondência de Padrões Regulares
A segunda armadilha é o problema de correspondência de padrões regulares, e há dois conjuntos de métodos de correspondência de strings em paralelo no OpenResty: a biblioteca de strings do Lua e a API ngx.re.*
do OpenResty.
A correspondência de padrões regulares do Lua é em seu formato único e é escrita de forma diferente do PCRE. Aqui está um exemplo simples.
resty -e 'print(string.match("foo 123 bar", "%d%d%d"))' — 123
Este código extrai a parte numérica da string, e você notará que é completamente diferente das expressões regulares que conhecemos. A biblioteca de correspondência de padrões do Lua é cara para manter e de baixo desempenho - o JIT não pode otimizá-la, e os padrões que foram compilados uma vez não são armazenados em cache.
Então, quando você usa a biblioteca de strings embutida do Lua para find
, match
, etc., não hesite em usar o ngx.re
do OpenResty se precisar de algo como um regular. Ao procurar por uma string fixa, consideramos usar o modo simples para chamar a biblioteca de strings.
Aqui está uma sugestão: No OpenResty, sempre priorizamos a API do OpenResty, depois a API do LuaJIT, e usamos as bibliotecas do Lua com cautela.
A codificação JSON não distingue entre array e dict
A terceira armadilha é que a codificação JSON não distingue entre array e dict; como o Lua tem apenas uma estrutura de dados, table, quando o JSON codifica uma tabela vazia, não há como determinar se é um array ou um dicionário.
resty -e 'local cjson = require "cjson"
local t = {}
print(cjson.encode(t))
'
Por exemplo, o código acima produz {}
, o que mostra que a biblioteca cjson
do OpenResty codifica uma tabela vazia como um dicionário por padrão. Claro, podemos mudar esse padrão global usando a função encode_empty_table_as_object
.
resty -e 'local cjson = require "cjson"
cjson.encode_empty_table_as_object(false)
local t = {}
print(cjson.encode(t))
'
Desta vez, a tabela vazia é codificada como um array []
.
No entanto, essa configuração global tem um impacto significativo, então podemos especificar as regras de codificação para uma tabela específica? A resposta é naturalmente sim, e há duas maneiras de fazer isso.
A primeira maneira é atribuir o userdata
cjson.empty_array
à tabela especificada, para que ela seja tratada como um array vazio quando codificada em JSON.
$ resty -e 'local cjson = require "cjson"
local t = cjson.empty_array
print(cjson.encode(t))
'
No entanto, às vezes não temos certeza se a tabela especificada está sempre vazia. Queremos codificá-la como um array quando estiver vazia, então usamos a função cjson.empty_array_mt
, que é nosso segundo método.
Ela marcará a tabela especificada e a codificará como um array quando a tabela estiver vazia. Como você pode ver pelo nome cjson.empty_array_mt
, ela é configurada usando uma metatable
, como na seguinte operação de código.
$ resty -e 'local cjson = require "cjson"
local t = {}
setmetatable(t, cjson.empty_array_mt)
print(cjson.encode(t))
t = {123}
print(cjson.encode(t))
'
Limitação no número de variáveis
Vamos olhar para a quarta armadilha, o limite no número de variáveis. O Lua tem um limite superior no número de variáveis locais e no número de upvalue
s em uma função, como você pode ver no código-fonte do Lua.
/*
@@ LUAI_MAXVARS é o número máximo de variáveis locais por função
@* (deve ser menor que 250).
*/
#define LUAI_MAXVARS 200
/*
@@ LUAI_MAXUPVALUES é o número máximo de upvalues por função
@* (deve ser menor que 250).
*/
#define LUAI_MAXUPVALUES 60
Esses dois limites são codificados como 200
e 60
, respectivamente, e embora você possa modificar manualmente o código-fonte para ajustar esses dois valores, eles só podem ser definidos para um máximo de 250
.
Geralmente, não excedemos esse limite. Ainda assim, ao escrever código OpenResty, você deve ter cuidado para não abusar de variáveis locais e upvalue
s, mas usar do ... end
o máximo possível para reduzir o número de variáveis locais e upvalue
s.
Por exemplo, vamos olhar para o seguinte pseudo-código.
local re_find = ngx.re.find
function foo() ... end
function bar() ... end
function fn() ... end
Se apenas a função foo
usa re_find
, então podemos modificá-la da seguinte forma:
do
local re_find = ngx.re.find
function foo() ... end
end
function bar() ... end
function fn() ... end
Resumo
Do ponto de vista de "fazer mais perguntas", de onde vem o limite de 250
no Lua? Essa é nossa questão de reflexão de hoje. Você é bem-vindo a deixar seus comentários e compartilhar este artigo com seus colegas e amigos. Vamos nos comunicar e melhorar juntos.