Задача:
Есть некий Lua модуль. Нужно сделать все его поля доступными только для
чтения. При попытке добавить в модуль новое поле или изменить
существующее генерируется ошибка или выводится предупреждение.
Решение:
Как сказал кто-то умный, любая сложная задача имеет простое, очевидное
и неправильное решение. С него и начнем.
Очевидно, нам нужно установить поля __index и __newindex в метатаблице
модуля. Создадим модуль, содержащий, например, одно строковое поле и
одну функцию (файл Module1.lua):
-- сохраняем ссылку на глобальное окружение
local base = _G
module('Module1')
-- поле
field1 = 'test1'
-- функция
function fun(param)
base.print('Module1 fun call', param)
end
Тест для проверки работы модуля (файл module_test.lua):
require('Module1')
print('------Test Module1------')
print(Module1.field1)
Module1.fun()
print('Try to set new value to Module1.field1')
Module1.field1 = 'zzz'
print(Module1.field1)
print('------End Module1 test------')
В консоли мы должны увидеть:
------Test Module1------
test1
Module1 fun call nil
Try to set new value to Module1.field1
zzz
------End Module1 test------
Установим в модуле метатаблицу. Для этого в модуле создадим функцию setNewMetatable():
function setNewMetatable()
local meta = {}
meta.__index = function(t, k)
base.print('Module1 __index', k)
return _M[k]
end
meta.__newindex = function(t, k, v)
base.print('Module1 __newindex', k, v)
-- ничего не делаем
end
base.setmetatable(_M, meta)
end
Эта функция должна быть вызвана в конце загрузки модуля, иначе после того, как эта функция выполнится, модуль не сможет добавить в себя требуемые поля. Убедимся в этом. Вызовем функцию в начале загрузки модуля. Результат не впечатляет:
Module1 __newindex field1 test1
Module1 __newindex fun function: 0035F438
------Test Module1------
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
Module1 __index field1
lua.exe: C stack overflow
stack traceback:
[C]: in function 'print'
.\Module1.lua:8: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
...
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
.\Module1.lua:9: in function <.\Module1.lua:7>
module_test.lua:4: in main chunk
Два первых вызова это понятно что - создание полей field1 и
fun во
время загрузки модуля. Далее в строке 4 файла module_test.lua мы
обращаемся к полю field1. Попадаем в функцию __index метатаблицы
модуля, которая обращается к модулю посредством таблицы _M, у которой
установлена метатаблица с функцией __index, которая... И так до "C
stack overflow". После этого теста стали понятны две вещи:
1. Вызов setNewMetatable() должен идти в конце загрузки модуля,
поскольку поля таблицы смогут быть проинициализированы только в том
случае, если не установлена метатаблица с пустой функцией __newindex.
2. В методе __index обращаться к модулю вызовом _M[k] нельзя, поскольку
это вызывает циклическую зависимость.
Исправим выявленные недостатки. Функция __index теперь использует вызов
rawget(), при котором обращения к метатаблице не происходит:
meta.__index = function(t, k)
base.print('Module1 __index', k)
return base.rawget(_M, k)
end
Вызов setNewMetatable() помещаем в конец загрузки модуля. Проверяем:
------Test Module1------
test1
Module1 fun call nil
Try to set new value to Module1.field1
zzz
------End Module1 test------
Упс! Ничего не произошло :(. Пробуем разобраться. Открываем книжку Иерусалимского Programming in Lua, Second Edition на странице 123. Там обнаруживаем, что метаметоды __index и __newindex вызываются только в случае обращения к отсутствующим полям таблицы. Теперь понятно, почему не произошло вызова функции __newindex при присвоении нового значения полю field1 - это поле уже существует! Значит, нужно сделать следующее - все данные хранить не в самом модуле, а в какой-нибудь специальной таблице внутри него, а через функции _index и __newindex метатаблицы модуля обращаться к этой специальной таблице. Пробуем (файл Module1.lua):
-- сохраняем ссылку на глобальное окружение
local base = _G
module('Module1')
-- помещаем данные в специальную таблицу
local privateData = {
-- поле
filed1 = 'test1',
-- функция
fun = function (param)
base.print('Module1 fun call', param)
end,
}
function setNewMetatable()
local meta = {}
meta.__index = function(t, k)
base.print('Module1 __index', k)
-- обращаемся к специальной таблице
return privateData[k]
end
meta.__newindex = function(t, k, v)
base.print('Module1 __newindex', k, v)
end
base.setmetatable(_M, meta)
end
setNewMetatable()
Запускаем тест:
------Test Module1------
Module1 __index field1
test1
Module1 __index fun
Module1 fun call nil
Try to set new value to Module1.field1
Module1 __newindex field1 zzz
Module1 __index field1
test1
------End Module1 test------
В общем, это то, что нам нужно. Теперь нужно закрепить результат, и размножить его на другие модули, которые тоже могут захотеть быть read-only. Модули должны быть в традиционной форме, то есть пользователь не должен сам помещать данные в специальную таблицу, пусть за него все это сделает программа. Итак, Module1 выглядит так, как он выглядел в самом начале:
-- сохраняем ссылку на глобальное окружение
local base = _G
module('Module1')
-- поле
field1 = 'test1'
-- функция
function fun(param)
base.print('Module1 fun call', param)
end
Создадим модуль ModuleLocker, который будет делать для модулей всю работу по защите их данных от изменения (файл ModuleLocker.lua):
-- package.seeall устанавливает у модуля метатаблицу с полем __index,
-- ссылающемся на глобальное окружение
module('ModuleLocker', package.seeall)
function lock(module)
-- убираем из модуля все поля,
-- чтобы вызывались методы __index и __newindex метатаблицы
-- данные сохраняем в таблицу t
local t = {}
for k, v in pairs(module) do
print(k, v)
t[k] = v
-- удаляем данные из модуля
rawset(module, k, nil)
end
-- создаем в модуле таблицу с защищенными данными
rawset(module, 'privateData_', t)
local meta = {
__index = function(t, k)
-- возвращаем данные из потайной таблицы
return t.privateData_[k]
end,
__newindex = function(t, k, v)
-- генерируем ошибку
error('access denied!')
end,
}
setmetatable(module, meta)
end
В файле Module1.lua нужно в конец добавить одну строчку:
base.require('ModuleLocker').lock(_M)
Запускаем наш тест:
_NAME Module1
_PACKAGE
_M table: 0037F650
field1 test1
fun function: 0037DBB0
------Test Module1------
test1
Module1 fun call nil
Try to set new value to Module1.field1
..\..\bin\x86\vc80.debug\lua.exe: .\ModuleLocker.lua:30: access denied!
stack traceback:
[C]: in function 'error'
.\ModuleLocker.lua:30: in function <.\ModuleLocker.lua:28>
module_test.lua:7: in main chunk
[C]: ?
Вначале напечатаны поля, которые мы удаляем из модуля. Первые три поля мы в модуль не заносили, это служебные поля и удалять их, пожалуй не стоит. Вызов __newindex сгенерировал ошибку и показал стек, так что видно, в каком месте эта ошибка произошла (при желании вызов error() можно заменить на что-нибудь более гуманное). Исправим ModuleLocker так, чтобы он не удалял служебные поля из модулей:
function lock(module)
-- убираем из модуля все поля, кроме служебных,
-- чтобы вызывались методы __index и __newindex метатаблицы
-- данные сохраняем в таблицу t
local serviceFields = {
_NAME = true,
_PACKAGE = true,
_M = true,
}
local t = {}
for k, v in pairs(module) do
if not serviceFields[k] then
t[k] = v
-- удаляем данные из модуля
rawset(module, k, nil)
end
end
-- создаем в модуле таблицу с защищенными данными
rawset(module, 'privateData_', t)
local meta = {
__index = function(t, k)
-- возвращаем данные из потайной таблицы
return t.privateData_[k]
end,
__newindex = function(t, k, v)
-- генерируем ошибку
error('access denied!')
end,
}
setmetatable(module, meta)
end
Это, в общем, все. Однако, когда я стал применять ModuleLocker для работы, оказалось, что в модулях могут быть поля, которые пользователь может менять. Например:
local base = _G
module('Text')
local ModuleLocker = base.require('ModuleLocker')
-- это не меняется
defaultFontSize = 12
-- путь к папке с темой
themePath = './'
function setThemePath(path)
themePath = path
end
...
...
...
ModuleLocker.lock(_M)
При попытке установить поле themePath вызовом setThemePath() генерируется ошибка 'access denied!'. Есть два способа решения проблемы - хороший и плохой. Сначала плохой способ:
function setThemePath(path)
_M.privateData_['themePath'] = path
end
Фу... Это отвратительно! Мы исподтишка залезли в вотчину ModuleLocker и что-то там подкрутили. Тот, кто позже будет править ModuleLocker, может спокойно заменить privateData_ на что-то другое и в результате получит непонятно откуда появившуюся ошибку, или, хуже того, сначала ничего не произойдет, а сломается все позже у пользователей. Хороший способ такой - добавляем метод setValue() в модуль ModuleLocker:
function setValue(module, key, value)
module.privateData_[key] = value
end
И теперь вызов из модуля Text выглядит так:
function setThemePath(path)
ModuleLocker.setValue(_M, 'themePath', path)
end
Убедимся на практике :) Добавим поле field2 и функцию для его изменения в модуль Module1 (файл Module1.lua):
field2 = 1
function setField2(val)
ModuleLocker.setValue(_M, 'field2', 2)
end
Изменим тест (файл module_test.lua):
require('Module1')
print('------Test Module1------')
print(Module1.field1)
Module1.fun()
-- изменяем поле в read-only модуле
print('Module1.field2', Module1.field2)
Module1.setField2(2)
print('Module1.field2', Module1.field2)
print('Try to set new value to Module1.field1')
Module1.field1 = 'zzz'
print(Module1.field1)
print('------End Module1 test------')
Результат:
------Test Module1------
test1
Module1 fun call nil
Module1.field2 1
Module1.field2 2
Try to set new value to Module1.field1
..\..\bin\x86\vc80.debug\lua.exe: .\ModuleLocker.lua:38: access denied!
stack traceback:
[C]: in function 'error'
.\ModuleLocker.lua:38: in function <.\ModuleLocker.lua:36>
module_test.lua:10: in main chunk
[C]: ?
Исходные файлы можно взять здесь.
Исходник статьи в формате markdown здесь.
Благодарю за помощь Брезина Николая aka Jesus.