In-memory workspace
— the engine reads and writes here; nothing touches your disk
Drop files anywhere on this card (64 MB per file). Scripts see them by name:
swmm.run("yourmodel.inp", "yourmodel.rpt", "yourmodel.out").
Each Run works on a snapshot taken when you press Run; files you add mid-run
join the next one.
🔍This tab documents the swmm.* binding
the playground gives your Lua — and how it maps, one to one, onto the C toolkit API
exported by swmm5.dll / libswmm5.so. Everything here was
read out of the EPA 5.2.4 source, not from folklore.
How the binding works
Your Lua 5.3 scriptcalls swmm.open(), swmm.step(), swmm.set()…
→
Fengari VMa Lua 5.3 implementation in JavaScript; each bridge
function is registered with lua_pushjsfunction — the same C-API
pattern a native binding uses
→
Toolkit callsstrings and doubles are marshalled through
malloc/free into wasm linear memory,
then the exported swmm_* functions run
→
EPA SWMM 5.2.4the untouched solver, compiled
wasm32-wasi; its fopen/fprintf land in…
→
In-memory FSa WASI filesystem shim: the workspace you see on
the Playground tab is the engine's working directory
This is the browser twin of the LuaJIT-FFI approach on the Desktop Lua
tab: same lifecycle, same property codes, same error discipline — so a script sketched
here ports to a desktop LuaJIT + swmm5 shared library almost line for line.
swmm.* — the binding, function by function
Lua
C toolkit call (swmm5.h, 5.2.4)
Notes
swmm.version()
int swmm_getVersion(void)
52004 for this build
swmm.run(inp, rpt, out)
int swmm_run(f1, f2, f3)
batch: opens, simulates, reports and closes by itself
swmm.open(inp, rpt, out)
int swmm_open(f1, f2, f3)
begin the step lifecycle
swmm.start(save)
int swmm_start(int saveFlag)
save = 1 writes the binary .out
swmm.step()
int swmm_step(double *elapsed)
returns elapsed decimal days; 0 means finished
swmm.stride(n)
int swmm_stride(int, double *)
n routing steps at once
swmm.finish() · swmm["end"]()
int swmm_end(void)
end is a Lua keyword, so dot-syntax needs
finish
swmm.report()
int swmm_report(void)
writes the .rpt summary tables
swmm.close()
int swmm_close(void)
only for the open/start/step lifecycle — see rules below
swmm.mass_bal()
int swmm_getMassBalErr(float*, float*, float*)
three returns: runoff %, routing %, quality %
swmm.warnings()
int swmm_getWarnings(void)
swmm.error_message()
int swmm_getError(char*, int)
the bridge also raises it for you on any nonzero code
swmm.count(objType)
int swmm_getCount(int)
swmm.index(objType, name)
int swmm_getIndex(int, const char*)
0-based, exactly like the C API; raises if not found
swmm.name(objType, i)
void swmm_getName(int, int, char*, int)
swmm.get(prop, i)
double swmm_getValue(int property, int index)
index optional for SYSTEM codes
swmm.set(prop, i, v)
void swmm_setValue(int, int, double)
only the codes in the “settable” table act
swmm.saved(prop, i, period)
double swmm_getSavedValue(int, int, int)
period starts at 1; window: after finish, before close
swmm.writeline(s)
void swmm_writeLine(const char*)
drops a line into the .rpt
swmm.decode_date(d)
void swmm_decodeDate(double, int*×7)
7 returns: y, mo, d, h, mi, s, dow
swmm.elapsed()
swmm_getValue(ELAPSEDTIME, 0)
convenience
Playground extras (not part of the C API)
files.read / write / exists / list
the in-memory workspace; write an .inp, then open it by name
stream values to the live chart; unit "flow" or "depth" picks the panel
Property codes (swmm5.h, verbatim)
Object types
constant
value
swmm.GAGE
0
swmm.SUBCATCH
1
swmm.NODE
2
swmm.LINK
3
swmm.SYSTEM
100
System (index ignored)
constant
value
STARTDATE
0
CURRENTDATE
1
ELAPSEDTIME
2
ROUTESTEP
3
MAXROUTESTEP
4
REPORTSTEP
5
TOTALSTEPS
6
NOREPORT
7
FLOWUNITS
8
Gage / subcatchment
constant
value
GAGE_RAINFALL
100
SUBCATCH_AREA
200
SUBCATCH_RAINGAGE
201
SUBCATCH_RAINFALL
202
SUBCATCH_EVAP
203
SUBCATCH_INFIL
204
SUBCATCH_RUNOFF
205
SUBCATCH_RPTFLAG
206
Node
constant
value
NODE_TYPE
300
NODE_ELEV
301
NODE_MAXDEPTH
302
NODE_DEPTH
303
NODE_HEAD
304
NODE_VOLUME
305
NODE_LATFLOW
306
NODE_INFLOW
307
NODE_OVERFLOW
308
NODE_RPTFLAG
309
Link
constant
value
LINK_TYPE
400
LINK_NODE1
401
LINK_NODE2
402
LINK_LENGTH
403
LINK_SLOPE
404
LINK_FULLDEPTH
405
LINK_FULLFLOW
406
LINK_SETTING
407
LINK_TIMEOPEN
408
LINK_TIMECLOSED
409
LINK_FLOW
410
LINK_DEPTH
411
LINK_VELOCITY
412
LINK_TOPWIDTH
413
LINK_RPTFLAG
414
What swmm.set() actually accepts
Read straight from swmm_setValue() in
swmm5.c (5.2.4) — anything else is silently ignored:
code
effect
when
GAGE_RAINFALL
override a gage's rainfall intensity
mid-run
NODE_LATFLOW
set a node's lateral inflow
mid-run
NODE_HEAD
set an outfall's stage
mid-run
LINK_SETTING
open/close/throttle a link — the RTC workhorse
(sample 03)
mid-run
ROUTESTEP
change the routing time step
mid-run
REPORTSTEP · NOREPORT · *_RPTFLAG
reporting control
before swmm.start()
Three lifecycle rules this playground enforces
1 · Never swmm.close() after swmm.run(). swmm_run closes the
project itself; a second close double-fcloses the report file. On glibc that is quiet
undefined behaviour — in WebAssembly it is a hard trap (“null function or function
signature mismatch”), measured on this exact build. The bridge turns it into a friendly
Lua error instead.
2 · swmm.saved() has a window. It reads the binary .out only while
the project is open and the run has ended: after swmm.finish(),
before swmm.close(). Sample 05 lives in that window. The same
applies to swmm.mass_bal() — after swmm.run()
it reads zeros by design, so batch mode parses the .rpt (sample 01).
3 · swmm.step() speaks decimal days. Multiply by 24 for hours; a
return of 0 means the simulation is finished — break before plotting it.
🖥️Everything on the Playground tab runs against the
same toolkit API your desktop has. This tab is the take-home: batch driving with plain Lua,
and the LuaJIT-FFI binding against the official
swmm5.dll / libswmm5.so.
Which approach do you need?
Approach
Best for
Needs
1 · Call the executable
batch runs, Monte Carlo, sensitivity sweeps,
.inp generation
any Lua 5.x + the runswmm/swmm5 CLI
2 · LuaJIT FFI on the shared library
real-time control,
step-by-step coupling, live parameter changes
LuaJIT + swmm5.dll / libswmm5.so
(5.2+)
3 · This playground
teaching, sharing runnable snippets, trying RTC
rules with zero install
a browser tab
4 · Lua as the model generator
tables → [SECTIONS] → .inp, feeding
any of the above (sample 04)
any Lua
Approach 1 — batch mode with plain Lua
-- run the official CLI and read the report back (any Lua 5.x)
local inp, rpt, out = "model.inp", "model.rpt", "model.out"
-- Windows: runswmm.exe (ships with the EPA installer)
-- Linux: runswmm from your own build (see the build card below)
local cmd = ('runswmm "%s" "%s" "%s"'):format(inp, rpt, out)
local ok = os.execute(cmd)
assert(ok, "SWMM run failed")
-- pull both continuity errors out of the .rpt
local f = assert(io.open(rpt, "r"))
local text = f:read("*a"); f:close()
for v in text:gmatch("Continuity Error %(%%%)[%s%.]*(-?%d+%.%d+)") do
print("continuity:", v .. " %")
end
For hundreds of runs, generate each .inp from Lua tables (sample 04 shows
the pattern), spawn with a process library (luaposix, lua-subprocess) instead of
os.execute, and read the binary .out with a small reader — its layout is the classic
SWMM output format, magic number 516114522.
Approach 2 — LuaJIT FFI on the official shared library
Header check first. A widely-circulated snippet declares
swmm_getNodeResult() / SM_* enums —
those belong to the OpenWaterAnalytics extended toolkit (toolkit.h), which the
official EPA swmm5.dll does not export. The functions
below are the EPA 5.2+ API from swmm5.h — the same ones this playground binds — and they
work on the DLL that ships with the EPA installer.
local ffi = require("ffi") -- LuaJIT
ffi.cdef[[
int swmm_run(const char *f1, const char *f2, const char *f3);
int swmm_open(const char *f1, const char *f2, const char *f3);
int swmm_start(int saveFlag);
int swmm_step(double *elapsedTime);
int swmm_stride(int strideStep, double *elapsedTime);
int swmm_end(void);
int swmm_report(void);
int swmm_close(void);
int swmm_getMassBalErr(float *runoffErr, float *flowErr, float *qualErr);
int swmm_getVersion(void);
int swmm_getError(char *errMsg, int msgLen);
int swmm_getWarnings(void);
int swmm_getCount(int objType);
void swmm_getName(int objType, int index, char *name, int size);
int swmm_getIndex(int objType, const char *name);
double swmm_getValue(int property, int index);
void swmm_setValue(int property, int index, double value);
double swmm_getSavedValue(int property, int index, int period);
void swmm_writeLine(const char *line);
void swmm_decodeDate(double date, int *y, int *mo, int *d,
int *h, int *mi, int *s, int *dow);
]]
local swmm = ffi.load("swmm5") -- swmm5.dll / libswmm5.so on the path
-- property codes (swmm5.h): NODE=2, LINK=3,
-- NODE_DEPTH=303, LINK_FLOW=410, LINK_SETTING=407
local NODE, LINK = 2, 3
local NODE_DEPTH, LINK_FLOW, LINK_SETTING = 303, 410, 407
assert(swmm.swmm_open("model.inp", "model.rpt", "model.out") == 0)
assert(swmm.swmm_start(1) == 0)
local pond = swmm.swmm_getIndex(NODE, "POND")
local gate = swmm.swmm_getIndex(LINK, "OR1")
local elapsed = ffi.new("double[1]")
while true do
if swmm.swmm_step(elapsed) ~= 0 or elapsed[0] <= 0 then break end
local d = swmm.swmm_getValue(NODE_DEPTH, pond)
-- the same rule sample 03 runs in your browser:
swmm.swmm_setValue(LINK_SETTING, gate, d > 6.5 and 1.0 or 0.15)
end
swmm.swmm_end(); swmm.swmm_report(); swmm.swmm_close()
LuaJIT implements Lua 5.1; the playground runs Lua 5.3
(Fengari). Watch the usual gaps when porting: integer division
(//), bitwise operators, and
goto. The swmm.* call sequence itself is identical.
Building the shared library
# Linux / macOS — one command, from the EPA source tree (tag v5.2.4):
gcc -O2 -fPIC -shared -fopenmp \
src/solver/*.c -Isrc/solver -Isrc/solver/include -lm \
-o libswmm5.so
# Windows — the EPA installer already ships swmm5.dll (5.2.x exports the
# full swmm_getValue/setValue API); or build with CMake from the same repo:
# cmake -B build ; cmake --build build --config Release
Source: github.com/USEPA/Stormwater-Management-Model. The
simplified toolkit API used on this page (getValue / setValue / getCount…)
landed in 5.2.0 — a 5.1-era DLL only has the run/open/step lifecycle.
🧪Every number below was measured on the exact engine
build embedded in this file — a 68-check node test suite ran the same core, the same demo
model and the same six sample scripts before anything was written down.
Verified on this build
Claim
Measured
Engine identity
swmm_getVersion() = 52004; .rpt header
“EPA STORM WATER MANAGEMENT MODEL — VERSION 5.2”
gate wide open: peak outflow
5.04 cfs, pond 2.01 ft → Lua-controlled: 0.68 cfs, pond 3.20 ft —
an 86 % peak reduction with 4.8 ft of freeboard left
Sample 06 dwell control
under pulsed rainfall a bare threshold rule
moves the gate 41 times; the persistence + minimum-dwell controller
(reading LINK_TIMEOPEN / LINK_TIMECLOSED) does the same job in
9 movements at a similar peak depth (2.00 vs 2.28 ft)
Determinism
run A → run B → run A in one engine instance:
A and the repeat of A produce byte-identical .out files
Runaway scripts
while true do end stopped by the
instruction-count hook in < 3 s; Stop rebuilds the worker
Speed
the full 5,760-step demo simulates in ≈ 0.4 s (node,
this wasm)
Corrections to the source note
This app grew out of an AI-written note on using Lua with SWMM5.
The note's architecture (batch → FFI → hybrid) is sound; three details needed fixing:
Claim in the note
Status
FFI example uses swmm_getNodeResult(index, 5, …) with
SM_* codes
corrected — that is the OWA extended
toolkit; the official EPA DLL exports swmm_getValue(property,
index) instead (Desktop Lua tab has the working cdef)
“The SWMM5 Rosetta Stone project includes a ~200 KB Lua engine implementation”
unverified — no such Lua engine is known to
exist in that project; treat the claim as hallucinated unless a source appears
LuaJIT recommended for the FFI path
kept —
with the note that LuaJIT is Lua 5.1 while this playground is Lua 5.3
(Fengari)
Provenance
Piece
Exactly what it is
Engine
EPA Stormwater-Management-Model, tag v5.2.4,
src/solver/*.c unmodified, plus a 2-function compatibility file
(mkstemp, realpath — both compiled
out of wasi-libc; SWMM needs the first for its scratch rainfall file and the second
only to echo a path)
Compile
clang 18, --target=wasm32-wasi
-mexec-model=reactor -O2, all 20 toolkit entry points +
malloc/free exported; payload checksum (SHA-256)
edf4f4eefc21dd862b89a183371d2934b998c324628cff38a8a7e35e4a811c7e — re-hashed in your browser at
load and the engine refuses to start on a mismatch. Being baked into the same file,
it proves integrity in transit, not authorship; the independent reference hash lives
in the swmm5.org post for this app
WASI runtime
@bjorn3/browser_wasi_shim 0.4.2 — a pure-JS WASI with an
in-memory filesystem; the Playground workspace is its preopened directory
Lua
Fengari 0.1.4 (fengari-web bundle) — Lua 5.3 semantics,
implemented in JavaScript; bridge functions registered through the Lua C API
(lua_pushjsfunction)
Verification
68-check node suite over this exact core: engine identity,
model census, lifecycle rules, both RTC comparisons, unit detection (CFS and CMS
models), rainfall override, generator round-trip, saved-results window, reuse
determinism, timeout and error paths — plus a 51-check end-to-end suite that extracts
the payloads back out of this very file and reruns the samples through them
Honest limits
Lua 5.3, not LuaJIT — no ffi,
no JIT speed; the engine does the heavy lifting, so sample runs still finish in
under a second.
io.open is not wired to the workspace — use
files.read / files.write. os.time
and friends work.
Scripts get 120 s per run before the instruction-count hook stops them; the Stop
button is the hard cancel (kills and rebuilds the worker).
One engine instance per script run. Within a run, repeated
open→close cycles are fine — verified byte-identical — but a wasm trap ends the
instance, exactly as the run-discipline notes for browser SWMM engines predict.
Everything is in-memory: reload the page and the workspace resets to
demo.inp. Download anything you want to keep. Uploads are capped at 64 MB per
file — bigger models belong on the desktop toolkit.
The simulator is fully self-contained — it runs offline and from file://
(where the checksum badge falls back to the build-time value, since
crypto.subtle needs a secure origin). Only the GIF recorder loads cdnjs
scripts.