diff --git a/CMakeLists.txt b/CMakeLists.txt index e496a3f3..48d6f791 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -452,6 +452,10 @@ if(BUILD_TESTING) target_link_libraries(emscripten_exception_test ${LIBS}) add_test(NAME Emscripten_Exception_Test COMMAND emscripten_exception_test) + add_executable(emscripten_state_test unittests/emscripten_state_test.cpp) + target_link_libraries(emscripten_state_test ${LIBS}) + add_test(NAME Emscripten_State_Test COMMAND emscripten_state_test) + add_executable(threading_config_test unittests/threading_config_test.cpp) target_link_libraries(threading_config_test ${LIBS}) add_test(NAME Threading_Config_Test COMMAND threading_config_test) diff --git a/emscripten/chaiscript_em.cpp b/emscripten/chaiscript_em.cpp index 06806797..f6026689 100644 --- a/emscripten/chaiscript_em.cpp +++ b/emscripten/chaiscript_em.cpp @@ -19,5 +19,8 @@ EMSCRIPTEN_BINDINGS(chaiscript) { emscripten::function("evalInt", &chaiscript_eval_int); emscripten::function("evalFloat", &chaiscript_eval_float); emscripten::function("evalDouble", &chaiscript_eval_double); + emscripten::function("saveState", &chaiscript_save_state); + emscripten::function("restoreState", &chaiscript_restore_state); + emscripten::function("releaseState", &chaiscript_release_state); } #endif diff --git a/emscripten/chaiscript_eval.hpp b/emscripten/chaiscript_eval.hpp index 45cf2378..70c861e6 100644 --- a/emscripten/chaiscript_eval.hpp +++ b/emscripten/chaiscript_eval.hpp @@ -13,12 +13,23 @@ #include #include +#include namespace detail { inline chaiscript::ChaiScript &get_chai_instance() { static chaiscript::ChaiScript chai; return chai; } + + inline std::unordered_map &state_registry() { + static std::unordered_map registry; + return registry; + } + + inline int next_state_handle() { + static int handle = 0; + return ++handle; + } } // namespace detail inline void chaiscript_eval(const std::string &input) { @@ -45,4 +56,28 @@ inline double chaiscript_eval_double(const std::string &input) { return detail::get_chai_instance().eval(input); } +// Snapshot the current engine state and return an opaque handle. The handle +// is owned by the caller; release it with chaiscript_release_state when it is +// no longer needed. +inline int chaiscript_save_state() { + const int handle = detail::next_state_handle(); + detail::state_registry().emplace(handle, detail::get_chai_instance().get_state()); + return handle; +} + +// Restore a previously snapshotted state. Unknown handles are silently +// ignored so JS callers do not need to track validity defensively. +inline void chaiscript_restore_state(const int handle) { + const auto it = detail::state_registry().find(handle); + if (it != detail::state_registry().end()) { + detail::get_chai_instance().set_state(it->second); + } +} + +// Release a handle returned by chaiscript_save_state. Releasing an unknown +// handle is a no-op. +inline void chaiscript_release_state(const int handle) { + detail::state_registry().erase(handle); +} + #endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */ diff --git a/unittests/emscripten_state_test.cpp b/unittests/emscripten_state_test.cpp new file mode 100644 index 00000000..b7866d18 --- /dev/null +++ b/unittests/emscripten_state_test.cpp @@ -0,0 +1,62 @@ +// Test that validates the save/restore/release state helpers used by the +// Emscripten wrapper. A handle-based registry is exposed so the JS playground +// can capture a pristine engine once and restore it between runs without +// reloading the WASM module. + +#ifndef CHAISCRIPT_NO_THREADS +#define CHAISCRIPT_NO_THREADS +#endif + +#ifndef CHAISCRIPT_NO_DYNLOAD +#define CHAISCRIPT_NO_DYNLOAD +#endif + +#include "../emscripten/chaiscript_eval.hpp" +#include +#include +#include + +int main() { + // Capture a baseline before any user code has been evaluated. + const int baseline = chaiscript_save_state(); + assert(baseline > 0 && "saveState must return a positive handle"); + + // Distinct calls produce distinct handles. + const int second = chaiscript_save_state(); + assert(second != baseline && "saveState must mint a fresh handle each call"); + chaiscript_release_state(second); + + // Define a global that did not exist in the baseline. + chaiscript_eval("var post_baseline_marker = 123"); + assert(chaiscript_eval_int("post_baseline_marker") == 123); + + // Restoring the baseline must drop the post-baseline definition: evaluating + // it should now fail with eval_error because the variable is undefined. + chaiscript_restore_state(baseline); + + [[maybe_unused]] bool caught = false; + try { + chaiscript_eval("post_baseline_marker"); + } catch (const chaiscript::exception::eval_error &) { + caught = true; + } + assert(caught && "restoreState must drop globals defined after the snapshot"); + + // The engine remains usable after a restore. + chaiscript_eval("var after_restore = 7"); + assert(chaiscript_eval_int("after_restore * 6") == 42); + + // Releasing the handle is required so the registry does not grow unbounded. + chaiscript_release_state(baseline); + + // Releasing a handle that was never minted (or has already been released) is + // a no-op rather than an error. + chaiscript_release_state(baseline); + chaiscript_release_state(99999); + + // Restoring an unknown handle is a no-op: state must be unchanged. + chaiscript_restore_state(99999); + assert(chaiscript_eval_int("after_restore") == 7); + + return 0; +}