#Read-only модули Задача: Есть некий 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.