diff --git a/.github/workflows/emscripten.yml b/.github/workflows/emscripten.yml
new file mode 100644
index 00000000..4be8931f
--- /dev/null
+++ b/.github/workflows/emscripten.yml
@@ -0,0 +1,71 @@
+name: Emscripten
+
+on:
+ push:
+ branches: [develop, main]
+ pull_request:
+ branches: [develop, main]
+ workflow_dispatch:
+
+jobs:
+ emscripten:
+ name: Emscripten WebAssembly Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Emscripten
+ uses: mymindstorm/setup-emsdk@v14
+
+ - name: Configure
+ run: emcmake cmake -B build-em -S emscripten -DCMAKE_BUILD_TYPE=Release
+
+ - name: Build
+ run: cmake --build build-em -j
+
+ - name: Verify artifacts
+ run: |
+ test -f build-em/chaiscript.js
+ test -f build-em/chaiscript.wasm
+ test -f build-em/chaiscript.html
+ echo "All expected artifacts present"
+ ls -lh build-em/chaiscript.*
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: chaiscript-web
+ path: |
+ build-em/chaiscript.js
+ build-em/chaiscript.wasm
+ build-em/chaiscript.html
+ retention-days: 90
+
+ publish:
+ name: Publish WASM Release
+ needs: emscripten
+ if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: chaiscript-web
+ path: artifacts
+
+ - name: Publish to wasm-latest release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ # Flatten: artifacts may contain build-em/ subdirectory
+ find artifacts -type f \( -name 'chaiscript.js' -o -name 'chaiscript.wasm' -o -name 'chaiscript.html' \) -exec cp {} . \;
+
+ # Delete existing release if present, then recreate
+ gh release delete wasm-latest --repo "$GITHUB_REPOSITORY" -y 2>/dev/null || true
+ gh release create wasm-latest \
+ --repo "$GITHUB_REPOSITORY" \
+ --title "ChaiScript WASM Build (latest)" \
+ --notes "Auto-published from develop branch. Built with Emscripten for browser use." \
+ --prerelease \
+ chaiscript.js chaiscript.wasm chaiscript.html
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ad3e79a0..7fcb4bc2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -413,6 +413,9 @@ if(BUILD_TESTING)
"CHAI_USE_PATH=${CMAKE_CURRENT_SOURCE_DIR}/unittests/"
"CHAI_MODULE_PATH=${CMAKE_CURRENT_BINARY_DIR}/"
)
+ add_executable(async_engine_lifetime_test unittests/async_engine_lifetime_test.cpp)
+ target_link_libraries(async_engine_lifetime_test ${LIBS})
+ add_test(NAME Async_Engine_Lifetime_Test COMMAND async_engine_lifetime_test)
endif()
add_executable(multifile_test
@@ -423,6 +426,10 @@ if(BUILD_TESTING)
target_link_libraries(multifile_test ${LIBS})
add_test(NAME MultiFile_Test COMMAND multifile_test)
+ add_executable(emscripten_eval_test unittests/emscripten_eval_test.cpp)
+ target_link_libraries(emscripten_eval_test ${LIBS})
+ add_test(NAME Emscripten_Eval_Test COMMAND emscripten_eval_test)
+
install(TARGETS test_module RUNTIME DESTINATION bin LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/chaiscript")
endif()
endif()
diff --git a/emscripten/CMakeLists.txt b/emscripten/CMakeLists.txt
new file mode 100644
index 00000000..7751631f
--- /dev/null
+++ b/emscripten/CMakeLists.txt
@@ -0,0 +1,35 @@
+# Emscripten/WebAssembly build for ChaiScript
+# Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
+#
+# Usage:
+# emcmake cmake -B build-em -S emscripten
+# cmake --build build-em
+
+cmake_minimum_required(VERSION 3.12)
+project(chaiscript_em)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# Emscripten-specific compiler/linker flags
+add_definitions(-DCHAISCRIPT_NO_THREADS -DCHAISCRIPT_NO_DYNLOAD)
+
+add_executable(chaiscript chaiscript_em.cpp)
+target_include_directories(chaiscript PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
+
+# Emscripten link flags: enable embind, allow memory growth, export as ES module-compatible
+target_link_options(chaiscript PRIVATE
+ --bind
+ -sALLOW_MEMORY_GROWTH=1
+ -sEXPORT_ES6=0
+ -sMODULARIZE=0
+ -sINVOKE_RUN=0
+)
+
+# Copy the HTML shell to the build output directory
+add_custom_command(TARGET chaiscript POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_CURRENT_SOURCE_DIR}/chaiscript.html
+ ${CMAKE_CURRENT_BINARY_DIR}/chaiscript.html
+ COMMENT "Copying HTML frontend to build directory"
+)
diff --git a/emscripten/chaiscript.html b/emscripten/chaiscript.html
new file mode 100644
index 00000000..29e32252
--- /dev/null
+++ b/emscripten/chaiscript.html
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+ ChaiScript
+
+
+
+
+ ChaiScript
+ Interactive Playground
+ Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/emscripten/chaiscript_em.cpp b/emscripten/chaiscript_em.cpp
new file mode 100644
index 00000000..06806797
--- /dev/null
+++ b/emscripten/chaiscript_em.cpp
@@ -0,0 +1,23 @@
+// This file is distributed under the BSD License.
+// See "license.txt" for details.
+// Copyright 2019, Rob Loach (https://github.com/RobLoach/ChaiScript.js)
+// Copyright 2009-2018, Jason Turner (jason@emptycrate.com)
+// http://www.chaiscript.com
+//
+// Emscripten/WebAssembly wrapper for ChaiScript.
+// Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
+
+#include "chaiscript_eval.hpp"
+
+#ifdef __EMSCRIPTEN__
+#include
+
+EMSCRIPTEN_BINDINGS(chaiscript) {
+ emscripten::function("eval", &chaiscript_eval);
+ emscripten::function("evalString", &chaiscript_eval_string);
+ emscripten::function("evalBool", &chaiscript_eval_bool);
+ emscripten::function("evalInt", &chaiscript_eval_int);
+ emscripten::function("evalFloat", &chaiscript_eval_float);
+ emscripten::function("evalDouble", &chaiscript_eval_double);
+}
+#endif
diff --git a/emscripten/chaiscript_eval.hpp b/emscripten/chaiscript_eval.hpp
new file mode 100644
index 00000000..52791ab3
--- /dev/null
+++ b/emscripten/chaiscript_eval.hpp
@@ -0,0 +1,48 @@
+// This file is distributed under the BSD License.
+// See "license.txt" for details.
+// Copyright 2019, Rob Loach (https://github.com/RobLoach/ChaiScript.js)
+// Copyright 2009-2018, Jason Turner (jason@emptycrate.com)
+// http://www.chaiscript.com
+
+// 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.
+
+#ifndef CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_
+#define CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_
+
+#include
+#include
+
+namespace detail {
+inline chaiscript::ChaiScript &get_chai_instance() {
+ static chaiscript::ChaiScript chai;
+ return chai;
+}
+} // 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(input);
+}
+
+inline bool chaiscript_eval_bool(const std::string &input) {
+ return detail::get_chai_instance().eval(input);
+}
+
+inline int chaiscript_eval_int(const std::string &input) {
+ return detail::get_chai_instance().eval(input);
+}
+
+inline float chaiscript_eval_float(const std::string &input) {
+ return detail::get_chai_instance().eval(input);
+}
+
+inline double chaiscript_eval_double(const std::string &input) {
+ return detail::get_chai_instance().eval(input);
+}
+
+#endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */
diff --git a/include/chaiscript/chaiscript_stdlib.hpp b/include/chaiscript/chaiscript_stdlib.hpp
index 5cb4fa68..0daac71e 100644
--- a/include/chaiscript/chaiscript_stdlib.hpp
+++ b/include/chaiscript/chaiscript_stdlib.hpp
@@ -49,9 +49,8 @@ namespace chaiscript {
#ifndef CHAISCRIPT_NO_THREADS
bootstrap::standard_library::future_type>("future", *lib);
- lib->add(chaiscript::fun(
- [](const std::function &t_func) { return std::async(std::launch::async, t_func); }),
- "async");
+ // Note: async() is registered in ChaiScript_Basic::build_eval_system()
+ // with thread tracking to prevent heap-use-after-free on engine destruction.
#endif
json_wrap::library(*lib);
diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp
index ba1de5ad..d9b3e23e 100644
--- a/include/chaiscript/dispatchkit/bootstrap.hpp
+++ b/include/chaiscript/dispatchkit/bootstrap.hpp
@@ -115,6 +115,8 @@ namespace chaiscript::bootstrap {
m.add(fun(&parse_string), "to_" + name);
m.add(fun([](const T t) { return t; }), "to_" + name);
+ m.add(fun([](const Boxed_Number &bn) { return bn.get_as(); }), "to_" + name);
+ m.add(fun(&parse_string), name);
}
/// "clone" function for a shared_ptr type. This is used in the case
diff --git a/include/chaiscript/dispatchkit/dispatchkit.hpp b/include/chaiscript/dispatchkit/dispatchkit.hpp
index b54d42b4..be43905d 100644
--- a/include/chaiscript/dispatchkit/dispatchkit.hpp
+++ b/include/chaiscript/dispatchkit/dispatchkit.hpp
@@ -19,6 +19,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -370,6 +371,28 @@ namespace chaiscript {
, m_parser(parser) {
}
+ ~Dispatch_Engine() {
+ join_async_threads();
+ }
+
+ Dispatch_Engine(const Dispatch_Engine &) = delete;
+ Dispatch_Engine &operator=(const Dispatch_Engine &) = delete;
+ Dispatch_Engine(Dispatch_Engine &&) = default;
+ Dispatch_Engine &operator=(Dispatch_Engine &&) = default;
+
+#ifndef CHAISCRIPT_NO_THREADS
+ /// Track an async thread so it can be joined during destruction
+ void track_async_thread(std::thread t_thread) {
+ chaiscript::detail::threading::unique_lock l(m_async_mutex);
+ // Clean up already-finished threads to avoid unbounded growth
+ m_async_threads.erase(
+ std::remove_if(m_async_threads.begin(), m_async_threads.end(),
+ [](std::thread &t) { return !t.joinable(); }),
+ m_async_threads.end());
+ m_async_threads.push_back(std::move(t_thread));
+ }
+#endif
+
/// \brief casts an object while applying any Dynamic_Conversion available
template
decltype(auto) boxed_cast(const Boxed_Value &bv) const {
@@ -1064,13 +1087,21 @@ namespace chaiscript {
// overridden methods in derived classes are tried first during dispatch
const auto &lhs_dotn = lhs->dynamic_object_type_name();
const auto &rhs_dotn = rhs->dynamic_object_type_name();
- if (!lhs_dotn.empty() && !rhs_dotn.empty() && lhs_dotn != rhs_dotn) {
- if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) {
- return true; // lhs is derived from rhs, so lhs is more specific
+ if (lhs_dotn != rhs_dotn) {
+ if (!lhs_dotn.empty() && !rhs_dotn.empty()) {
+ if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) {
+ return true; // lhs is derived from rhs, so lhs is more specific
+ }
+ if (dispatch::Dynamic_Object::type_matches(rhs_dotn, lhs_dotn)) {
+ return false; // rhs is derived from lhs, so rhs is more specific
+ }
}
- if (dispatch::Dynamic_Object::type_matches(rhs_dotn, lhs_dotn)) {
- return false; // rhs is derived from lhs, so rhs is more specific
+ // Impose a total order on type names to maintain strict-weak ordering:
+ // non-empty names sort before empty names, then lexicographically
+ if (lhs_dotn.empty() != rhs_dotn.empty()) {
+ return !lhs_dotn.empty();
}
+ return lhs_dotn < rhs_dotn;
}
const auto &lhsparamtypes = lhs->get_param_types();
@@ -1120,7 +1151,9 @@ namespace chaiscript {
return lt < rt;
}
- return false;
+ // When all overlapping parameters match, order by arity to maintain
+ // strict-weak ordering (transitivity of equivalence)
+ return lhssize < rhssize;
}
/// Implementation detail for adding a function.
@@ -1165,6 +1198,21 @@ namespace chaiscript {
get_function_objects_int().insert_or_assign(t_name, std::move(new_func));
}
+ void join_async_threads() {
+#ifndef CHAISCRIPT_NO_THREADS
+ std::vector threads;
+ {
+ chaiscript::detail::threading::unique_lock l(m_async_mutex);
+ threads = std::move(m_async_threads);
+ }
+ for (auto &t : threads) {
+ if (t.joinable()) {
+ t.join();
+ }
+ }
+#endif
+ }
+
mutable chaiscript::detail::threading::shared_mutex m_mutex;
Type_Conversions m_conversions;
@@ -1174,6 +1222,11 @@ namespace chaiscript {
mutable std::atomic_uint_fast32_t m_method_missing_loc = {0};
State m_state;
+
+#ifndef CHAISCRIPT_NO_THREADS
+ mutable chaiscript::detail::threading::shared_mutex m_async_mutex;
+ std::vector m_async_threads;
+#endif
};
class Dispatch_State {
diff --git a/include/chaiscript/dispatchkit/proxy_functions.hpp b/include/chaiscript/dispatchkit/proxy_functions.hpp
index fe65ee5a..002e473b 100644
--- a/include/chaiscript/dispatchkit/proxy_functions.hpp
+++ b/include/chaiscript/dispatchkit/proxy_functions.hpp
@@ -798,6 +798,14 @@ namespace chaiscript {
++numdiffs;
}
}
+
+ // Deprioritize functions whose first parameter (object/receiver) requires
+ // type conversion: conversions create temporaries, so mutations on the
+ // converted object are silently lost.
+ if (plist.size() > 1 && !func->get_param_types()[1].bare_equal(plist[0].get_type_info())) {
+ numdiffs = plist.size();
+ }
+
ordered_funcs.emplace_back(numdiffs, func.get());
}
}
diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp
index c9f22e58..50dfb3c7 100644
--- a/include/chaiscript/language/chaiscript_engine.hpp
+++ b/include/chaiscript/language/chaiscript_engine.hpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include