mirror of
https://github.com/RTimothyEdwards/magic.git
synced 2026-08-28 17:15:13 +02:00
Add WASM entry point and Emscripten build wiring
The pieces that make Magic actually buildable as a WASM library.
* magic/magicWasm.c — new headless entry point exporting four
functions used by the JS wrapper:
- magic_wasm_init() idempotent initialisation
- magic_wasm_run_command(s) dispatch one Magic command
- magic_wasm_source_file(p) execute a script from the VFS
- magic_wasm_update() drive a display-update cycle
Sets CAD_ROOT=/ if unset, so embedded technology files under
/magic/sys/ resolve correctly. Centers the command point inside
GrScreenRect so commands route to the layout window client
rather than the border/window-management client.
* utils/main.c, utils/main.h — split magicMain() into magicMainInit()
+ the dispatch loop. magicMainInit is idempotent (a static flag
guards against re-initialisation) so JS callers can call any of
the four wasm entry points first without sequencing.
* magic/Makefile — adds the WASM link target, gated by MAKE_WASM=1
set from toolchains/emscripten/defs.mak. Conditionally compiles
magicWasm.c into the main binary, links to magic.js and runs
post-build.sh on the result.
* toolchains/emscripten/defs.mak — Emscripten linker flags (WASM=1,
MODULARIZE, EXPORT_ES6, ALLOW_MEMORY_GROWTH, INITIAL_MEMORY=32M,
STACK_SIZE=5M), the four EXPORTED_FUNCTIONS, and the embed-file
bindings for the technology files under /magic/sys/.
* toolchains/emscripten/post-build.sh — patches Emscripten's ESM
output so it works in pure Node.js ESM: aliases require()
through createRequire, injects __filename / __dirname shims,
and resyncs the ___emscripten_embedded_file_data constant from
the wasm global section if Emscripten emitted a stale value.
Idempotent and pinned to emsdk 3.1.56 (see WARNING in the
header).
* toolchains/emscripten/README.md — full build documentation:
quick-start via npm/build.sh, manual build, list of embedded
files, exported C API, JavaScript usage example, and notes on
CAD_ROOT, DISPLAY_SUSPEND, and the signal-API stubs.
* .gitignore — adds the WASM artefacts (magic.js, magic.wasm,
magic.symbols), tightens the editor/OS cruft list, and keeps
toolchains/emscripten/defs.mak tracked despite the `defs.mak`
ignore rule.
This commit is contained in:
committed by
R. Timothy Edwards
parent
f4c22438c6
commit
8e8fada32f
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# Post-process Emscripten output for Node.js 22+ ES module compatibility.
|
||||
#
|
||||
# Emscripten (3.1.x) produces ES module output that still references
|
||||
# `require()`, `__dirname`, and `__filename` — all of which are undefined
|
||||
# in pure ESM. This script patches magic.js to:
|
||||
# 1. Alias every `require("x")` call to `___cr("x")` (createRequire).
|
||||
# 2. Inject ESM-safe `__filename` / `__dirname` / `require` at module top.
|
||||
# 3. Sync `___emscripten_embedded_file_data` from the .wasm global.
|
||||
#
|
||||
# Usage: post-build.sh <magic.js> <magic.wasm>
|
||||
#
|
||||
# Idempotent — safe to run multiple times.
|
||||
#
|
||||
# WARNING: These patches depend on exact text patterns emitted by a specific
|
||||
# Emscripten version. If you upgrade emsdk, verify the patches still apply
|
||||
# (see .github/workflows/main.yml for the pinned version).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <magic.js> <magic.wasm>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JS=$1
|
||||
WASM=$2
|
||||
|
||||
if [ ! -f "$JS" ] || [ ! -f "$WASM" ]; then
|
||||
echo "Error: $JS or $WASM not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Portable in-place sed: BSD (macOS) and GNU diverge on -i.
|
||||
# Using a temp file + mv keeps both happy without relying on -i at all.
|
||||
_sed_inplace() {
|
||||
local expr=$1 file=$2
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
sed "$expr" "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
# --- 1 & 2. ES module compatibility -----------------------------------------
|
||||
if ! grep -q 'createRequire' "$JS"; then
|
||||
echo "[post-build] Injecting ESM createRequire + __filename/__dirname shims"
|
||||
_sed_inplace 's/require("\([^"]*\)")/___cr("\1")/g' "$JS"
|
||||
_sed_inplace '1s|^|\nimport{createRequire as ___cr}from"module";\nimport{fileURLToPath as ___fup}from"url";\nimport{dirname as ___dn}from"path";\nvar __filename=___fup(import.meta.url);\nvar __dirname=___dn(__filename);\n|' "$JS"
|
||||
fi
|
||||
|
||||
# Ensure a top-level `require` binding exists (Emscripten's environment probe
|
||||
# does `typeof require == "function"`, which would otherwise be "undefined").
|
||||
# Skip if Emscripten already emits one (newer versions do).
|
||||
if ! grep -qE 'var[[:space:]]+require[[:space:]]*=' "$JS" \
|
||||
&& grep -q 'import{createRequire as ___cr}from"module";' "$JS"; then
|
||||
echo "[post-build] Adding top-level require binding"
|
||||
_sed_inplace 's|import{createRequire as ___cr}from"module";|import{createRequire as ___cr}from"module";var require=___cr(import.meta.url);|' "$JS"
|
||||
fi
|
||||
|
||||
# --- 3. Sync ___emscripten_embedded_file_data from .wasm --------------------
|
||||
# Emscripten sometimes bakes a stale constant into magic.js after an
|
||||
# incremental rebuild. Read the true value from the WASM global section
|
||||
# (section id 6, global index 1, i32.const opcode 0x41, signed LEB128).
|
||||
python3 - "$JS" "$WASM" <<'PY'
|
||||
import sys, re
|
||||
|
||||
js_path, wasm_path = sys.argv[1], sys.argv[2]
|
||||
data = open(wasm_path, 'rb').read()
|
||||
|
||||
def read_uleb(buf, pos):
|
||||
val = shift = 0
|
||||
while True:
|
||||
b = buf[pos]; pos += 1
|
||||
val |= (b & 0x7f) << shift
|
||||
shift += 7
|
||||
if not (b & 0x80):
|
||||
return val, pos
|
||||
|
||||
def read_sleb(buf, pos):
|
||||
val = shift = 0
|
||||
while True:
|
||||
b = buf[pos]; pos += 1
|
||||
val |= (b & 0x7f) << shift
|
||||
shift += 7
|
||||
if not (b & 0x80):
|
||||
if b & 0x40:
|
||||
val |= (~0) << shift
|
||||
return val & 0xffffffff, pos
|
||||
|
||||
# Walk sections looking for section id 6 (Global)
|
||||
pos = 8 # skip magic + version
|
||||
actual = None
|
||||
while pos < len(data):
|
||||
sid = data[pos]; pos += 1
|
||||
size, pos = read_uleb(data, pos)
|
||||
end = pos + size
|
||||
if sid == 6:
|
||||
count, p = read_uleb(data, pos)
|
||||
for gi in range(count):
|
||||
p += 2 # valtype + mutability
|
||||
opcode = data[p]; p += 1
|
||||
if opcode == 0x41: # i32.const
|
||||
val, p = read_sleb(data, p)
|
||||
if gi == 1:
|
||||
actual = val
|
||||
break
|
||||
p += 1 # skip end opcode
|
||||
break
|
||||
pos = end
|
||||
|
||||
if actual is None:
|
||||
sys.stderr.write(
|
||||
'[post-build] WARN: could not parse global index 1 from wasm; '
|
||||
'skipping ___emscripten_embedded_file_data sync\n')
|
||||
sys.exit(0)
|
||||
|
||||
js = open(js_path).read()
|
||||
m = re.search(r'Module\[.___emscripten_embedded_file_data.\]=(\d+)', js)
|
||||
if not m:
|
||||
# Emscripten may have fixed the stale-constant bug in a newer version.
|
||||
# Not fatal, but worth surfacing so we can drop this patch eventually.
|
||||
sys.stderr.write(
|
||||
'[post-build] INFO: ___emscripten_embedded_file_data not present; '
|
||||
'sync patch no longer applies (likely fixed upstream)\n')
|
||||
sys.exit(0)
|
||||
|
||||
old = int(m.group(1))
|
||||
if old != actual:
|
||||
print(f'[post-build] Fixing ___emscripten_embedded_file_data: {old} -> {actual}')
|
||||
js = js.replace(
|
||||
f'Module["___emscripten_embedded_file_data"]={old}',
|
||||
f'Module["___emscripten_embedded_file_data"]={actual}',
|
||||
)
|
||||
open(js_path, 'w').write(js)
|
||||
PY
|
||||
|
||||
echo "[post-build] Done."
|
||||
Reference in New Issue
Block a user