diff --git a/luprex/core/Makefile b/luprex/core/Makefile index dfc3dc2a..4a9009eb 100644 --- a/luprex/core/Makefile +++ b/luprex/core/Makefile @@ -14,6 +14,7 @@ CPP_FILES=\ cpp/gui.cpp\ cpp/luasnap.cpp\ cpp/animqueue.cpp\ + cpp/packer.cpp\ cpp/source.cpp\ cpp/world.cpp\ cpp/textgame.cpp\ diff --git a/luprex/core/cpp/luasnap.cpp b/luprex/core/cpp/luasnap.cpp index 6c9b81a3..1ee7836d 100644 --- a/luprex/core/cpp/luasnap.cpp +++ b/luprex/core/cpp/luasnap.cpp @@ -26,39 +26,8 @@ LuaSnap::~LuaSnap() { std::cerr << "LuaSnap destructor not implemented yet" << std::endl; } -bool LuaSnap::have_snapshot() const { - return !snapshot_.empty(); -} - -static int oss_writer(lua_State *L, const void *p, size_t sz, void *ud) { - std::ostringstream *oss = (std::ostringstream *)ud; - oss->write((const char *)p, sz); - return 0; -} - -// Convert a C++ string into a lua_Reader. -class StringReader { -public: - const std::string &str_; - bool done_; - StringReader(const std::string &s) : str_(s), done_(false) {} - - static const char *fn(lua_State *L, void *data, size_t *size) { - StringReader *sr = (StringReader*)data; - if (sr->done_) { - *size = 0; - return nullptr; - } else { - sr->done_ = true; - *size = sr->str_.size(); - return sr->str_.c_str(); - } - } -}; - -void LuaSnap::snapshot() { - // Snapshot and the lua stack should both be empty. - assert(snapshot_.empty()); +void LuaSnap::serialize(Packer *pk) { + // Lua stack should be empty. assert(lua_gettop(state_) == 0); // lua variables that we'll need. @@ -93,24 +62,26 @@ void LuaSnap::snapshot() { LS.result(); assert(lua_gettop(state_) == 2); - // Call eris to dump the state to an ostringstream, - // then save the result. - std::ostringstream oss; - eris_dump(state_, oss_writer, (void *)&oss); - snapshot_ = oss.str(); - std::cerr << "Eris dump is " << snapshot_.size() << " bytes." << std::endl; + // Write dummy length, use eris to write data, then overwrite length. + pk->write_int64(0); + size_t tell = pk->tellp(); + eris_dump(state_, pk->lua_writer, pk->lua_writer_ud()); + pk->overwrite_int64(tell, pk->tellp() - tell); lua_settop(state_, 0); + std::cerr << "Eris dump is " << (pk->tellp() - tell) << " bytes." << std::endl; } -void LuaSnap::rollback() { - // Snapshot should have data, lua stack should be empty. - assert(!snapshot_.empty()); +void LuaSnap::deserialize(Unpacker *unpk) { + // Lua stack should be empty. assert(lua_gettop(state_) == 0); + // Get a reader subsection containing the eris data. + size_t len = unpk->read_int64(); + Unpacker subsec = unpk->read_section(len); + // Call eris with the permanents table and passing the snapshot as a lua_Reader. lua_getfield(state_, LUA_REGISTRYINDEX, "unpersist"); - StringReader sr(snapshot_); - eris_undump(state_, StringReader::fn, &sr); + eris_undump(state_, subsec.lua_reader, subsec.lua_reader_ud()); assert(lua_gettop(state_) == 2); // Set up a stack frame. @@ -127,6 +98,47 @@ void LuaSnap::rollback() { } LS.result(); assert(lua_gettop(state_) == 0); +} + +// Snapshot and rollback can trivially be implemented on top of serialize and +// deserialize. However, it's also possible to implement snapshot and rollback +// using an alternative technique: +// +// 1. When constructing the lua interpreter, use a custom memory allocator that +// keeps track of all the memory blocks used by lua. +// +// 2. Snapshot simply copies all the memory blocks used by lua into a buffer. +// +// 3. Rollback restores lua's memory blocks back to their previous state. This +// has the effect of restoring lua's state. +// +// A proof-of-concept implementation of the memory-snapshotting design was +// created, and it worked. It is probably faster than using serialize and +// deserialize. +// +// Note: even if we implement this alternative design, we still need to keep +// serialize and deserialize around in order to implement the save-game +// functionality. So for now, we're sticking with this design, which doesn't +// require us to maintain any additional code. + +bool LuaSnap::have_snapshot() const { + return !snapshot_.empty(); +} + +void LuaSnap::snapshot() { + assert(snapshot_.empty()); + std::ostringstream oss; + Packer pk(&oss); + serialize(&pk); + snapshot_ = oss.str(); +} + +void LuaSnap::rollback() { + assert(!snapshot_.empty()); + Unpacker unpk(snapshot_.c_str(), snapshot_.size()); + deserialize(&unpk); snapshot_.clear(); } + + diff --git a/luprex/core/cpp/luasnap.hpp b/luprex/core/cpp/luasnap.hpp index d8794c34..3e85051d 100644 --- a/luprex/core/cpp/luasnap.hpp +++ b/luprex/core/cpp/luasnap.hpp @@ -14,6 +14,7 @@ #ifndef LUASNAP_HPP #define LUASNAP_HPP +#include "packer.hpp" #include "luastack.hpp" class LuaSnap { @@ -29,6 +30,14 @@ public: // lua_State *state() const { return state_; } + // Serialize the state of the lua interpreter. + // + void serialize(Packer *pk); + + // Restore the the lua interpreter given a serialized state. + // + void deserialize(Unpacker *unpk); + // Return true if there's a saved snapshot. // bool have_snapshot() const; @@ -44,6 +53,7 @@ public: // If there is no snapshot, this panics. // void rollback(); + }; diff --git a/luprex/core/cpp/packer.cpp b/luprex/core/cpp/packer.cpp new file mode 100644 index 00000000..a1405c18 --- /dev/null +++ b/luprex/core/cpp/packer.cpp @@ -0,0 +1,115 @@ +#include +#include + +void Packer::write_int8(int8_t v) { + oss_->write((const char *)&v, sizeof(v)); +} + +void Packer::write_int16(int16_t v) { + oss_->write((const char *)&v, sizeof(v)); +} + +void Packer::write_int32(int32_t v) { + oss_->write((const char *)&v, sizeof(v)); +} + +void Packer::write_int64(int64_t v) { + oss_->write((const char *)&v, sizeof(v)); +} + +void Packer::overwrite_int8(size_t tell_after, int8_t v) { + oss_->seekp(tell_after - 1); + write_int8(v); + oss_->seekp(0, std::ios_base::end); +} + +void Packer::overwrite_int16(size_t tell_after, int16_t v) { + oss_->seekp(tell_after - 2); + write_int16(v); + oss_->seekp(0, std::ios_base::end); +} + +void Packer::overwrite_int32(size_t tell_after, int32_t v) { + oss_->seekp(tell_after - 4); + write_int32(v); + oss_->seekp(0, std::ios_base::end); +} + +void Packer::overwrite_int64(size_t tell_after, int64_t v) { + oss_->seekp(tell_after - 8); + write_int64(v); + oss_->seekp(0, std::ios_base::end); +} + +int Packer::lua_writer(lua_State *L, const void *p, size_t sz, void *ud) { + Packer *pkr = (Packer *)ud; + pkr->oss_->write((const char *)p, sz); + return 0; +} + +Unpacker::Unpacker(const char *d, size_t s) { + assert(d != nullptr); + assert(s >= 0); + data_ = d; + size_ = s; +} + +int8_t Unpacker::read_int8() { + assert(size_ >= 1); + int8_t result; + memcpy(&result, data_, 1); + data_ += 1; + size_ -= 1; + return result; +} + +int16_t Unpacker::read_int16() { + assert(size_ >= 2); + int16_t result; + memcpy(&result, data_, 2); + data_ += 2; + size_ -= 2; + return result; +} + +int32_t Unpacker::read_int32() { + assert(size_ >= 4); + int32_t result; + memcpy(&result, data_, 4); + data_ += 4; + size_ -= 4; + return result; +} + +int64_t Unpacker::read_int64() { + assert(size_ >= 8); + int64_t result; + memcpy(&result, data_, 8); + data_ += 8; + size_ -= 8; + return result; +} + +void Unpacker::skip(size_t n) { + assert(size_ >= n); + data_ += n; + size_ -= n; +} + +Unpacker Unpacker::read_section(size_t n) { + assert(size_ >= n); + const char *d = data_; + data_ += n; + size_ -= n; + return Unpacker(d, n); +} + +const char *Unpacker::lua_reader(lua_State *L, void *ud, size_t *size) { + Unpacker *unpk = (Unpacker *)ud; + const char *retval = unpk->data_; + *size = unpk->size_; + if (unpk->size_ == 0) retval = nullptr; + unpk->data_ += unpk->size_; + unpk->size_ = 0; + return retval; +} diff --git a/luprex/core/cpp/packer.hpp b/luprex/core/cpp/packer.hpp new file mode 100644 index 00000000..6f462c8e --- /dev/null +++ b/luprex/core/cpp/packer.hpp @@ -0,0 +1,72 @@ +#ifndef PACKER_HPP +#define PACKER_HPP + +#include "luastack.hpp" +#include +#include +#include +#include + +class Packer { +private: + std::ostringstream *oss_; + + // Packer is not copyable. + Packer(const Packer &other) { assert(false); } +public: + using fixhandle = size_t; + Packer(std::ostringstream *s) : oss_(s) {} + + // Get the current file position. + size_t tellp() { return oss_->tellp(); } + + void write_int8(int8_t v); + void write_int16(int16_t v); + void write_int32(int32_t v); + void write_int64(int64_t v); + + // Sometimes, you want to write a length followed by a block of data. + // Sometimes, you don't know the length until after you've written the block + // of data. To help with this situation, we provide the overwrite_int + // functions. Here's what you do: + // + // 1. write a dummy length into the stream. + // 2. use 'tellp' to fetch the stream position *after* the dummy length. + // 3. write the block of data. + // 4. use an overwrite function below to overwrite the dummy length. + // + void overwrite_int8(size_t tell_after, int8_t value); + void overwrite_int16(size_t tell_after, int16_t value); + void overwrite_int32(size_t tell_after, int32_t value); + void overwrite_int64(size_t tell_after, int64_t value); + + // The packer can be used as a lua_Writer. + static int lua_writer(lua_State *L, const void *p, size_t sz, void *ud); + void *lua_writer_ud() { return this; } +}; + +class Unpacker { +private: + const char *data_; + size_t size_; + +public: + Unpacker(const char *d, size_t s); + const char *data() { return data_; } + size_t size() { return size_; } + + int8_t read_int8(); + int16_t read_int16(); + int32_t read_int32(); + int64_t read_int64(); + void skip(size_t n); + Unpacker read_section(size_t n); + + // The unpacker can be used as a lua_Reader + static const char *lua_reader(lua_State *L, void *ud, size_t *size); + void *lua_reader_ud() { return this; } +}; + + + +#endif // PACKER_HPP