Fix #472: Emscripten Frontend (#662)

* 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:
leftibot 2026-04-11 15:58:28 -06:00 committed by GitHub
parent 255ff87f37
commit 340e7d4b16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 469 additions and 0 deletions

71
.github/workflows/emscripten.yml vendored Normal file
View 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

View File

@ -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
View 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
View 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>

View 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

View 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_ */

View 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;
}