Skip to content

Commit 340e7d4

Browse files
leftibotRobLoachclaude
authored
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>
1 parent 255ff87 commit 340e7d4

7 files changed

Lines changed: 469 additions & 0 deletions

File tree

.github/workflows/emscripten.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Emscripten
2+
3+
on:
4+
push:
5+
branches: [develop, main]
6+
pull_request:
7+
branches: [develop, main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
emscripten:
12+
name: Emscripten WebAssembly Build
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Setup Emscripten
18+
uses: mymindstorm/setup-emsdk@v14
19+
20+
- name: Configure
21+
run: emcmake cmake -B build-em -S emscripten -DCMAKE_BUILD_TYPE=Release
22+
23+
- name: Build
24+
run: cmake --build build-em -j
25+
26+
- name: Verify artifacts
27+
run: |
28+
test -f build-em/chaiscript.js
29+
test -f build-em/chaiscript.wasm
30+
test -f build-em/chaiscript.html
31+
echo "All expected artifacts present"
32+
ls -lh build-em/chaiscript.*
33+
34+
- name: Upload artifacts
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: chaiscript-web
38+
path: |
39+
build-em/chaiscript.js
40+
build-em/chaiscript.wasm
41+
build-em/chaiscript.html
42+
retention-days: 90
43+
44+
publish:
45+
name: Publish WASM Release
46+
needs: emscripten
47+
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
48+
runs-on: ubuntu-latest
49+
permissions:
50+
contents: write
51+
steps:
52+
- uses: actions/download-artifact@v4
53+
with:
54+
name: chaiscript-web
55+
path: artifacts
56+
57+
- name: Publish to wasm-latest release
58+
env:
59+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
run: |
61+
# Flatten: artifacts may contain build-em/ subdirectory
62+
find artifacts -type f \( -name 'chaiscript.js' -o -name 'chaiscript.wasm' -o -name 'chaiscript.html' \) -exec cp {} . \;
63+
64+
# Delete existing release if present, then recreate
65+
gh release delete wasm-latest --repo "$GITHUB_REPOSITORY" -y 2>/dev/null || true
66+
gh release create wasm-latest \
67+
--repo "$GITHUB_REPOSITORY" \
68+
--title "ChaiScript WASM Build (latest)" \
69+
--notes "Auto-published from develop branch. Built with Emscripten for browser use." \
70+
--prerelease \
71+
chaiscript.js chaiscript.wasm chaiscript.html

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@ if(BUILD_TESTING)
423423
target_link_libraries(multifile_test ${LIBS})
424424
add_test(NAME MultiFile_Test COMMAND multifile_test)
425425

426+
add_executable(emscripten_eval_test unittests/emscripten_eval_test.cpp)
427+
target_link_libraries(emscripten_eval_test ${LIBS})
428+
add_test(NAME Emscripten_Eval_Test COMMAND emscripten_eval_test)
429+
426430
install(TARGETS test_module RUNTIME DESTINATION bin LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/chaiscript")
427431
endif()
428432
endif()

emscripten/CMakeLists.txt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Emscripten/WebAssembly build for ChaiScript
2+
# Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
3+
#
4+
# Usage:
5+
# emcmake cmake -B build-em -S emscripten
6+
# cmake --build build-em
7+
8+
cmake_minimum_required(VERSION 3.12)
9+
project(chaiscript_em)
10+
11+
set(CMAKE_CXX_STANDARD 20)
12+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
13+
14+
# Emscripten-specific compiler/linker flags
15+
add_definitions(-DCHAISCRIPT_NO_THREADS -DCHAISCRIPT_NO_DYNLOAD)
16+
17+
add_executable(chaiscript chaiscript_em.cpp)
18+
target_include_directories(chaiscript PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
19+
20+
# Emscripten link flags: enable embind, allow memory growth, export as ES module-compatible
21+
target_link_options(chaiscript PRIVATE
22+
--bind
23+
-sALLOW_MEMORY_GROWTH=1
24+
-sEXPORT_ES6=0
25+
-sMODULARIZE=0
26+
-sINVOKE_RUN=0
27+
)
28+
29+
# Copy the HTML shell to the build output directory
30+
add_custom_command(TARGET chaiscript POST_BUILD
31+
COMMAND ${CMAKE_COMMAND} -E copy
32+
${CMAKE_CURRENT_SOURCE_DIR}/chaiscript.html
33+
${CMAKE_CURRENT_BINARY_DIR}/chaiscript.html
34+
COMMENT "Copying HTML frontend to build directory"
35+
)

emscripten/chaiscript.html

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<!doctype html>
2+
<!--
3+
ChaiScript Emscripten Frontend
4+
Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
5+
Copyright 2019, Rob Loach
6+
Copyright 2009-2018, Jason Turner
7+
Licensed under the BSD License. See "license.txt" for details.
8+
-->
9+
<html lang="en">
10+
<head>
11+
<meta charset="utf-8">
12+
<meta name="viewport" content="width=device-width, initial-scale=1">
13+
<title>ChaiScript</title>
14+
<style>
15+
* { box-sizing: border-box; margin: 0; padding: 0; }
16+
body {
17+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
18+
background: #1a1a2e;
19+
color: #e0e0e0;
20+
min-height: 100vh;
21+
display: flex;
22+
flex-direction: column;
23+
}
24+
header {
25+
background: #16213e;
26+
padding: 1rem 2rem;
27+
border-bottom: 2px solid #0f3460;
28+
display: flex;
29+
align-items: center;
30+
gap: 1rem;
31+
}
32+
header h1 {
33+
font-size: 1.5rem;
34+
color: #e94560;
35+
font-weight: 700;
36+
}
37+
header span {
38+
font-size: 0.85rem;
39+
color: #888;
40+
}
41+
#status {
42+
margin-left: auto;
43+
font-size: 0.85rem;
44+
padding: 0.3rem 0.8rem;
45+
border-radius: 4px;
46+
background: #0f3460;
47+
}
48+
#status.ready { color: #4ecca3; }
49+
#status.loading { color: #e9c46a; }
50+
#status.error { color: #e94560; }
51+
main {
52+
flex: 1;
53+
display: flex;
54+
gap: 0;
55+
}
56+
.panel {
57+
flex: 1;
58+
display: flex;
59+
flex-direction: column;
60+
min-width: 0;
61+
}
62+
.panel-header {
63+
background: #16213e;
64+
padding: 0.5rem 1rem;
65+
font-size: 0.8rem;
66+
text-transform: uppercase;
67+
letter-spacing: 0.1em;
68+
color: #888;
69+
border-bottom: 1px solid #0f3460;
70+
}
71+
#input {
72+
flex: 1;
73+
background: #0d1117;
74+
color: #c9d1d9;
75+
border: none;
76+
padding: 1rem;
77+
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
78+
font-size: 0.9rem;
79+
line-height: 1.6;
80+
resize: none;
81+
outline: none;
82+
tab-size: 2;
83+
}
84+
.divider {
85+
width: 2px;
86+
background: #0f3460;
87+
}
88+
#output {
89+
flex: 1;
90+
background: #0d1117;
91+
padding: 1rem;
92+
font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
93+
font-size: 0.9rem;
94+
line-height: 1.6;
95+
overflow-y: auto;
96+
white-space: pre-wrap;
97+
word-break: break-all;
98+
}
99+
.output-line { color: #c9d1d9; }
100+
.output-error { color: #e94560; }
101+
footer {
102+
background: #16213e;
103+
padding: 0.5rem 1rem;
104+
display: flex;
105+
gap: 0.5rem;
106+
align-items: center;
107+
border-top: 2px solid #0f3460;
108+
}
109+
button {
110+
background: #e94560;
111+
color: #fff;
112+
border: none;
113+
padding: 0.4rem 1.2rem;
114+
border-radius: 4px;
115+
cursor: pointer;
116+
font-size: 0.85rem;
117+
font-weight: 600;
118+
}
119+
button:hover { background: #c73652; }
120+
button:disabled { background: #555; cursor: not-allowed; }
121+
#btn-clear {
122+
background: #0f3460;
123+
}
124+
#btn-clear:hover { background: #1a4a8a; }
125+
.hint {
126+
margin-left: auto;
127+
font-size: 0.75rem;
128+
color: #555;
129+
}
130+
</style>
131+
</head>
132+
<body>
133+
<header>
134+
<h1>ChaiScript</h1>
135+
<span>Interactive Playground</span>
136+
<div id="status" class="loading">Loading...</div>
137+
</header>
138+
<main>
139+
<div class="panel">
140+
<div class="panel-header">Input</div>
141+
<textarea id="input" spellcheck="false">// Welcome to ChaiScript!
142+
// Write your code here and click Run (or press Ctrl+Enter).
143+
144+
def greet(name) {
145+
return "Hello, " + name + "!"
146+
}
147+
148+
print(greet("World"))
149+
print(greet("ChaiScript"))
150+
151+
// Math example
152+
def factorial(n) {
153+
if (n <= 1) { return 1 }
154+
return n * factorial(n - 1)
155+
}
156+
157+
print("5! = " + to_string(factorial(5)))
158+
print("10! = " + to_string(factorial(10)))
159+
</textarea>
160+
</div>
161+
<div class="divider"></div>
162+
<div class="panel">
163+
<div class="panel-header">Output</div>
164+
<div id="output"></div>
165+
</div>
166+
</main>
167+
<footer>
168+
<button id="btn-run" disabled>Run</button>
169+
<button id="btn-clear">Clear</button>
170+
<span class="hint">Ctrl+Enter to run</span>
171+
</footer>
172+
173+
<script>
174+
var outputEl = document.getElementById('output');
175+
var inputEl = document.getElementById('input');
176+
var btnRun = document.getElementById('btn-run');
177+
var btnClear = document.getElementById('btn-clear');
178+
var statusEl = document.getElementById('status');
179+
180+
function appendOutput(text, className) {
181+
var line = document.createElement('div');
182+
line.className = className || 'output-line';
183+
line.textContent = text;
184+
outputEl.appendChild(line);
185+
outputEl.scrollTop = outputEl.scrollHeight;
186+
}
187+
188+
var Module = {
189+
print: function(text) {
190+
appendOutput(text);
191+
},
192+
printErr: function(text) {
193+
appendOutput(text, 'output-error');
194+
},
195+
onRuntimeInitialized: function() {
196+
statusEl.textContent = 'Ready';
197+
statusEl.className = 'ready';
198+
btnRun.disabled = false;
199+
}
200+
};
201+
202+
function runCode() {
203+
var code = inputEl.value;
204+
if (!code.trim()) return;
205+
206+
appendOutput('> Running...', 'output-line');
207+
try {
208+
Module.eval(code);
209+
} catch (e) {
210+
appendOutput('Error: ' + e.message, 'output-error');
211+
}
212+
appendOutput('', 'output-line');
213+
}
214+
215+
btnRun.addEventListener('click', runCode);
216+
btnClear.addEventListener('click', function() {
217+
outputEl.innerHTML = '';
218+
});
219+
220+
inputEl.addEventListener('keydown', function(e) {
221+
if (e.ctrlKey && e.key === 'Enter') {
222+
e.preventDefault();
223+
runCode();
224+
}
225+
// Tab key inserts spaces
226+
if (e.key === 'Tab') {
227+
e.preventDefault();
228+
var start = this.selectionStart;
229+
var end = this.selectionEnd;
230+
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
231+
this.selectionStart = this.selectionEnd = start + 2;
232+
}
233+
});
234+
</script>
235+
<script src="chaiscript.js"></script>
236+
</body>
237+
</html>

emscripten/chaiscript_em.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// This file is distributed under the BSD License.
2+
// See "license.txt" for details.
3+
// Copyright 2019, Rob Loach (https://github.com/RobLoach/ChaiScript.js)
4+
// Copyright 2009-2018, Jason Turner (jason@emptycrate.com)
5+
// http://www.chaiscript.com
6+
//
7+
// Emscripten/WebAssembly wrapper for ChaiScript.
8+
// Based on work by Rob Loach: https://github.com/RobLoach/ChaiScript.js
9+
10+
#include "chaiscript_eval.hpp"
11+
12+
#ifdef __EMSCRIPTEN__
13+
#include <emscripten/bind.h>
14+
15+
EMSCRIPTEN_BINDINGS(chaiscript) {
16+
emscripten::function("eval", &chaiscript_eval);
17+
emscripten::function("evalString", &chaiscript_eval_string);
18+
emscripten::function("evalBool", &chaiscript_eval_bool);
19+
emscripten::function("evalInt", &chaiscript_eval_int);
20+
emscripten::function("evalFloat", &chaiscript_eval_float);
21+
emscripten::function("evalDouble", &chaiscript_eval_double);
22+
}
23+
#endif

0 commit comments

Comments
 (0)