mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-07-31 00:37:02 +08:00
Merge branch 'develop' into fix/issue-524-a-std-vector-of-std-unique-ptrs-can-t-be
This commit is contained in:
commit
83e43d918e
71
.github/workflows/emscripten.yml
vendored
Normal file
71
.github/workflows/emscripten.yml
vendored
Normal file
@ -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
|
||||
@ -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()
|
||||
|
||||
35
emscripten/CMakeLists.txt
Normal file
35
emscripten/CMakeLists.txt
Normal file
@ -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"
|
||||
)
|
||||
237
emscripten/chaiscript.html
Normal file
237
emscripten/chaiscript.html
Normal file
@ -0,0 +1,237 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
ChaiScript Emscripten Frontend
|
||||
Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
|
||||
Copyright 2019, Rob Loach
|
||||
Copyright 2009-2018, Jason Turner
|
||||
Licensed under the BSD License. See "license.txt" for details.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ChaiScript</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
header {
|
||||
background: #16213e;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 2px solid #0f3460;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: #e94560;
|
||||
font-weight: 700;
|
||||
}
|
||||
header span {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
#status {
|
||||
margin-left: auto;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
background: #0f3460;
|
||||
}
|
||||
#status.ready { color: #4ecca3; }
|
||||
#status.loading { color: #e9c46a; }
|
||||
#status.error { color: #e94560; }
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
.panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.panel-header {
|
||||
background: #16213e;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #888;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
#input {
|
||||
flex: 1;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
outline: none;
|
||||
tab-size: 2;
|
||||
}
|
||||
.divider {
|
||||
width: 2px;
|
||||
background: #0f3460;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
background: #0d1117;
|
||||
padding: 1rem;
|
||||
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.output-line { color: #c9d1d9; }
|
||||
.output-error { color: #e94560; }
|
||||
footer {
|
||||
background: #16213e;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
border-top: 2px solid #0f3460;
|
||||
}
|
||||
button {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.4rem 1.2rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
button:hover { background: #c73652; }
|
||||
button:disabled { background: #555; cursor: not-allowed; }
|
||||
#btn-clear {
|
||||
background: #0f3460;
|
||||
}
|
||||
#btn-clear:hover { background: #1a4a8a; }
|
||||
.hint {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ChaiScript</h1>
|
||||
<span>Interactive Playground</span>
|
||||
<div id="status" class="loading">Loading...</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="panel">
|
||||
<div class="panel-header">Input</div>
|
||||
<textarea id="input" spellcheck="false">// Welcome to ChaiScript!
|
||||
// Write your code here and click Run (or press Ctrl+Enter).
|
||||
|
||||
def greet(name) {
|
||||
return "Hello, " + name + "!"
|
||||
}
|
||||
|
||||
print(greet("World"))
|
||||
print(greet("ChaiScript"))
|
||||
|
||||
// Math example
|
||||
def factorial(n) {
|
||||
if (n <= 1) { return 1 }
|
||||
return n * factorial(n - 1)
|
||||
}
|
||||
|
||||
print("5! = " + to_string(factorial(5)))
|
||||
print("10! = " + to_string(factorial(10)))
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="panel">
|
||||
<div class="panel-header">Output</div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<button id="btn-run" disabled>Run</button>
|
||||
<button id="btn-clear">Clear</button>
|
||||
<span class="hint">Ctrl+Enter to run</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var outputEl = document.getElementById('output');
|
||||
var inputEl = document.getElementById('input');
|
||||
var btnRun = document.getElementById('btn-run');
|
||||
var btnClear = document.getElementById('btn-clear');
|
||||
var statusEl = document.getElementById('status');
|
||||
|
||||
function appendOutput(text, className) {
|
||||
var line = document.createElement('div');
|
||||
line.className = className || 'output-line';
|
||||
line.textContent = text;
|
||||
outputEl.appendChild(line);
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
}
|
||||
|
||||
var Module = {
|
||||
print: function(text) {
|
||||
appendOutput(text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
appendOutput(text, 'output-error');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
statusEl.textContent = 'Ready';
|
||||
statusEl.className = 'ready';
|
||||
btnRun.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
function runCode() {
|
||||
var code = inputEl.value;
|
||||
if (!code.trim()) return;
|
||||
|
||||
appendOutput('> Running...', 'output-line');
|
||||
try {
|
||||
Module.eval(code);
|
||||
} catch (e) {
|
||||
appendOutput('Error: ' + e.message, 'output-error');
|
||||
}
|
||||
appendOutput('', 'output-line');
|
||||
}
|
||||
|
||||
btnRun.addEventListener('click', runCode);
|
||||
btnClear.addEventListener('click', function() {
|
||||
outputEl.innerHTML = '';
|
||||
});
|
||||
|
||||
inputEl.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
runCode();
|
||||
}
|
||||
// Tab key inserts spaces
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 2;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="chaiscript.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
emscripten/chaiscript_em.cpp
Normal file
23
emscripten/chaiscript_em.cpp
Normal file
@ -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/bind.h>
|
||||
|
||||
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
|
||||
48
emscripten/chaiscript_eval.hpp
Normal file
48
emscripten/chaiscript_eval.hpp
Normal file
@ -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 <string>
|
||||
#include <chaiscript/chaiscript.hpp>
|
||||
|
||||
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<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);
|
||||
}
|
||||
|
||||
#endif /* CHAISCRIPT_EMSCRIPTEN_EVAL_HPP_ */
|
||||
@ -49,9 +49,8 @@ namespace chaiscript {
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
bootstrap::standard_library::future_type<std::future<chaiscript::Boxed_Value>>("future", *lib);
|
||||
lib->add(chaiscript::fun(
|
||||
[](const std::function<chaiscript::Boxed_Value()> &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);
|
||||
|
||||
@ -115,6 +115,8 @@ namespace chaiscript::bootstrap {
|
||||
|
||||
m.add(fun(&parse_string<T>), "to_" + name);
|
||||
m.add(fun([](const T t) { return t; }), "to_" + name);
|
||||
m.add(fun([](const Boxed_Number &bn) { return bn.get_as<T>(); }), "to_" + name);
|
||||
m.add(fun(&parse_string<T>), name);
|
||||
}
|
||||
|
||||
/// "clone" function for a shared_ptr type. This is used in the case
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@ -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<chaiscript::detail::threading::shared_mutex> 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<typename Type>
|
||||
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<std::thread> threads;
|
||||
{
|
||||
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::shared_mutex> 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<std::thread> m_async_threads;
|
||||
#endif
|
||||
};
|
||||
|
||||
class Dispatch_State {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@ -179,6 +180,25 @@ namespace chaiscript {
|
||||
}),
|
||||
"namespace");
|
||||
m_engine.add(fun([this](const std::string &t_namespace_name) { import(t_namespace_name); }), "import");
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
// Register async() with thread tracking so the engine can join all
|
||||
// async threads before destroying shared state (issues #632, #636).
|
||||
m_engine.add(chaiscript::fun(
|
||||
[this](const std::function<chaiscript::Boxed_Value()> &t_func) {
|
||||
auto promise_ptr = std::make_shared<std::promise<chaiscript::Boxed_Value>>();
|
||||
auto future = promise_ptr->get_future();
|
||||
m_engine.track_async_thread(std::thread([promise_ptr, t_func]() {
|
||||
try {
|
||||
promise_ptr->set_value(t_func());
|
||||
} catch (...) {
|
||||
promise_ptr->set_exception(std::current_exception());
|
||||
}
|
||||
}));
|
||||
return future;
|
||||
}),
|
||||
"async");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Skip BOM at the beginning of file
|
||||
|
||||
17
unittests/async_engine_lifetime.chai
Normal file
17
unittests/async_engine_lifetime.chai
Normal file
@ -0,0 +1,17 @@
|
||||
// Regression test for #632 and #636: Heap-use-after-free in async threads
|
||||
// Async threads must complete before the engine is destroyed.
|
||||
|
||||
var func = fun(){
|
||||
var ret = 0;
|
||||
for (var i = 0; i < 1000; ++i) {
|
||||
ret += i;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
var fut1 = async(func);
|
||||
var fut2 = async(func);
|
||||
|
||||
// Wait for results to verify correctness
|
||||
assert_equal(fut1.get(), 499500);
|
||||
assert_equal(fut2.get(), 499500);
|
||||
46
unittests/async_engine_lifetime_test.cpp
Normal file
46
unittests/async_engine_lifetime_test.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
// Regression test for #632 and #636:
|
||||
// Heap-use-after-free when async threads outlive the ChaiScript engine.
|
||||
// The engine must join all outstanding async threads before destroying shared state.
|
||||
|
||||
#include <chaiscript/chaiscript.hpp>
|
||||
|
||||
int main() {
|
||||
// Test 1: Async threads still running when engine is destroyed.
|
||||
// Without the fix, this triggers heap-use-after-free under ASAN/TSAN.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
{
|
||||
chaiscript::ChaiScript chai;
|
||||
try {
|
||||
chai.eval(R"(
|
||||
var func = fun(){
|
||||
var ret = 0;
|
||||
for (var i = 0; i < 5000; ++i) {
|
||||
ret += i;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
var fut1 = async(func);
|
||||
var fut2 = async(func);
|
||||
)");
|
||||
// Engine destroyed here without waiting for futures.
|
||||
} catch (const std::exception &) {
|
||||
// Exceptions are fine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Verify async still works correctly (results are accessible).
|
||||
{
|
||||
chaiscript::ChaiScript chai;
|
||||
auto result = chai.eval<int>(R"(
|
||||
var f = async(fun() { return 42; });
|
||||
f.get();
|
||||
)");
|
||||
if (result != 42) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@ -1462,6 +1462,87 @@ TEST_CASE("Issue #524 - vector of unique_ptr can be registered") {
|
||||
CHECK(chai.eval<bool>("var v2 = UniqueVec(); v2.empty()") == true);
|
||||
}
|
||||
|
||||
// Issue #625: function_less_than comparator must satisfy strict-weak ordering.
|
||||
// Registering overloaded functions with different arities triggered a
|
||||
// std::stable_sort assertion on macOS 15.2 (hardened libc++) because the
|
||||
// comparator violated transitivity of equivalence.
|
||||
TEST_CASE("Issue 625: function_less_than strict-weak ordering with different arities") {
|
||||
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
|
||||
|
||||
// Register overloaded functions with varying arities under the same name.
|
||||
// If the comparator doesn't order by arity when overlapping params match,
|
||||
// std::stable_sort may exhibit undefined behavior.
|
||||
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, const chaiscript::Boxed_Value &) { return x; }), "overloaded"));
|
||||
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, double y) { return x + static_cast<int>(y); }), "overloaded"));
|
||||
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x) { return x; }), "overloaded"));
|
||||
|
||||
// Verify dispatch still works correctly
|
||||
CHECK(chai.eval<int>("overloaded(5)") == 5);
|
||||
CHECK(chai.eval<int>("overloaded(3, 2.0)") == 5);
|
||||
}
|
||||
|
||||
// Regression test: push_back() on script-created vector has no effect when
|
||||
// vector_conversion is in effect. The bug occurs because dispatch selects
|
||||
// the C++ push_back for the converted type over the built-in one, operating
|
||||
// on a temporary copy of the vector.
|
||||
TEST_CASE("push_back on script vector with vector_conversion") {
|
||||
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
|
||||
|
||||
auto m = std::make_shared<chaiscript::Module>();
|
||||
chaiscript::bootstrap::standard_library::vector_type<std::vector<std::string>>("VectorString", *m);
|
||||
m->add(chaiscript::vector_conversion<std::vector<std::string>>());
|
||||
chai.add(m);
|
||||
|
||||
// Register a C++ function that accepts the converted type, so we can
|
||||
// verify that vector_conversion actually works for passing vectors
|
||||
chai.add(chaiscript::fun([](const std::vector<std::string> &v) -> std::string {
|
||||
std::string result;
|
||||
for (const auto &s : v) {
|
||||
if (!result.empty()) { result += ","; }
|
||||
result += s;
|
||||
}
|
||||
return result;
|
||||
}), "join_strings");
|
||||
|
||||
// push_back on an empty script-created vector must be visible
|
||||
CHECK(chai.eval<bool>(
|
||||
"auto x = [];"
|
||||
"x.push_back(\"Hello\");"
|
||||
"x.size() == 1"
|
||||
));
|
||||
|
||||
// push_back on a vector with initial elements must grow correctly
|
||||
CHECK(chai.eval<bool>(
|
||||
"auto y = [\"a\", \"b\"];"
|
||||
"y.push_back(\"c\");"
|
||||
"y.push_back(\"d\");"
|
||||
"y.size() == 4"
|
||||
));
|
||||
|
||||
// Verify the actual content is preserved after push_back
|
||||
CHECK(chai.eval<std::string>(
|
||||
"auto z = [];"
|
||||
"z.push_back(\"World\");"
|
||||
"z[0]"
|
||||
) == "World");
|
||||
|
||||
// Round-trip: build a vector in script, push_back elements, then pass it
|
||||
// to a C++ function via vector_conversion and verify the contents
|
||||
CHECK(chai.eval<std::string>(
|
||||
"auto v = [\"one\", \"two\"];"
|
||||
"v.push_back(\"three\");"
|
||||
"join_strings(v)"
|
||||
) == "one,two,three");
|
||||
|
||||
// Verify conversion works on a freshly created vector too
|
||||
CHECK(chai.eval<std::string>(
|
||||
"auto w = [];"
|
||||
"w.push_back(\"hello\");"
|
||||
"w.push_back(\"world\");"
|
||||
"join_strings(w)"
|
||||
) == "hello,world");
|
||||
}
|
||||
|
||||
// Regression test for issue #607: AST_Node_Trace must be a complete type
|
||||
// when used in eval_error's std::vector<AST_Node_Trace> call_stack member.
|
||||
// This failed to compile with C++20 on clang/libc++ when AST_Node_Trace
|
||||
|
||||
33
unittests/conversion_functions.chai
Normal file
33
unittests/conversion_functions.chai
Normal file
@ -0,0 +1,33 @@
|
||||
// Test that all conversion functions work across char, int, and string types
|
||||
|
||||
// char() constructor
|
||||
assert_equal('A', char('A')) // char from char (identity via Boxed_Number)
|
||||
assert_equal('A', char(65)) // char from int (via Boxed_Number)
|
||||
assert_equal('A', char("A")) // char from string (new)
|
||||
|
||||
// to_char() conversions
|
||||
assert_equal('A', to_char('A')) // to_char from char (identity)
|
||||
assert_equal('A', to_char(65)) // to_char from int (new, via Boxed_Number)
|
||||
assert_equal('A', to_char("A")) // to_char from string (existing)
|
||||
|
||||
// int() constructor
|
||||
assert_equal(65, int('A')) // int from char (via Boxed_Number)
|
||||
assert_equal(65, int(65)) // int from int (identity via Boxed_Number)
|
||||
assert_equal(65, int("65")) // int from string (new)
|
||||
|
||||
// to_int() conversions
|
||||
assert_equal(65, to_int('A')) // to_int from char (new, via Boxed_Number)
|
||||
assert_equal(65, to_int(65)) // to_int from int (identity)
|
||||
assert_equal(65, to_int("65")) // to_int from string (existing)
|
||||
|
||||
// to_string() conversions
|
||||
assert_equal("A", to_string('A'))
|
||||
assert_equal("65", to_string(65))
|
||||
assert_equal("A", to_string("A"))
|
||||
|
||||
// double conversions
|
||||
assert_equal(3.5, double("3.5")) // double from string (new)
|
||||
assert_equal(3.5, to_double("3.5")) // to_double from string (existing)
|
||||
assert_equal(65.0, to_double('A')) // to_double from char (new, via Boxed_Number)
|
||||
assert_equal(65.0, to_double(65)) // to_double from int (new, via Boxed_Number)
|
||||
assert_equal("3.5", to_string(3.5)) // to_string from double (existing)
|
||||
51
unittests/emscripten_eval_test.cpp
Normal file
51
unittests/emscripten_eval_test.cpp
Normal file
@ -0,0 +1,51 @@
|
||||
// Test that validates the eval patterns used by the Emscripten wrapper.
|
||||
// Based on work by Rob Loach (https://github.com/RobLoach/ChaiScript.js)
|
||||
|
||||
#ifndef CHAISCRIPT_NO_THREADS
|
||||
#define CHAISCRIPT_NO_THREADS
|
||||
#endif
|
||||
|
||||
#ifndef CHAISCRIPT_NO_DYNLOAD
|
||||
#define CHAISCRIPT_NO_DYNLOAD
|
||||
#endif
|
||||
|
||||
#include <chaiscript/chaiscript.hpp>
|
||||
#include "../emscripten/chaiscript_eval.hpp"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
int main() {
|
||||
// Test eval (void return) - same as Emscripten eval()
|
||||
chaiscript_eval("var x = 42");
|
||||
|
||||
// Test evalString - same as Emscripten evalString()
|
||||
std::string s = chaiscript_eval_string("to_string(x)");
|
||||
assert(s == "42");
|
||||
|
||||
// Test evalInt - same as Emscripten evalInt()
|
||||
int i = chaiscript_eval_int("1 + 2");
|
||||
assert(i == 3);
|
||||
|
||||
// Test evalBool - same as Emscripten evalBool()
|
||||
bool b = chaiscript_eval_bool("true");
|
||||
assert(b == true);
|
||||
|
||||
b = chaiscript_eval_bool("false");
|
||||
assert(b == false);
|
||||
|
||||
// Test evalFloat - same as Emscripten evalFloat()
|
||||
float f = chaiscript_eval_float("1.5f");
|
||||
assert(std::abs(f - 1.5f) < 0.001f);
|
||||
|
||||
// Test evalDouble - same as Emscripten evalDouble()
|
||||
double d = chaiscript_eval_double("3.14");
|
||||
assert(std::abs(d - 3.14) < 0.001);
|
||||
|
||||
// Test a more complex expression
|
||||
chaiscript_eval("def square(n) { return n * n; }");
|
||||
int sq = chaiscript_eval_int("square(7)");
|
||||
assert(sq == 49);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user