mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-06-15 00:16:17 +08:00
Address review: make ChaiScript engine an opaque handle, drop singleton
Replace the static ChaiScript singleton in the Emscripten wrapper with a handle-based registry symmetric to the existing State registry. JS callers now create an engine with chaiscript_create(), pass the resulting handle to the eval/state helpers, and release it with chaiscript_destroy(). Multiple independent engines are now possible, and a state snapshot can be restored onto any engine. Updated the playground HTML and the three native regression tests to exercise the new API. Requested by @lefticus in PR #699 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
34a95a28a0
commit
19b54e1d53
@ -185,6 +185,8 @@ print("10! = " + to_string(factorial(10)))
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
}
|
||||
|
||||
var chaiHandle = 0;
|
||||
|
||||
var Module = {
|
||||
print: function(text) {
|
||||
appendOutput(text);
|
||||
@ -193,6 +195,7 @@ print("10! = " + to_string(factorial(10)))
|
||||
appendOutput(text, 'output-error');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
chaiHandle = Module.create();
|
||||
statusEl.textContent = 'Ready';
|
||||
statusEl.className = 'ready';
|
||||
btnRun.disabled = false;
|
||||
@ -205,7 +208,7 @@ print("10! = " + to_string(factorial(10)))
|
||||
|
||||
appendOutput('> Running...', 'output-line');
|
||||
try {
|
||||
Module.eval(code);
|
||||
Module.eval(chaiHandle, code);
|
||||
} catch (e) {
|
||||
appendOutput('Error: ' + e.message, 'output-error');
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
EMSCRIPTEN_BINDINGS(chaiscript) {
|
||||
emscripten::function("create", &chaiscript_create);
|
||||
emscripten::function("destroy", &chaiscript_destroy);
|
||||
emscripten::function("eval", &chaiscript_eval);
|
||||
emscripten::function("evalString", &chaiscript_eval_string);
|
||||
emscripten::function("evalBool", &chaiscript_eval_bool);
|
||||
|
||||
@ -7,21 +7,26 @@
|
||||
// Shared eval helper functions for the ChaiScript Emscripten wrapper.
|
||||
// These functions provide typed evaluation of ChaiScript expressions,
|
||||
// used by both the Emscripten/WebAssembly build and native tests.
|
||||
//
|
||||
// The interface is opaque and handle-based: JS callers create one or more
|
||||
// engines via chaiscript_create(), pass the resulting handle to the eval and
|
||||
// state helpers, and release the engine with chaiscript_destroy() when done.
|
||||
// State snapshots are themselves opaque handles produced by
|
||||
// chaiscript_save_state() and consumed by chaiscript_restore_state() /
|
||||
// chaiscript_release_state(). Hiding ChaiScript and ChaiScript::State behind
|
||||
// integer handles keeps embind from having to manage their lifetimes and
|
||||
// avoids forcing a static singleton on the C++ side.
|
||||
|
||||
#ifndef CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_
|
||||
#define CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_
|
||||
|
||||
#include <chaiscript/chaiscript.hpp>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace detail {
|
||||
inline chaiscript::ChaiScript &get_chai_instance() {
|
||||
static chaiscript::ChaiScript chai;
|
||||
return chai;
|
||||
}
|
||||
|
||||
// ChaiScript::State captures globals/functions/types but not the top-level
|
||||
// scripting locals (variables created by `var x = ...` at the script's
|
||||
// outermost scope). The playground's reset-between-runs use case needs both,
|
||||
@ -31,66 +36,89 @@ namespace detail {
|
||||
std::map<std::string, chaiscript::Boxed_Value> locals;
|
||||
};
|
||||
|
||||
inline std::unordered_map<int, std::unique_ptr<chaiscript::ChaiScript>> &chai_registry() {
|
||||
static std::unordered_map<int, std::unique_ptr<chaiscript::ChaiScript>> registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
inline std::unordered_map<int, Snapshot> &state_registry() {
|
||||
static std::unordered_map<int, Snapshot> registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
inline int next_state_handle() {
|
||||
inline int next_handle() {
|
||||
static int handle = 0;
|
||||
return ++handle;
|
||||
}
|
||||
|
||||
inline chaiscript::ChaiScript &get_chai(const int handle) {
|
||||
return *chai_registry().at(handle);
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
inline void chaiscript_eval(const std::string &input) {
|
||||
detail::get_chai_instance().eval(input);
|
||||
}
|
||||
|
||||
inline std::string chaiscript_eval_string(const std::string &input) {
|
||||
return detail::get_chai_instance().eval<std::string>(input);
|
||||
}
|
||||
|
||||
inline bool chaiscript_eval_bool(const std::string &input) {
|
||||
return detail::get_chai_instance().eval<bool>(input);
|
||||
}
|
||||
|
||||
inline int chaiscript_eval_int(const std::string &input) {
|
||||
return detail::get_chai_instance().eval<int>(input);
|
||||
}
|
||||
|
||||
inline float chaiscript_eval_float(const std::string &input) {
|
||||
return detail::get_chai_instance().eval<float>(input);
|
||||
}
|
||||
|
||||
inline double chaiscript_eval_double(const std::string &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();
|
||||
auto &chai = detail::get_chai_instance();
|
||||
detail::state_registry().emplace(handle, detail::Snapshot{chai.get_state(), chai.get_locals()});
|
||||
// Construct a fresh ChaiScript engine and return an opaque handle. The handle
|
||||
// is owned by the caller and must be released with chaiscript_destroy().
|
||||
inline int chaiscript_create() {
|
||||
const int handle = detail::next_handle();
|
||||
detail::chai_registry().emplace(handle, std::make_unique<chaiscript::ChaiScript>());
|
||||
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);
|
||||
// Destroy an engine handle. Unknown handles are silently ignored so JS callers
|
||||
// do not need to track validity defensively.
|
||||
inline void chaiscript_destroy(const int handle) {
|
||||
detail::chai_registry().erase(handle);
|
||||
}
|
||||
|
||||
inline void chaiscript_eval(const int handle, const std::string &input) {
|
||||
detail::get_chai(handle).eval(input);
|
||||
}
|
||||
|
||||
inline std::string chaiscript_eval_string(const int handle, const std::string &input) {
|
||||
return detail::get_chai(handle).eval<std::string>(input);
|
||||
}
|
||||
|
||||
inline bool chaiscript_eval_bool(const int handle, const std::string &input) {
|
||||
return detail::get_chai(handle).eval<bool>(input);
|
||||
}
|
||||
|
||||
inline int chaiscript_eval_int(const int handle, const std::string &input) {
|
||||
return detail::get_chai(handle).eval<int>(input);
|
||||
}
|
||||
|
||||
inline float chaiscript_eval_float(const int handle, const std::string &input) {
|
||||
return detail::get_chai(handle).eval<float>(input);
|
||||
}
|
||||
|
||||
inline double chaiscript_eval_double(const int handle, const std::string &input) {
|
||||
return detail::get_chai(handle).eval<double>(input);
|
||||
}
|
||||
|
||||
// Snapshot the engine identified by chai_handle and return a fresh opaque
|
||||
// state handle. Release it with chaiscript_release_state() when no longer
|
||||
// needed.
|
||||
inline int chaiscript_save_state(const int chai_handle) {
|
||||
const int state_handle = detail::next_handle();
|
||||
auto &chai = detail::get_chai(chai_handle);
|
||||
detail::state_registry().emplace(state_handle, detail::Snapshot{chai.get_state(), chai.get_locals()});
|
||||
return state_handle;
|
||||
}
|
||||
|
||||
// Restore a previously snapshotted state onto the engine identified by
|
||||
// chai_handle. Unknown state handles are silently ignored.
|
||||
inline void chaiscript_restore_state(const int chai_handle, const int state_handle) {
|
||||
const auto it = detail::state_registry().find(state_handle);
|
||||
if (it != detail::state_registry().end()) {
|
||||
auto &chai = detail::get_chai_instance();
|
||||
auto &chai = detail::get_chai(chai_handle);
|
||||
chai.set_state(it->second.engine_state);
|
||||
chai.set_locals(it->second.locals);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Release a state handle returned by chaiscript_save_state. Releasing an
|
||||
// unknown handle is a no-op.
|
||||
inline void chaiscript_release_state(const int state_handle) {
|
||||
detail::state_registry().erase(state_handle);
|
||||
}
|
||||
|
||||
#endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */
|
||||
|
||||
@ -16,36 +16,57 @@
|
||||
#include <string>
|
||||
|
||||
int main() {
|
||||
const int chai = chaiscript_create();
|
||||
assert(chai > 0 && "create must return a positive handle");
|
||||
|
||||
// Test eval (void return) - same as Emscripten eval()
|
||||
chaiscript_eval("var x = 42");
|
||||
chaiscript_eval(chai, "var x = 42");
|
||||
|
||||
// Test evalString - same as Emscripten evalString()
|
||||
[[maybe_unused]] std::string s = chaiscript_eval_string("to_string(x)");
|
||||
[[maybe_unused]] const std::string s = chaiscript_eval_string(chai, "to_string(x)");
|
||||
assert(s == "42");
|
||||
|
||||
// Test evalInt - same as Emscripten evalInt()
|
||||
[[maybe_unused]] int i = chaiscript_eval_int("1 + 2");
|
||||
[[maybe_unused]] const int i = chaiscript_eval_int(chai, "1 + 2");
|
||||
assert(i == 3);
|
||||
|
||||
// Test evalBool - same as Emscripten evalBool()
|
||||
[[maybe_unused]] bool b = chaiscript_eval_bool("true");
|
||||
[[maybe_unused]] bool b = chaiscript_eval_bool(chai, "true");
|
||||
assert(b == true);
|
||||
|
||||
b = chaiscript_eval_bool("false");
|
||||
b = chaiscript_eval_bool(chai, "false");
|
||||
assert(b == false);
|
||||
|
||||
// Test evalFloat - same as Emscripten evalFloat()
|
||||
[[maybe_unused]] float f = chaiscript_eval_float("1.5f");
|
||||
[[maybe_unused]] const float f = chaiscript_eval_float(chai, "1.5f");
|
||||
assert(std::abs(f - 1.5f) < 0.001f);
|
||||
|
||||
// Test evalDouble - same as Emscripten evalDouble()
|
||||
[[maybe_unused]] double d = chaiscript_eval_double("3.14");
|
||||
[[maybe_unused]] const double d = chaiscript_eval_double(chai, "3.14");
|
||||
assert(std::abs(d - 3.14) < 0.001);
|
||||
|
||||
// Test a more complex expression
|
||||
chaiscript_eval("def square(n) { return n * n; }");
|
||||
[[maybe_unused]] int sq = chaiscript_eval_int("square(7)");
|
||||
chaiscript_eval(chai, "def square(n) { return n * n; }");
|
||||
[[maybe_unused]] const int sq = chaiscript_eval_int(chai, "square(7)");
|
||||
assert(sq == 49);
|
||||
|
||||
// A second engine is fully independent of the first.
|
||||
const int chai2 = chaiscript_create();
|
||||
assert(chai2 != chai && "create must mint a fresh handle each call");
|
||||
[[maybe_unused]] bool caught = false;
|
||||
try {
|
||||
chaiscript_eval(chai2, "x");
|
||||
} catch (const chaiscript::exception::eval_error &) {
|
||||
caught = true;
|
||||
}
|
||||
assert(caught && "engines must not share globals");
|
||||
|
||||
chaiscript_destroy(chai2);
|
||||
chaiscript_destroy(chai);
|
||||
|
||||
// Destroying an unknown handle is a no-op.
|
||||
chaiscript_destroy(chai);
|
||||
chaiscript_destroy(99999);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -22,12 +22,14 @@ int main() {
|
||||
// through the eval wrapper functions. In WASM builds without exception
|
||||
// support, these would abort instead of throwing.
|
||||
|
||||
const int chai = chaiscript_create();
|
||||
|
||||
[[maybe_unused]] bool caught = false;
|
||||
|
||||
// Test 1: eval with undefined variable should throw
|
||||
caught = false;
|
||||
try {
|
||||
chaiscript_eval("this_variable_does_not_exist");
|
||||
chaiscript_eval(chai, "this_variable_does_not_exist");
|
||||
} catch (const chaiscript::exception::eval_error &) {
|
||||
caught = true;
|
||||
}
|
||||
@ -36,7 +38,7 @@ int main() {
|
||||
// Test 2: evalString with a type mismatch should throw
|
||||
caught = false;
|
||||
try {
|
||||
chaiscript_eval_string("1 + 2");
|
||||
chaiscript_eval_string(chai, "1 + 2");
|
||||
} catch (const chaiscript::exception::bad_boxed_cast &) {
|
||||
caught = true;
|
||||
}
|
||||
@ -45,7 +47,7 @@ int main() {
|
||||
// Test 3: evalInt with invalid syntax should throw
|
||||
caught = false;
|
||||
try {
|
||||
chaiscript_eval_int("def {}");
|
||||
chaiscript_eval_int(chai, "def {}");
|
||||
} catch (const chaiscript::exception::eval_error &) {
|
||||
caught = true;
|
||||
}
|
||||
@ -54,7 +56,7 @@ int main() {
|
||||
// Test 4: eval with throw statement should propagate exception
|
||||
caught = false;
|
||||
try {
|
||||
chaiscript_eval("throw(\"user exception\")");
|
||||
chaiscript_eval(chai, "throw(\"user exception\")");
|
||||
} catch (const chaiscript::Boxed_Value &) {
|
||||
caught = true;
|
||||
} catch (...) {
|
||||
@ -63,11 +65,13 @@ int main() {
|
||||
assert(caught && "ChaiScript throw must propagate as an exception");
|
||||
|
||||
// Test 5: Verify normal operation still works after caught exceptions
|
||||
chaiscript_eval("var post_exception_test = 100");
|
||||
chaiscript_eval(chai, "var post_exception_test = 100");
|
||||
|
||||
[[maybe_unused]] const int result = chaiscript_eval_int("post_exception_test");
|
||||
[[maybe_unused]] const int result = chaiscript_eval_int(chai, "post_exception_test");
|
||||
assert(result == 100 && "normal eval must work after caught exceptions");
|
||||
|
||||
chaiscript_destroy(chai);
|
||||
|
||||
std::cout << "All emscripten exception tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -17,34 +17,37 @@
|
||||
#include <string>
|
||||
|
||||
int main() {
|
||||
const int chai = chaiscript_create();
|
||||
assert(chai > 0 && "create must return a positive handle");
|
||||
|
||||
// Capture a baseline before any user code has been evaluated.
|
||||
const int baseline = chaiscript_save_state();
|
||||
const int baseline = chaiscript_save_state(chai);
|
||||
assert(baseline > 0 && "saveState must return a positive handle");
|
||||
|
||||
// Distinct calls produce distinct handles.
|
||||
const int second = chaiscript_save_state();
|
||||
const int second = chaiscript_save_state(chai);
|
||||
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);
|
||||
chaiscript_eval(chai, "var post_baseline_marker = 123");
|
||||
assert(chaiscript_eval_int(chai, "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);
|
||||
chaiscript_restore_state(chai, baseline);
|
||||
|
||||
[[maybe_unused]] bool caught = false;
|
||||
try {
|
||||
chaiscript_eval("post_baseline_marker");
|
||||
chaiscript_eval(chai, "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);
|
||||
chaiscript_eval(chai, "var after_restore = 7");
|
||||
assert(chaiscript_eval_int(chai, "after_restore * 6") == 42);
|
||||
|
||||
// Releasing the handle is required so the registry does not grow unbounded.
|
||||
chaiscript_release_state(baseline);
|
||||
@ -55,8 +58,19 @@ int main() {
|
||||
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);
|
||||
chaiscript_restore_state(chai, 99999);
|
||||
assert(chaiscript_eval_int(chai, "after_restore") == 7);
|
||||
|
||||
// A snapshot taken on one engine can be restored onto a different engine,
|
||||
// confirming that state is decoupled from any particular instance.
|
||||
const int snapshot_with_after_restore = chaiscript_save_state(chai);
|
||||
const int chai2 = chaiscript_create();
|
||||
chaiscript_restore_state(chai2, snapshot_with_after_restore);
|
||||
assert(chaiscript_eval_int(chai2, "after_restore") == 7);
|
||||
|
||||
chaiscript_release_state(snapshot_with_after_restore);
|
||||
chaiscript_destroy(chai2);
|
||||
chaiscript_destroy(chai);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user