Fix #693: Add Emscripten/embind bindings for get_state/set_state

The Emscripten wrapper exported only the eval family, leaving JS consumers
with no way to snapshot or restore the singleton ChaiScript engine. The
playground in chaiscript.github.io needs that to reset between runs without
reloading the WASM module. Added handle-based wrappers that hide
ChaiScript::State behind an int registry so JS callers don't have to manage
embind object lifetimes, exported them as saveState/restoreState/releaseState,
and added a native regression test that exercises capture, restore, and
release through the same wrapper functions the WASM binding uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-30 15:48:46 -06:00
parent d3c94e4451
commit 810888defc
4 changed files with 104 additions and 0 deletions

View File

@ -452,6 +452,10 @@ if(BUILD_TESTING)
target_link_libraries(emscripten_exception_test ${LIBS}) target_link_libraries(emscripten_exception_test ${LIBS})
add_test(NAME Emscripten_Exception_Test COMMAND emscripten_exception_test) 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) add_executable(threading_config_test unittests/threading_config_test.cpp)
target_link_libraries(threading_config_test ${LIBS}) target_link_libraries(threading_config_test ${LIBS})
add_test(NAME Threading_Config_Test COMMAND threading_config_test) add_test(NAME Threading_Config_Test COMMAND threading_config_test)

View File

@ -19,5 +19,8 @@ EMSCRIPTEN_BINDINGS(chaiscript) {
emscripten::function("evalInt", &chaiscript_eval_int); emscripten::function("evalInt", &chaiscript_eval_int);
emscripten::function("evalFloat", &chaiscript_eval_float); emscripten::function("evalFloat", &chaiscript_eval_float);
emscripten::function("evalDouble", &chaiscript_eval_double); 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 #endif

View File

@ -13,12 +13,23 @@
#include <chaiscript/chaiscript.hpp> #include <chaiscript/chaiscript.hpp>
#include <string> #include <string>
#include <unordered_map>
namespace detail { namespace detail {
inline chaiscript::ChaiScript &get_chai_instance() { inline chaiscript::ChaiScript &get_chai_instance() {
static chaiscript::ChaiScript chai; static chaiscript::ChaiScript chai;
return chai; return chai;
} }
inline std::unordered_map<int, chaiscript::ChaiScript::State> &state_registry() {
static std::unordered_map<int, chaiscript::ChaiScript::State> registry;
return registry;
}
inline int next_state_handle() {
static int handle = 0;
return ++handle;
}
} // namespace detail } // namespace detail
inline void chaiscript_eval(const std::string &input) { 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<double>(input); return detail::get_chai_instance().eval<double>(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_ */ #endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */

View File

@ -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 <cassert>
#include <chaiscript/chaiscript.hpp>
#include <string>
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;
}