Odin bindings for DDS, Bo Haglund and Soren Hein's double-dummy solver for the game of bridge.
A double-dummy solver computes the exact number of tricks each side can take on a given deal with all four hands visible and optimal play from everyone. DDS is the de-facto standard engine behind bridge hand analysis, par-score and par-contract calculation, and play analysis; it is fast, multi-threaded, and battle-tested.
The DDS C/C++ source is included as a git submodule under external/dds and is not modified — these
bindings wrap its public C ABI (include/dll.h).
See docs/api.md for the idiomatic-Odin API guide — types, core functions, threading, and
memory lifecycle. The bindings are a near 1-to-1 port, so the original DDS interface
documentation also maps directly.
- Functions keep their C names:
SolveBoard,CalcDDtable,Par, ... are called asdds.SolveBoard,dds.CalcDDtable,dds.Par. - Struct/type names drop the
ddprefix and areAda_Caseper Odin convention: Cstruct deal→dds.Deal,struct ddTableResults→dds.Table_Results,struct futureTricks→dds.Future_Tricks. (struct DDSInfois renameddds.DDS_Info.) - The
#defineerror/return codes are exposed as constants:dds.RETURN_NO_FAULT,dds.RETURN_UNKNOWN_FAULT, ...
DDS needs a one-time setup call before any other function — it sizes the per-thread transposition-table memory and
builds its constant lookup tables. When DDS is used as a DLL this happens automatically from DllMain, but these
bindings link a static library, which has no DllMain, so you must do it yourself:
package main
import dds ".."
main :: proc() {
dds.SetMaxThreads(0) // 0 = let DDS pick the thread count from the core count. Call once, before anything else.
defer dds.FreeMemory()
deal: dds.Deal
// ... fill in the deal ...
fut: dds.Future_Tricks
// target = TARGET_FIND_MAX (find the best), solutions = .All, mode = .Auto_Skip_Single.
dds.SolveBoard(deal, dds.TARGET_FIND_MAX, .All, .Auto_Skip_Single, &fut)
}Skipping SetMaxThreads (or SetResources) means the first DDS call dereferences unsized state and crashes. See
examples/smoke.odin for a runnable smoke test (just run).
| odin-dds tag | DDS version | notes |
|---|---|---|
| 2026-07 | 2.9.1 code (7219c95) |
Initial bindings |
DDS never tagged 2.9.1, so the
external/ddssubmodule is pinned to commit7219c95— the code-complete 2.9.1 point on the flat-layoutinclude/+src/tree (DDS_VERSIONupstream is still20900; the version was never bumped). This gets the 2.9.1 fixes — rimmington's transposition-table memory-freeing fixes,#include <stdbool.h>in C mode, and theSolveAllBoardsBinentry point — without the later v3 refactor that moveddll.htolibrary/src/api/. Validated with DDS's owndtest(0 differences across solve/calc/play/par).
The pre-built static libraries are shipped with the bindings in ./lib:
| Platform | File | Built by |
|---|---|---|
| Windows | lib/dds.lib |
src/build.cmd (MSVC cl + lib) |
| Linux / BSD | lib/dds.a |
src/Makefile (g++ + ar) |
| macOS | lib/darwin/dds.a |
src/Makefile (clang++ + ar) |
All are self-contained static archives — the whole solver folds into your one Odin executable with no shared libraries to ship. Each platform uses its native threading: WinAPI on Windows, GCD on macOS, STL on Linux (no OpenMP dependency). The Odin bindings pull in libstdc++ and libpthread on Linux/BSD automatically via foreign import.
Rebuild with:
just build-lib
On Windows this runs src/build.cmd lib external/dds (requires MSVC; auto-detected via vswhere) and stages lib/dds.lib. On Unix it runs make -C src and stages lib/dds.a or lib/darwin/dds.a. The submodule is checked out if needed.
src/build.cmd also has a dll mode (src/build.cmd dll external/dds) if you prefer a DLL on Windows — the script's header comment weighs the static-vs-DLL trade-offs in depth.
Static linking has no
DllMain/constructor auto-init, so consumers must callSetMaxThreads(0)once before any other DDS call (see above). The DLL build auto-initializes but then requires shippingdds.dll(+VCOMP140.DLL) alongside the executable.
dds.odin is generated from external/dds/include/dll.h by odin-c-bindgen
using bindgen.sjson. Regenerate with:
just bindgen
A bindgen.sjson detail worth knowing: bindgen names its output after the header stem (dll.h → dll.odin),
so just bindgen renames it to dds.odin.
Historical note: DDS 2.9.0's
dll.husedboolwithout including<stdbool.h>, soboolwas undefined when libclang parsed the header as C — the bindings needed aclang_defines = { "bool" = "_Bool" }override. The 2.9.1 pin (7219c95) adds that include upstream, soboolresolves on its own and the override is gone.
Hand-written additions (the foreign import block, platform selection) live in src/prelude.odin
and are pasted near the top of the generated file.
Tasks are run with just (just TASK); the Windows shell is PowerShell.
just run [name]— build and run an example (examples/<name>.odin, defaultsmoke); e.g.just run solve_boardjust lint— type check + vet + strict stylejust format—odinfmt -w .just test— run all tests (they live as@(test)procs insideexamples/*.odin);just test1 <name>runs one example's testsjust build-lib— (re)build and stage the DDS static libjust bindgen— regenerate the bindingsjust submodules— check out / update theexternal/ddssubmodule
These bindings are provided under the terms in LICENSE. DDS itself is licensed under the Apache 2.0
license — see external/dds/LICENSE.