z3-solver
works with JavaScript apps through bindings to WebAssembly.
Some important notes from the npm package:
The package requires threads, which means you'll need to be running in an environment which supports SharedArrayBuffer
... you'll need to serve your page with special headers.
When building for the web, you should include that file [z3-built.js
] as its own script on the page.
With those notes in mind, let's get started integrating z3-solver
into your Next.js app!
From your terminal, run npm install z3-solver
.
Once installed, you'll need to copy some pre-built files that z3-solver
provides into your application. If you're running z3-solver
in a Node.js environment, i.e. using Server Actions in Next.js, you can skip this step. If you're using z3-solver
client-side, i.e. in the browser, this is required.
Files:
node_modules/z3-solver/build/z3-built.js
node_modules/z3-solver/build/z3-built.wasm
node_modules/z3-solver/build/z3-built.worker.js
You'll copy these files into your public
directory in your Next.js app.
Add a <script>
component to the page/component that requires the z3-solver
package. You have two options here:
Option 1: Add a <script>
component that loads the public file synchronously.
1<script src="/z3-built.js" />
This will defer loading of your page/component until the script has been downloaded. This may hurt the first-time-to-load performance of your page, but is the easiest way to guarentee that the scripts have been loaded.
Be warned, Next.js will throw a @next/next/no-sync-scripts
error when you try to build the app. You can disable this error, or you can try the option below.
Option 2: Add a <Script>
component (from Next.js) that loads the public file asynchronously and updates state after loading.
1234<Script
src="/z3-built.js"
onLoad={() => setStatus("loaded")}
/>
Your page/component will be free to load in parallel, allowing a faster initial load of your page. You can then do fine-grain state management to prevent usage of the z3-solver
package until the script has finished loading.
For z3-solver
's JavaScript<->WASM bindings to work, the library requires that SharedArrayBuffer
be availble in the browser. Every major browser supports SharedArrayBuffer
, but only in a secure context.
To enable a secure context, you'll set two headers:
"Cross-Origin-Opener-Policy"
to"same-origin"
"Cross-Origin-Embedder-Policy"
to"require-corp"
Add the following to your Next.js config file (next.config.{js|ts|mjs}
):
123456789101112131415161718async headers() {
return [
{
source: "/:path*", // Match all routes
headers: [
// These headers are required for z3-solver
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
];
},
12345678910111213// 1) Import
import { init } from "z3-solver";
...
// 2) Initialize
// You'll need to wait for your script to be loaded first
const { Context } = await init();
// 3) Create a Context
// Types fall over beyond here
// @ts-ignore
const { Solver, Int } = new Context("main");
After this, you should be set up to use z3-solver
.
Because the documentation is a bit lacking and the TypeScript types don't work very well, it can be a bit challenging to know how you can use z3-solver
in JavaScript.
To see what methods are available for the data types (Int
, Real
, BitVec
, etc.), you can create one of those objects and console.log
it. You'll see the Prototype
methods for that object, which should give you an idea how you can use these objects.
The console.log output of a z3-solver Int object
If you'd like to see a real example of z3-solver
being used, check out my Advent of Code 2024 Day 13 Part 2 solution.