mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-04-30 19:09:26 +08:00
* Fix #472: Add Emscripten/WebAssembly build for browser-based ChaiScript Port Rob Loach's ChaiScript.js work (https://github.com/RobLoach/ChaiScript.js) into the main repository as an Emscripten build target. Adds a GitHub Actions workflow that builds ChaiScript to WebAssembly and publishes artifacts (JS, WASM, HTML) for embedding in the official ChaiScript.com website. Includes an HTML interactive playground frontend and a native test validating the eval API surface. Co-Authored-By: Rob Loach <robloach@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: publish WASM assets as release under wasm-latest tag Add a publish job to the emscripten workflow that creates a prerelease tagged wasm-latest with chaiscript.js, chaiscript.wasm, and chaiscript.html as downloadable assets. Runs only on pushes to the develop branch. The website repo can fetch these via the public GitHub Releases API on a daily cron without any cross-repo auth. Requested by @lefticus in PR #662 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: leftibot <leftibot@users.noreply.github.com> Co-authored-by: Rob Loach <robloach@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
255ff87f37
commit
340e7d4b16
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
|
||||
@ -423,6 +423,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_ */
|
||||
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