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:
Intubun
2026-05-11 14:20:47 -04:00
committed by R. Timothy Edwards
parent f4c22438c6
commit 8e8fada32f
8 changed files with 473 additions and 15 deletions
+100
View File
@@ -0,0 +1,100 @@
# Magic VLSI — Headless WASM Build
This toolchain builds Magic as a headless WebAssembly module using Emscripten.
X11, Tk, OpenGL, and readline are all disabled. The resulting `magic.js` /
`magic.wasm` pair can be loaded in Node.js, a browser, or a Web Worker.
## Quick start (npm package)
The easiest way to build and use the WASM module is through the npm package:
```bash
# Build magic.js + magic.wasm and copy them into npm/
bash npm/build.sh
# Run the test suite (extract, GDS, DRC, CIF)
npm --prefix npm test
```
See [`npm/examples/`](../../npm/examples/) for usage examples.
## Manual build
Prerequisites: an activated [emsdk](https://emscripten.org/docs/getting_started/downloads.html)
checkout (`emcc`, `emar`, `emranlib` on `PATH`), plus standard `make` and `gcc`.
```bash
# 1. Configure for Emscripten
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1 -g" \
emconfigure ./configure \
--without-cairo --without-opengl --without-x --without-tk --without-tcl \
--disable-readline --disable-compression \
--host=asmjs-unknown-emscripten \
--target=asmjs-unknown-emscripten
# 2. Append the Emscripten-specific make settings
cat toolchains/emscripten/defs.mak >> defs.mak
# 3. Build
emmake make depend
emmake make -j$(nproc) modules libs
emmake make techs
emmake make mains
```
The outputs are `magic/magic.js` and `magic/magic.wasm`.
## Embedded files
The following runtime files are baked directly into the WASM binary via
Emscripten's `--embed-file` mechanism and are available at startup without
any host filesystem access:
| Host path | VFS path |
|-----------|----------|
| `scmos/` | `/magic/sys/current/` |
| `windows/windows7.glyphs` | `/magic/sys/windows7.glyphs` |
| `windows/windows7.glyphs` | `/magic/sys/bw.glyphs` |
To embed a custom technology file, add an `--embed-file` entry to
`TOP_EXTRA_LIBS` in [`defs.mak`](defs.mak).
## Exported C API
The WASM module exports four functions:
| Function | Description |
|----------|-------------|
| `magic_wasm_init()` | Initialize Magic (idempotent — safe to call multiple times). Returns 0 on success. |
| `magic_wasm_run_command(const char *cmd)` | Dispatch one Magic command. Calls `magic_wasm_init()` automatically if needed. Returns 0 on success. |
| `magic_wasm_source_file(const char *path)` | Read and execute a command file from the virtual filesystem. |
| `magic_wasm_update()` | Drive a display-update cycle. No-op in headless builds (null display suspends all redraws). |
### JavaScript usage
```js
import createMagic from 'magic-vlsi-wasm';
const { runCommand, FS } = await createMagic();
// Write a layout file into the virtual filesystem
FS.writeFile('/work/inv.mag', layoutBytes);
// Run Magic commands
runCommand('tech load sky130A');
runCommand('load /work/inv');
runCommand('gds write /work/inv');
// Read the result back out
const gdsBytes = FS.readFile('/work/inv.gds');
```
## Notes
- `CAD_ROOT` is automatically set to `/` so that embedded system files are
resolved under `/magic/sys/`.
- The null display driver (`-d null`) sets `GrDisplayStatus = DISPLAY_SUSPEND`,
which causes `WindUpdate` to return immediately without invoking any display
callbacks. This is what makes the WASM build safe to run without a screen.
- All POSIX signal/timer APIs (`setitimer`, `SIGALRM`, `fcntl`) are compiled
out under `__EMSCRIPTEN__`; the display progress timer becomes a no-op.
+37
View File
@@ -0,0 +1,37 @@
# WASM-specific make additions — append to the configure-generated defs.mak.
#
# Usage (matches what the CI workflow does):
#
# source <emsdk>/emsdk_env.sh
# CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1 -g" \
# emconfigure ./configure --without-cairo --without-opengl --without-x \
# --without-tk --without-tcl \
# --disable-readline --disable-compression \
# --target=asmjs-unknown-emscripten
# cat toolchains/emscripten/defs.mak >> defs.mak
# emmake make depend
# emmake make -j$(nproc) modules libs
# emmake make techs
# emmake make mains
# Activate the WASM link target in magic/Makefile.
MAKE_WASM = 1
# Emscripten linker flags.
# The link step runs from the magic/ subdirectory, so embed-file paths
# are relative to that directory (../scmos, ../windows/...).
TOP_EXTRA_LIBS += \
-sWASM=1 \
-sMODULARIZE=1 \
-sEXPORT_ES6=1 \
-sEXPORTED_FUNCTIONS=_magic_wasm_init,_magic_wasm_run_command,_magic_wasm_source_file,_magic_wasm_update \
-sEXPORTED_RUNTIME_METHODS=cwrap,ccall,FS,setValue,getValue \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=33554432 \
-sSTACK_SIZE=5242880 \
-sASSERTIONS=1 \
-sENVIRONMENT=node,web,worker \
-sFORCE_FILESYSTEM=1 \
--embed-file ../scmos@/magic/sys/current \
--embed-file ../windows/windows7.glyphs@/magic/sys/windows7.glyphs \
--embed-file ../windows/windows7.glyphs@/magic/sys/bw.glyphs
+137
View File
@@ -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."