Packed binary data. Native plugins. Static validation before execution. No garbage collector. Two-way C integration. A scripting language for C applications where native data can stay native — without turning every buffer into a runtime object.
First downloadable release expected late October 2026.
opengl_ffmpeg_script_driven_demo.dom — the script that produces the demo above — opens an OpenGL window, decodes a video stream with FFmpeg into a spinning textured cube, handles mouse input, and runs at 60 FPS. Every drawing decision lives in the source; there is no rendering engine hidden behind a high-level façade.
The excerpt below shows the spine: plugin declarations, the packed render state struct, the two callbacks the host dispatches into, and the main lifecycle. Geometry helpers, draw functions, and the per-frame renderer are omitted — see the full source for those (about 770 lines total).
/* The runtime knows almost nothing on its own. Each `plugin` line loads a shared library that contributes a namespace (gl, ffmpeg, ...). */ plugin "builtin:os"; plugin "builtin:struct"; plugin "../plugins/worker/WorkerPlugin"; plugin "../plugins/opengl/GlPlugin"; plugin "../plugins/ffmpeg/FfmpegPlugin"; plugin "../plugins/image/ImagePlugin"; #define WINDOW_WIDTH 1280 #define WINDOW_HEIGHT 720 #define VIDEO_WIDTH 640 #define VIDEO_HEIGHT 360 #define AUTO_SPEED_X 0.23 #define AUTO_SPEED_Y 0.37 #define AUTO_SPEED_Z 0.11 /* Packed struct: byte-precise layout, no padding surprises. The runtime can overlay this onto any blob with `$X assign RenderState;` and read typed fields without copying. */ struct RenderState packet { i32 WindowWidth; i32 WindowHeight; i32 VideoSlot; i32 LogoSlot; double CameraDistance; double ViewScale; double AngleX; double AngleY; double AngleZ; double SpeedX; double SpeedY; double SpeedZ; /* ... edge color, cube gap, line width ... */ }; /* The interactive state lives in the callback world -- the thread the mouse/resize and frame callbacks run on -- as a callback global. The main thread never names it: once per frame it calls Tick, which advances the rotation and writes a snapshot into the blob main lends. */ callback global blob $State[sizeof(RenderState)]; callback i32 Tick(ref blob $Snapshot) { $Snapshot assign RenderState; $State assign RenderState; /* ... advance AngleX/Y/Z by Speed * dt (clamped) ... */ $Snapshot.AngleX = $State.AngleX; /* ... copy the remaining fields into the snapshot ... */ return 1; } /* =========== Callbacks the host dispatches into =================== */ /* Called by the ffmpeg plugin for each decoded video frame. The frame bytes arrive in $FramePacket as a borrowed, mutable blob -- no allocation, no memcpy. We hand them straight to the GL texture slot and return; the plugin reclaims ownership the moment we exit. */ callback i32 OnVideoFrame(ref blob $FramePacket) { $State assign RenderState; gl.UpdateTexture($State.VideoSlot, $FramePacket); return 1; } /* The event packet the gl plugin hands to the callback: a packed struct over the same bytes, so `$EventPacket assign GlEvent;` gives typed fields with no copy. */ struct GlEvent packet { u8 magic0; u8 magic1; u8 magic2; u8 magic3; u8 version; u8 type; u8 button; u8 pad0; i16 x; i16 y; i16 dx; i16 dy; i16 wheel; u16 width; u16 height; }; /* Called by the gl plugin for each pumped input event. We pull the typed event view over the same packet bytes and update shared state. */ callback i32 OnOpenGlEvent(ref blob $EventPacket) { $State assign RenderState; $EventPacket assign GlEvent; if ($EventPacket.type == GL_EVENT_MOUSE_WHEEL) { $State.CameraDistance = $State.CameraDistance - (0.25 * (double)$EventPacket.wheel); $State.CameraDistance = ClampCameraDistance($State.CameraDistance); } /* ... mouse down / up / move handlers for left-drag rotation ... */ return 1; } /* =========== Entry point ========================================== */ i32 main() { i32 $Video = 0; blob $LogoPacket[0]; /* Open the GL window. gl.Open returns 0 if no display is available -- on a headless box the demo just exits cleanly. */ if (gl.Open(WINDOW_WIDTH, WINDOW_HEIGHT, "Domin OpenGL FFmpeg demo") == 0) { return 0; } /* main's own copy of the state: seeded here, then refreshed by Tick every frame. Configure carries it into the callback world once. */ blob $S[sizeof(RenderState)]; $S assign RenderState; $S.WindowWidth = WINDOW_WIDTH; $S.WindowHeight = WINDOW_HEIGHT; $S.CameraDistance = 4.2; $S.SpeedX = AUTO_SPEED_X; $S.SpeedY = AUTO_SPEED_Y; $S.SpeedZ = AUTO_SPEED_Z; $S.VideoSlot = gl.CreateTextureSlot(); $S.LogoSlot = gl.CreateTextureSlot(); /* ... starting angles, edge colors, cube gap ... */ worker.Call("Configure", ref $S, 1000); /* seed the callback world */ /* Load the logo and bind it to its texture slot. */ $LogoPacket = image.Load("Assets/Images/domin_logo.png"); gl.UpdateTexture($S.LogoSlot, $LogoPacket); /* Wire FFmpeg to deliver frames into OnVideoFrame at real-time pace. */ ffmpeg.SetOutputSize(VIDEO_WIDTH, VIDEO_HEIGHT); ffmpeg.SetRealtime(1); $Video = ffmpeg.Open("Assets/Videos/demo.mp4"); ffmpeg.StartAudio($Video); /* Main loop: pump input -> decode frame -> draw. */ while (gl.IsOpen() != 0) { gl.PumpEvents("OnOpenGlEvent"); if (gl.IsOpen() == 0) { break; } if (ffmpeg.DecodeNext($Video, "OnVideoFrame") == 0) { ffmpeg.Rewind($Video); } worker.Call("Tick", ref $S, 1000); /* advance + snapshot */ RenderScriptFrame(ref $S); /* draws the cube -- see full source */ } ffmpeg.Close($Video); gl.Close(); return 0; }
In most scripting languages the answer is a pipeline: parse → allocate → convert to runtime objects → script → marshal back → native. In DominScript the answer is: lay a typed view over the bytes you already have. blob → typed view → script.
/* 9 bytes on the wire, byte-precise: u8 u8 u16 i32 u8 */ struct SensorFrame packet { u8 version; u8 kind; u16 sensorId; i32 value; u8 flags; } /* The frame handler. $Request aliases the bytes the listener received. */ callback i32 OnFrame(ref blob $Request) { $Request assign SensorFrame; /* typed view over the bytes that arrived */ if ($Request.version != 1) { return 0; } /* validate */ printf("sensor=%d value=%d\n", $Request.sensorId, $Request.value); $Request.value = $Request.value * 2; /* modify in place */ $Request.flags = 0x80; /* ack bit */ $Request = $Request; /* reply = these bytes */ return 1; }
The runtime allocates nothing and copies nothing for a callback's ref blob. The plugin lends a pointer and a length — any memory it can point at: its own receive buffer (the TCP server reads the socket straight into it), a decoded frame (the ffmpeg plugin's frame slot), or the wrapped library's own buffer — and the script sees exactly those bytes, in place. A release hook hands ownership back the moment the callback returns. The principle is general: if the library guarantees that the area behind the pointer it handed over stays valid until the callback returns, the adapter plugin can wrap that very area into the blob and copy nothing. Where a library reuses its buffers on its own schedule or hands out non-contiguous data (a planar video frame), the plugin keeps one slot of its own and copies once into it — the runtime side stays copy-free either way. That is what makes an adapter plugin cheap: it wraps memory a .so/.dll already holds instead of re-creating it on the script side.
assign builds nothing. It gives the bytes a typed shape: $Request.value is a 4-byte read at a fixed offset.
Field access on a view that is too short is a runtime error with file and line, not a silent read past the end.
The whole script — handler and a client in one file — is Examples/packet_view_demo.dom; it is also a regression test and an example smoke recipe. The numbers above describe the shipped code path. The reply reassignment is the one copy of the round trip: it is the response contract.
None of these is unique on its own. Together they make one coherent model for scripting next to native code: the script and the native program do not have to live in two separate worlds.
blob is raw bytes. A packet struct is a byte-precise layout you lay over them with assign. Data that is already in the right shape is not parsed, not boxed, not re-encoded. The language makes zero-copy the natural path; a plugin that copies does so by its own choice.
No GC pauses, no hidden object conversion, no GC-managed object allocation hidden behind ordinary data access. Sized buffers, explicit casts, definite assignment. Data movement stays explicit — in a systems program you can tell what happens to the bytes.
Scripts call native plugins; native plugin threads can initiate callbacks, handing over a buffer by reference — script execution itself is serialized on the runtime's dispatcher thread. The plugin API is a small, versioned C ABI. A small existing C component can become a DominScript plugin in an afternoon.
Preprocessor, lexer, parser and binder run before execution: types, scopes, definite assignment, plugin signatures. The runtime keeps only the guards that cannot be static. Callbacks execute one at a time on a dispatcher thread, so script state needs no locks.
Without moving the data handling into a dynamic object model. That is the use case; these are its shapes.
Byte-precise schemas, packed structs, ref-style callbacks, zero-copy boundaries, a plugin architecture: each exists because a data collector, a broadcast statistics pipeline or a performance-critical client needed it. The philosophy, the design pillars and the seven things you notice in the first hour are on their own page.
The interpreter knows almost nothing on its own. Each capability is an independent shared library — load what you need, ignore the rest. 35 official plugins, 755 functions, every one documented — and the few things every script needs (output, blobs, structs, the OS) are built in.
→ See all 35 plugins and 755 functions · further plugins under consideration: log analysis · compression (gzip/zstd) · email (SMTP).
Need a capability that isn't shipping? Write your own plugin. DominScript ships with two complete skeletons you can copy as a starting point — one for event-driven plugins that call back into the script, and one for retain-style plugins that accept script-supplied data and keep their own copy. Fill in the parts specific to your task; the host integration, ABI, and lifecycle are already wired up.
A plugin is just a shared library (.so on Linux, .dll on Windows) exporting a fixed set of C entry points. Once compiled, a script picks it up with a single plugin "..." line at the top of the file.
No FFI bindings, no JNI-style glue, no marshaling layer between your code and the runtime. Your C function gets called directly when the script invokes it.
Start here: Writing an adapter plugin — the smallest complete plugin (plugins/hello, two verbs, ninety lines) walked through part by part: why capabilities live in adapters, what you must fill in, what is optional, and what the binder, the runtime and the loop machine do with each part. Then: How plugins start and stop — the init and deinit hooks every plugin has, with diagrams: why a missing library stops a script before it starts, and how plugins hand data to each other — and Optional libraries, the runtime folder a missing library is copied into.
The VS Code / VSCodium extension runs the validator on every keystroke. The four static phases — preprocess, lexer, parser, binder — finish in milliseconds and surface each problem inline with exact line and column. Errors are red; non-fatal observations (an unused function, an orphan callback) come up in yellow. Both shapes carry the source label that tells you which phase produced them. A step debugger ships with the same extension: breakpoints, stepping, Watch, threads, remote attach — installing and using the debugger →
GetCubeFace_Geometry — defined but never called. A red error elsewhere in the file flags an unresolved call to the same name (visible in the Problems panel at line 334). Each entry is tagged with its source — dominscript (binder warning) vs. dominscript (binder error) — so you know which static phase noticed.The interpreter is a tree-walker with one specialization: the binder compiles provably-numeric statements and call-free loops into a small register machine (no JIT, no machine code). Its design advantage is elsewhere: when data already exists in native memory, the script works on it in place instead of converting it into scripting-language objects first. Two platforms, measured on the same machine, shown as two separate tables.
These benchmarks are not a claim that DominScript is generally faster than Python, and the table is not a race. Python is there as a reference point: a language everyone can place, measured on the same machine with the same programs, so that a number like “44 ms” means something. The two languages are of different kinds — Python is a general-purpose language with a vast ecosystem; DominScript is a typed scripting layer for native data and C plugins — and a row where DominScript is ahead does not make it a Python replacement, any more than a row where it is behind makes it unfit for its own job. The Python side of every row is an idiomatic CPython program (the timed loop inside a function, range-based loops, hashlib/json/re/struct where a Python programmer would use them), not a line-by-line transcription of the DominScript source; both sides of every row are in the repository, and every row name below links to the two programs side by side. What the rows show is where the runtime spends its time and where the native-data model changes the cost: Fibonacci is a measurement of function calls; the arithmetic loop shows what the binder’s specialization buys; JSON and SHA-256 show the plugin model; the packet path shows what the language was built for. The rows stay on the list whichever way they point, so that a regression cannot hide.
| Six of the twenty-one rows, Linux | DominScript, ms | Python 3, ms | Elapsed time, shorter is better | DS / Py |
|---|---|---|---|---|
| Recursive Fibonacci(28)pure script · recursion — a small integer function calling itself | 9 | 24 | 0.38× | |
| Arithmetic loop, 10M iterationspure script · tight integer loop | 102 | 451 | 0.23× | |
| JSON parse + read, 30kplugin-heavy · json plugin | 22 | 38 | 0.58× | |
| Plugin call, 1M × str.Lenplugin-heavy · the price of one native call from script · a verb the plugin declares pure runs as a loop-machine op | 12 | 24 | 0.50× | |
| Packet data path, 200k framesworkload · receive buffer → slice → typed view → fields → decision · Python: bytearray + struct.unpack_from | 8 | 28 | 0.29× | |
Header parsing with a schema, 200k headersworkload · the same headers read through a packet big schema with bit fields · Python: the same program as the row above |
59 | 114 | 0.52× |
| Six of the twenty-one rows, Windows | DominScript, ms | Python 3, ms | Elapsed time, shorter is better | DS / Py |
|---|---|---|---|---|
| Recursive Fibonacci(28)pure script · recursion — a small integer function calling itself | 8 | 34 | 0.24× | |
| Arithmetic loop, 10M iterationspure script · tight integer loop | 102 | 520 | 0.20× | |
| JSON parse + read, 30kplugin-heavy · json plugin | 30 | 41 | 0.73× | |
| Plugin call, 1M × str.Lenplugin-heavy · the price of one native call from script · a verb the plugin declares pure runs as a loop-machine op | 13 | 28 | 0.46× | |
| Packet data path, 200k framesworkload · receive buffer → slice → typed view → fields → decision · Python: bytearray + struct.unpack_from | 8 | 32 | 0.25× | |
Header parsing with a schema, 200k headersworkload · the same headers read through a packet big schema with bit fields · Python: the same program as the row above |
63 | 124 | 0.51× |
Numeric loops, typed arrays and struct members run at a quarter to a half of CPython’s time, because the binder compiles them into a small register machine; plugin-bound rows follow the plugin’s C code at 0.3–1.0×; the packet data path, the shape this language exists for, at 0.25–0.30×; headers read through a packet big schema with bit fields at half of Python’s time — the readable form is the fast one. Python holds one row on each platform — SHA-256 at parity on Linux, regex on Windows (1.09×) — and neither is the interpreter’s doing. Twenty of the twenty-one rows are ahead on both platforms.
How many parses, copies, allocations and object conversions a socket → buffer → struct → decision → plugin path needs. On the shipped TCP listener path: 0 copies between the receive buffer and the script, 0 runtime object conversions, typed fields immediately (section 02). Per-call script→plugin and plugin→script overhead, blob hand-over at 1 kB / 64 kB, startup+validation time and steady event-loop latency are the next numbers to publish, measured the same way.
No cherry-picking: every benchmark script for both languages is in the repository, the four workload rows included, and the runner writes results.json. Rows are added, never dropped. The numbers above will move; the method won't.
A snapshot of what's working, what's in flight, and what's planned. The project is in active development, currently at version 0.0.3.
The test suite covers the language, the runtime and all 35 native plugins, with 900+ functional tests and roughly 18,000 injected-failure cases per pass. The test system itself is written in DominScript. It reaches from language semantics to networking and native plugin interactions: the functional tests and 50+ example smoke runs are driven by one .dom runner that is make test on Linux and on Windows alike — plus the harnesses around it: TLS round-trips against a live OpenSSL server, mutual-TLS listeners, TCP/UDP/HTTP callback servers, database smoke tests, the debugger protocol, out-of-memory injection sweeps (~18,000 injected runs per pass), and AddressSanitizer runs of the whole suite. A language that can drive processes, sockets, files and its own interpreter well enough to be its own test framework has been exercised the way users will exercise it.
The interpreter is on the 0.0.x development line — currently 0.0.3. The first public release is milestone 0.1.0, gated on a release-readiness audit; the number is deliberately conservative, because under semantic versioning 0.x means the surface may still change. A 1.0 would be a promise of stability, and that promise is made once, when it can be kept. The parser, binder, runtime, and plugin system are in daily use and covered by a regression suite of several hundred tests; the first downloadable release will be announced on this page.
It sits next to embeddable scripting runtimes — Lua/LuaJIT, AngelScript, Wren, QuickJS, or Python used as an extension host. None of them is "worse"; the trade-off is different: DominScript keeps the data native and typed instead of converting it into a runtime object model, and lets the native side call the script back. If your problem is glue over a dynamic object graph, one of those is a better fit. If your problem is a binary buffer that has to stay a binary buffer, this is the one built for it.
If you need byte-precise control over data, want to drive native libraries like SDL2 / OpenGL / FFmpeg directly from script, and want explicit, predictable data handling (no GC, no hidden object conversion), DominScript fits. If you want a one-liner to rename some files, use whatever shell you already have.
Linux and Windows today. The native Windows build runs the same regression suite as Linux, through the same DominScript runner; the only differences in the verdicts are platform-specific skips (a POSIX-only shell recipe here, a missing runtime library there), never a failure on one side. macOS and ARM / Raspberry Pi support are planned but not yet started — there will be no native macOS or ARM binaries in the first release.
By itself. Since version 0.0.2 the entire test suite is written in DominScript: the manifest runner that drives the 900+ regression tests, and every harness around it — TLS round-trips with a live OpenSSL server, mutual-TLS listeners, out-of-memory injection sweeps, the debugger protocol, database smoke tests, the benchmark self-test. The same .dom runner is make test on Linux and on Windows; the retired shell and PowerShell twins are gone.
That is not a stunt. A language that can drive processes, sockets, files, and its own interpreter well enough to be its own test framework has been exercised in exactly the ways users will exercise it — and every bug the migration surfaced (there were several, on both platforms) was found by DominScript code reading DominScript output.
Yes — a Visual Studio Code / VSCodium extension provides syntax highlighting, plugin-aware completion and hover for all 35 official plugins (755 functions) and the built-in namespaces — generated straight from the source so it stays in lockstep with the reference — plus a snippet pack of 131 templates: generic language scaffolds and plugin-specific patterns (2–5 per plugin, each naming the exact plugin load path it needs). A step debugger ships with it: breakpoints (including conditional ones), stepping, a Watch panel, and hover / Debug Console evaluation in the selected frame's real scope — see the debugger page.
It also gives live diagnostics: the four static phases — preprocessor, lexer, parser, binder — run on save (or as you type) and surface errors inline with exact line and column, without executing your script. The extension ships alongside the preview; diagnostics use the DominScript binary, so they light up once you have a build.
A plugin is a shared library (.so on Linux, .dll on Windows) that exports a fixed C ABI. Scripts declare what they need with a plugin "..." directive at the top of the file. The runtime loads each plugin once at startup, validates the ABI, and unloads everything cleanly at exit.
Built-in plugins (blob, string, struct, os) are linked into the interpreter directly. Everything else lives as an external library that any DominScript build can pick up.
Functions marked with the callback keyword are reserved entry points the runtime can dispatch into — they aren't directly callable from script. Plugins (TCP listener, UDP server, HTTP listener, worker, timer) register callbacks and the host calls them when their event fires.
Borrowed buffers come in as ref blob parameters: zero-copy, mutable, and freed by the plugin once the callback returns. The host provides timeout, error, and shutdown policies plus introspection helpers like Phase(), StatusText(), and JobInfo().
Crucially, a single dispatcher thread runs every callback, one event at a time — the same model as a JavaScript event loop. Plugin threads enqueue an event and wait; they never run script code themselves. The practical consequence: two callbacks never execute concurrently, so script-level state shared between them (a ref blob, a counter, a buffer) is race-free by construction. You don't write a mutex to protect ordinary script variables.
The core interpreter stays closed source; everything you build against is open. The release ships the domin binary for Linux and Windows, and with it the full source of the official plugins, the plugin ABI headers, the examples and the documentation. The source of the core itself is not published.
The binary is free — for individuals and companies alike, commercial use included. Free means free to download and to use in your own projects and products; the exact licence text ships with the release.
You never need the core source to extend it. A plugin is a C shared library that talks to the runtime through a small, versioned ABI (the plugin API headers, shipped with the release). With the plugin skeletons and the 35 official plugins as worked examples, a new plugin is a copy-and-edit job — small enough that an AI coding assistant can draft one from the examples. Nothing forces you to use the built-in plugins either: a plugin of your own with the same job can take their place, and your own namespaces sit next to the official ones. A plugin you write is yours: you choose its licence, open or closed — nothing in DominScript obliges you to publish its source.
The first release (milestone 0.1.0) is in preparation, and it will be free to download and use: individuals and companies alike, commercial use included. The core stays closed source; the plugins, the plugin ABI, the examples and the docs ship as source. The exact licence text ships with the release.