# Omezení paměti a zdrojů v Lua pískovnici na C++
Při integraci Lua do C++ aplikací často vzniká potřeba izolovat provádění skriptů od hlavního systému. To je obzvláště kritické pro scénáře, kde může být cizí kód zlomyslný nebo nestabilní – například v herních enginích, pluginových systémech nebo sandboxových prostředích. Jedním z klíčových aspektů takové izolace je kontrola spotřeby paměti. V tomto článku se rozebírá, jak implementovat přísný limit využití paměti v Lua prostřednictvím vlastního alokátoru integrovaného do stavu interpretu.
Jak Lua spravuje paměť
Lua deleguje všechny operace s pamětí externímu alokátoru. Ve výchozím nastavení se používá standardní realloc/free, ale jazyk umožňuje nahradit ho vlastní implementací. To se dosahuje předáním ukazatele na funkci při vytváření lua_State. Důležité: všechny požadavky na alokaci, změnu velikosti a uvolnění paměti procházejí tímto alokátorem – včetně práce sběrače odpadu, načítání modulů a vytváření tabulek, řetězců, funkcí aj.
Funkce alokátoru má následující signaturu:
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
ud— uživatelská data (obvykle ukazatel na stav řídícího paměti);ptr— ukazatel na aktuální blok (může býtNULLpři alokaci nového);currSize— aktuální velikost bloku nebo typ objektu (v Lua 5.2+ připtr == NULL);newSize— požadovaná nová velikost (0 znamená uvolnění).
To poskytuje plnou kontrolu nad distribucí paměti, což umožňuje realizovat její účetnictví a omezení.
Implementace omezeného alokátoru
Pro sledování objemu alokované paměti se zavádí struktura stavu:
struct LimitedAllocatorState {
size_t used {};
size_t limit {1 * 1024 * 1024}; // 1 MB ve výchozím nastavení
bool limitReached {false};
bool overflow {false};
bool isLimitEnabled() { return limit > 0; }
void disableLimit() { limit = 0; }
void resetErrorFlags() { limitReached = overflow = false; }
};
Alokátor kontroluje, zda nový požadavek (used - currSize + newSize) nepřekračuje stanovený limit. Při překročení vrací nullptr, což vede k chybě v Lua (např. not enough memory).
Zde jsou klíčové body implementace:
- Při
newSize == 0— uvolní se paměť ausedse sníží ocurrSize; - Při
ptr == NULL— alokuje se nový blok,currSizese ignoruje; - Ochrana před přetečením celých čísel je povinná;
- Stav alokátoru musí přežít déle než
lua_State.
Příklad funkce:
void *limitedAlloc(void *ud, void *ptr, size_t currSize, size_t newSize) {
auto *allocState = static_cast<LimitedAllocatorState*>(ud);
if (!allocState) return nullptr;
if (ptr == nullptr) currSize = 0;
if (newSize == 0) {
if (ptr != nullptr) {
allocState->used = (allocState->used >= currSize)
? allocState->used - currSize : 0;
}
std::free(ptr);
return nullptr;
}
const size_t usedBase = (allocState->used >= currSize)
? allocState->used - currSize : 0;
if (newSize > SIZE_MAX - usedBase) {
allocState->overflow = true;
return nullptr;
}
const size_t newUsed = usedBase + newSize;
if (allocState->isLimitEnabled() && newUsed > allocState->limit) {
allocState->limitReached = true;
return nullptr;
}
void *newPtr = std::realloc(ptr, newSize);
if (newPtr) allocState->used = newUsed;
return newPtr;
}
Integrace s C++ runtime
Při používání knihovny sol2 (populární obal nad Lua C API) vytvoření stavu s vlastním alokátorem vypadá takto:
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);
Důležité je dodržet pořadí deklarace členů v třídě: stav alokátoru musí být zničen po sol::state, jinak při destrukci stavu dojde k přístupu k již uvolněné paměti.
Doporučuje se zabalit celou logiku do třídy LuaRuntime, která:
- Spravuje životní cyklus stavu;
- Poskytuje metody pro reset chyb a změnu limitu za běhu;
- Umožňuje kontrolovat, zda byla překročena kvóta.
Příklad rozhraní:
class LuaRuntime {
private:
lua::memory::LimitedAllocatorState allocatorState;
sol::state state;
public:
LuaRuntime(size_t memoryLimit)
: allocatorState({.limit = memoryLimit}),
state(sol::default_at_panic, lua::memory::limitedAlloc, &allocatorState) {}
bool hasAllocError() {
return allocatorState.limitReached || allocatorState.overflow;
}
void resetAllocErrors() {
allocatorState.resetErrorFlags();
}
bool setMemoryLimit(size_t limit) {
allocatorState.limit = limit;
return true;
}
};
Omezení a praktická doporučení
Několik důležitých poznámek při práci s omezeným alokátorem:
- Limit je společný pro celý stav. Pokud používáte více pískovnic na jednom
lua_State, sdílejí si jeden rozpočet paměti. Pro striktní izolaci – jeden stav na jednu pískovnici. - Sběrač odpadu také spotřebovává paměť. I po odstranění objektů v Lua se paměť může dočasně zvětšit během práce GC.
- Chyby alokace ne vždy vedou k panice. Lua generuje výjimku, kterou lze zachytit přes
pcallnebo obslužný program paniky. - Nezapomeňte resetovat vlajky chyb. Po zpracování chyby
limitReachedzůstává aktivní až do explicitního resetu.
Také je třeba počítat s tím, že některé operace (např. konkatenace dlouhých řetězců nebo hluboká rekurze) mohou způsobit náhlé skoky v spotřebě paměti. Testování s realistickými zátěžemi je nezbytné.
Co je důležité
- Lua umožňuje plně kontrolovat alokaci paměti prostřednictvím vlastního alokátoru.
- Limit paměti se aplikuje na celý stav jako celek, ne na jednotlivé skripty.
- Při překročení limitu alokátor vrací
nullptr, což vyvolá chybu v Lua. - Stav alokátoru musí přežít
lua_Statez hlediska životnosti. - Přetečení celých čísel a správná aritmetika jsou povinné prvky spolehlivé implementace.
— Editorial Team
Zatím žádné komentáře.