From 8e8fada32ff8cbfe8f137ab8dbb7ef351ad552f7 Mon Sep 17 00:00:00 2001 From: Intubun <41478036+Intubun@users.noreply.github.com> Date: Mon, 4 May 2026 13:31:41 +0200 Subject: [PATCH] Add WASM entry point and Emscripten build wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 48 ++++++++-- magic/Makefile | 17 +++- magic/magicWasm.c | 122 +++++++++++++++++++++++++ toolchains/emscripten/README.md | 100 ++++++++++++++++++++ toolchains/emscripten/defs.mak | 37 ++++++++ toolchains/emscripten/post-build.sh | 137 ++++++++++++++++++++++++++++ utils/main.c | 26 +++++- utils/main.h | 1 + 8 files changed, 473 insertions(+), 15 deletions(-) create mode 100644 magic/magicWasm.c create mode 100644 toolchains/emscripten/README.md create mode 100644 toolchains/emscripten/defs.mak create mode 100755 toolchains/emscripten/post-build.sh diff --git a/.gitignore b/.gitignore index 0323a924..c8beb02b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,33 @@ +# Autoconf / configure outputs defs.mak -*/Depend +!toolchains/emscripten/defs.mak config.cache config.log scripts/config.log scripts/config.status scripts/defs.mak -.*.swp -*.o -*.so -*~ -scmos/cif_template/objs/* -database/database.h install.log -magic/proto.magicrc make.log +reconfigure.sh + +# Compiled objects / libraries +*.o +*.a +*.so +*/Depend +database/database.h + +# Editor / OS cruft +.*.swp +.*.swo +*~ +.DS_Store +.vscode/ +.idea/ + +# Magic runtime-generated files +magic/proto.magicrc +scmos/cif_template/objs/* scmos/gdsquery.tech scmos/minimum.tech scmos/scmos-sub.tech @@ -21,14 +35,28 @@ scmos/scmos-tm.tech scmos/scmos.tech scmos/scmosWR.tech scmos/nmos.tech + +# Native build artifacts +magic/magic +magic/tclmagic.dylib tcltk/magic.sh tcltk/magic.tcl tcltk/magicdnull tcltk/magicexec tcltk/ext2spice.sh tcltk/ext2sim.sh -magic/tclmagic.dylib tcltk/magicdnull.dSYM/ tcltk/magicexec.dSYM/ -reconfigure.sh pfx/ + +# WASM build artifacts +magic/magic.js +magic/magic.js.symbols +magic/magic.symbols +magic/magic.wasm +net2ir/net2ir +net2ir/net2ir.js +net2ir/net2ir.wasm + +# Generated test output +npm/examples/output/ diff --git a/magic/Makefile b/magic/Makefile index 40dfc655..87e23b1d 100644 --- a/magic/Makefile +++ b/magic/Makefile @@ -4,10 +4,14 @@ MODULE = magic MAGICDIR = .. -SRCS = magicTop.c include ${MAGICDIR}/defs.mak +SRCS = magicTop.c +ifeq (${MAKE_WASM},1) +SRCS += magicWasm.c +endif + EXTRA_LIBS = ${MAGICDIR}/bplane/libbplane.o \ ${MAGICDIR}/cmwind/libcmwind.o \ ${MAGICDIR}/commands/libcommands.o \ @@ -37,7 +41,18 @@ LIBS += ${GR_LIBS} ${READLINE_LIBS} -lm ${LD_EXTRA_LIBS} \ ${OA_LIBS} ${ZLIB_FLAG} ${TOP_EXTRA_LIBS} CLEANS += tclmagic${SHDLIB_EXT} libtclmagic${SHDLIB_EXT}.a proto.magicrc +ifeq (${MAKE_WASM},1) +magic: magic.js +magic.js: lib${MODULE}.o ${EXTRA_LIBS} + @echo --- building main magic WASM + ${RM} magic.js magic.wasm + ${CC} ${CFLAGS} ${CPPFLAGS} ${DFLAGS} lib${MODULE}.o ${EXTRA_LIBS} -o magic.js ${LIBS} +endif + main: magic proto.magicrc +ifeq (${MAKE_WASM},1) + @bash ${MAGICDIR}/toolchains/emscripten/post-build.sh magic.js magic.wasm +endif tcl-main: tclmagic${SHDLIB_EXT} proto.magicrc diff --git a/magic/magicWasm.c b/magic/magicWasm.c new file mode 100644 index 00000000..e8a116a9 --- /dev/null +++ b/magic/magicWasm.c @@ -0,0 +1,122 @@ +/* + * magicWasm.c -- + * + * Headless Emscripten entry points for running Magic without a + * terminal-driven event loop. + */ + +#include +#include + +#include "utils/main.h" +#include "utils/magic.h" +#include "utils/paths.h" +#include "textio/textio.h" +#include "textio/txcommands.h" +#include "utils/utils.h" +#include "windows/windows.h" +#include "graphics/graphics.h" + +#ifdef __EMSCRIPTEN__ +#include +#else +#define EMSCRIPTEN_KEEPALIVE +#endif + +static int +magicWasmEnsureCadRoot(void) +{ + if (getenv("CAD_ROOT") == NULL) + { + if (setenv("CAD_ROOT", "/", 0) != 0) + { + TxError("Failed to set CAD_ROOT for the WASM runtime.\n"); + return -1; + } + } + + return 0; +} + +EMSCRIPTEN_KEEPALIVE int +magic_wasm_init(void) +{ + static char *argv[] = { + "magic", + "-d", + "null", + "-T", + "minimum", + NULL + }; + + if (magicWasmEnsureCadRoot() != 0) + return -1; + + return magicMainInit(5, argv); +} + +EMSCRIPTEN_KEEPALIVE int +magic_wasm_run_command(const char *command) +{ + int status; + + status = magic_wasm_init(); + if (status != 0) + return status; + + if ((command == NULL) || (*command == '\0')) + return 0; + + /* Set the current point to the center of the screen so that + * WindSendCommand routes the command to the layout window client + * (not the window-management border client which handles point 0,0). + */ + TxSetPoint(GrScreenRect.r_xtop / 2, GrScreenRect.r_ytop / 2, + WIND_UNKNOWN_WINDOW); + + return TxDispatchString(command, FALSE); +} + +EMSCRIPTEN_KEEPALIVE int +magic_wasm_source_file(const char *path) +{ + FILE *f; + int status; + + status = magic_wasm_init(); + if (status != 0) + return status; + + if ((path == NULL) || (*path == '\0')) + return -1; + + f = PaOpen((char *)path, "r", (char *)NULL, ".", (char *)NULL, + (char **)NULL); + if (f == NULL) + { + TxError("Unable to open command file \"%s\".\n", path); + return -1; + } + + /* Set the current point to the centre of the screen so that + * WindSendCommand routes all commands from the file to the layout + * window client, just as magic_wasm_run_command does for single + * commands. Without this, commands arrive with point (0,0) and + * end up in the border/windClient context where most commands are + * unknown. + */ + TxSetPoint(GrScreenRect.r_xtop / 2, GrScreenRect.r_ytop / 2, + WIND_UNKNOWN_WINDOW); + + TxDispatch(f); + fclose(f); + return 0; +} + +EMSCRIPTEN_KEEPALIVE void +magic_wasm_update(void) +{ + if (magic_wasm_init() == 0) + WindUpdate(); +} diff --git a/toolchains/emscripten/README.md b/toolchains/emscripten/README.md new file mode 100644 index 00000000..eb89a1d7 --- /dev/null +++ b/toolchains/emscripten/README.md @@ -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. diff --git a/toolchains/emscripten/defs.mak b/toolchains/emscripten/defs.mak new file mode 100644 index 00000000..dcace819 --- /dev/null +++ b/toolchains/emscripten/defs.mak @@ -0,0 +1,37 @@ +# WASM-specific make additions — append to the configure-generated defs.mak. +# +# Usage (matches what the CI workflow does): +# +# source /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 diff --git a/toolchains/emscripten/post-build.sh b/toolchains/emscripten/post-build.sh new file mode 100755 index 00000000..4932fa09 --- /dev/null +++ b/toolchains/emscripten/post-build.sh @@ -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 +# +# 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 " >&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." diff --git a/utils/main.c b/utils/main.c index c2ed8682..fe679d0f 100644 --- a/utils/main.c +++ b/utils/main.c @@ -161,6 +161,8 @@ global char *MainMouseFile = NULL; global char *MainDisplayType = NULL; global char *MainMonType = NULL; +static bool MagicIsInitialized = FALSE; + /* Copyright notice for the binary file. */ global char *MainCopyright = "\n--- MAGIC: Copyright (C) 1985, 1990 " @@ -1286,13 +1288,29 @@ magicMain(argc, argv) { int rstatus; - if ((rstatus = mainInitBeforeArgs(argc, argv)) != 0) MainExit(rstatus); - if ((rstatus = mainDoArgs(argc, argv)) != 0) MainExit(rstatus); - if ((rstatus = mainInitAfterArgs()) != 0) MainExit(rstatus); - if ((rstatus = mainInitFinal()) != 0) MainExit(rstatus); + if ((rstatus = magicMainInit(argc, argv)) != 0) MainExit(rstatus); TxDispatch( (FILE *) NULL); mainFinished(); } +int +magicMainInit(argc, argv) + int argc; + char *argv[]; +{ + int rstatus; + + if (MagicIsInitialized) + return 0; + + if ((rstatus = mainInitBeforeArgs(argc, argv)) != 0) return rstatus; + if ((rstatus = mainDoArgs(argc, argv)) != 0) return rstatus; + if ((rstatus = mainInitAfterArgs()) != 0) return rstatus; + if ((rstatus = mainInitFinal()) != 0) return rstatus; + + MagicIsInitialized = TRUE; + return 0; +} + diff --git a/utils/main.h b/utils/main.h index df3948b9..3234ca1a 100644 --- a/utils/main.h +++ b/utils/main.h @@ -94,6 +94,7 @@ extern Transform RootToEditTransform; extern void MainExit(int) ATTR_NORETURN; /* a way of exiting that cleans up after itself */ extern void magicMain(); +extern int magicMainInit(int argc, char *argv[]); /* C99 compat */ extern int mainInitBeforeArgs();