Powrót do strony głównej

Ograniczenie pamięci w piaskownicy Lua na C++

Artykuł opisuje implementację kontroli pamięci w piaskownicy Lua na C++ za pomocą alokatora użytkownika. Omówiono cechy architektoniczne, przykłady kodu i praktyczne zalecenia dla programistów.

Kontrola pamięci w Lua: izolacja skryptów na C++
Advertisement 728x90

Ograniczenie pamięci i zasobów w piaskownicy Lua w C++

Podczas integrowania Lua z aplikacjami C++ często pojawia się potrzeba izolowania wykonywania skryptów od głównego systemu. Jest to szczególnie krytyczne w scenariuszach, w których kod zewnętrzny może być złośliwy lub niestabilny — na przykład w silnikach gier, systemach wtyczek lub środowiskach sandbox. Jednym z kluczowych aspektów takiej izolacji jest kontrola zużycia pamięci. W tym artykule omówiono, jak zaimplementować ścisły limit użycia pamięci w Lua za pomocą niestandardowego alokatora zintegrowanego ze stanem interpretera.

Jak Lua zarządza pamięcią

Lua deleguje wszystkie operacje z pamięcią zewnętrznemu alokatorowi. Domyślnie używany jest standardowy realloc/free, ale język pozwala zastąpić go własną implementacją. Osiąga się to poprzez przekazanie wskaźnika do funkcji podczas tworzenia lua_State. Ważne: wszystkie żądania alokacji, zmiany rozmiaru i zwalniania pamięci przechodzą przez ten alokator — w tym pracę kolektora śmieci, ładowanie modułów i tworzenie tabel, ciągów znaków, funkcji itp.

Funkcja alokatora ma następującą sygnaturę:

Google AdInline article slot
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
  • ud — dane użytkownika (zwykle wskaźnik na stan kontrolera pamięci);
  • ptr — wskaźnik na aktualny blok (może być NULL przy alokacji nowego);
  • currSize — aktualny rozmiar bloku lub typ obiektu (w Lua 5.2+ przy ptr == NULL);
  • newSize — żądany nowy rozmiar (0 oznacza zwolnienie).

To daje pełną kontrolę nad dystrybucją pamięci, co pozwala na jej ewidencję i ograniczenie użycia.

Implementacja ograniczonego alokatora

Do śledzenia objętości zaalokowanej pamięci wprowadza się strukturę stanu:

struct LimitedAllocatorState {
    size_t used {};
    size_t limit {1 * 1024 * 1024}; // 1 MB domyślnie
    bool limitReached {false};
    bool overflow {false};

    bool isLimitEnabled() { return limit > 0; }
    void disableLimit() { limit = 0; }
    void resetErrorFlags() { limitReached = overflow = false; }
};

Alokator sprawdza, czy nowe żądanie (used - currSize + newSize) nie przekracza ustalonego limitu. W przypadku przekroczenia zwraca nullptr, co powoduje błąd w Lua (np. not enough memory).

Google AdInline article slot

Oto kluczowe elementy implementacji:

  • Przy newSize == 0 — zwalnia się pamięć, a used zmniejsza się o currSize;
  • Przy ptr == NULL — alokuje się nowy blok, currSize jest ignorowane;
  • Ochrona przed przepełnieniem całkowitym jest obowiązkowa;
  • Stan alokatora musi przetrwać dłużej niż lua_State.

Przykład funkcji:

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;
}

Integracja z runtime'em C++

Podczas używania biblioteki sol2 (popularna biblioteka opakowująca Lua C API) tworzenie stanu z niestandardowym alokatorem wygląda następująco:

Google AdInline article slot
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);

Ważne jest zachowanie kolejności deklaracji pól w klasie: stan alokatora musi być niszczony po sol::state, w przeciwnym razie podczas destrukcji stanu dojdzie do odwołania do już zwolnionej pamięci.

Zaleca się opakowanie całej logiki w klasę LuaRuntime, która:

  • Zarządza cyklem życia stanu;
  • Dostarcza metody do resetowania błędów i zmiany limitu w locie;
  • Umożliwia sprawdzanie, czy quota została przekroczona.

Przykład interfejsu:

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;
    }
};

Ograniczenia i praktyczne zalecenia

Kilka ważnych uwag przy pracy z ograniczonym alokatorem:

  • Limit jest wspólny dla całego stanu. Jeśli używasz kilku piaskownic na jednym lua_State, dzielą one budżet pamięci. Dla ścisłej izolacji — jeden stan na jedną piaskownicę.
  • Kolektor śmieci również zużywa pamięć. Nawet po usunięciu obiektów w Lua pamięć może tymczasowo rosnąć podczas pracy GC.
  • Błędy alokacji nie zawsze powodują panikę. Lua generuje wyjątek, który można przechwycić za pomocą pcall lub obsługę paniki.
  • Nie zapominaj resetować flag błędów. Po obsłużeniu błędu limitReached pozostaje aktywny do jawnego zresetowania.

Warto też uwzględnić, że niektóre operacje (np. konkatenacja długich ciągów lub głęboka rekurencja) mogą powodować gwałtowne skoki zużycia pamięci. Testowanie z realistycznymi obciążeniami jest obowiązkowe.

Co ważne

  • Lua pozwala na pełną kontrolę alokacji pamięci za pomocą niestandardowego alokatora.
  • Limit pamięci dotyczy całego stanu, a nie pojedynczych skryptów.
  • Przy przekroczeniu limitu alokator zwraca nullptr, co powoduje błąd w Lua.
  • Stan alokatora musi przetrwać lua_State pod względem czasu życia.
  • Przepełnienie całkowite i poprawna arytmetyka to obowiązkowe elementy solidnej implementacji.

— Editorial Team

Advertisement 728x90

Czytaj dalej