Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c44f8f3
Extend the pure-C shim with the nine missing entry points
zzcgumn Jul 19, 2026
1bdd384
Regenerate export lists for the widened C shim
zzcgumn Jul 19, 2026
2bc191e
Add C++ coverage for the pure-C shim
zzcgumn Jul 19, 2026
922f823
Fix null deref in TransTableS::reset_memory after memory release
zzcgumn Jul 19, 2026
e9fdf16
Retarget the .NET binding onto the pure-C shim
zzcgumn Jul 19, 2026
a4c40c2
Add explicit native-library resolution via DDS_LIBRARY_PATH
zzcgumn Jul 19, 2026
25537c8
Declare the .NET projects AnyCPU rather than x64
zzcgumn Jul 19, 2026
f277280
Add the managed test project
zzcgumn Jul 19, 2026
71ff1fb
Add .NET binding coverage to Linux, Windows, and macOS CI
zzcgumn Jul 19, 2026
2e63190
Update docs and specs for the .NET binding
zzcgumn Jul 19, 2026
22d4cc4
Fix heap-use-after-free in clear_tt
zzcgumn Jul 20, 2026
e58416e
Route clear_tt through dispose_trans_table
zzcgumn Jul 20, 2026
3991945
Amend solver-context spec for the new clear_tt semantics
zzcgumn Jul 20, 2026
d562d06
Cover the TransTableS reset guard with a test that reaches it
zzcgumn Jul 20, 2026
dc6d4be
Correct the dds_c_api_test dependency comment
zzcgumn Jul 20, 2026
3b1a209
Surface native failures in Release, not only in DEBUG
zzcgumn Jul 20, 2026
f338bf9
Detect failed context creation via IsInvalid, not a null check
zzcgumn Jul 20, 2026
1536c5a
Fix out-of-bounds aggr_target in the TransTableS reset test
zzcgumn Jul 20, 2026
2373816
Run the .NET tests in Release as well as Debug in CI
zzcgumn Jul 21, 2026
a18255a
Sync clear_tt developer note with its disposal semantics
zzcgumn Jul 21, 2026
a40fd74
fix: updates the bazel lock file.
zzcgumn Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/ci_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,28 @@ jobs:
- name: Run all tests
run: bazel test --verbose_failures //...

# 7️⃣ Upload test logs
# 7️⃣ .NET binding — build and test the managed wrapper against the shared
# library just built. Pinned to 8.0.x because that is what DDS_Core
# targets; the test project's RollForward only matters where no 8.0
# runtime exists, which is not the case here.
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

# bazel-bin is a configuration-dependent convenience symlink, so ask Bazel
# for it rather than hardcoding a path that a different --config would move.
- name: Test .NET binding
run: |
bazel build --verbose_failures //jni:dds_shared
export DDS_LIBRARY_PATH="$(bazel info bazel-bin)/jni/libdds.so"
test -f "$DDS_LIBRARY_PATH" || { echo "native library not found at $DDS_LIBRARY_PATH"; exit 1; }
# Both configurations: ThrowIfError is unconditional, so a regression to
# [Conditional("DEBUG")] only surfaces in Release. See dotnet-binding.md.
dotnet test dotnet/DDS_Core.Tests/ --verbosity normal
dotnet test dotnet/DDS_Core.Tests/ -c Release --verbosity normal

# 8️⃣ Upload test logs
- name: Upload test logs - Linux
if: always()
uses: actions/upload-artifact@v6
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/ci_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ jobs:
- name: Run all tests
run: bazelisk test --verbose_failures //...

# .NET binding — this job is the only CI coverage of the AArch64 ABI, which
# is what the managed struct-layout tests exist to guard: a layout mismatch
# there corrupts results silently rather than throwing. Linux covers SysV
# x86-64 and Windows covers Win64, so all three ABIs are exercised.
# Pinned to 8.0.x to match what DDS_Core targets.
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

# bazel-bin is a configuration-dependent convenience symlink, so ask Bazel
# for it rather than hardcoding a path that a different --config would move.
- name: Test .NET binding
run: |
bazelisk build --verbose_failures //jni:dds_shared
export DDS_LIBRARY_PATH="$(bazelisk info bazel-bin)/jni/libdds.dylib"
test -f "$DDS_LIBRARY_PATH" || { echo "native library not found at $DDS_LIBRARY_PATH"; exit 1; }
# Both configurations: ThrowIfError is unconditional, so a regression to
# [Conditional("DEBUG")] only surfaces in Release. See dotnet-binding.md.
dotnet test dotnet/DDS_Core.Tests/ --verbosity normal
dotnet test dotnet/DDS_Core.Tests/ -c Release --verbosity normal

# Upload test logs
- name: Upload test logs - macOS
if: always()
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/ci_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ jobs:
- name: Run all tests
run: bazel test --verbose_failures //...

# .NET binding — this is the platform the shim retarget can regress.
# Windows was previously the only place the binding worked, via
# dds_native.dll and dds_api.hpp's dds_* symbols; it now binds dds_c_*
# in the Bazel-built dds.dll, a path no other test covers (the JNI smoke
# tests and export_set_test are target_compatible_with-excluded here).
# Pinned to 8.0.x to match what DDS_Core targets.
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"

# bazel-bin is a configuration-dependent convenience symlink, so ask Bazel
# for it rather than hardcoding a path that a different --config would move.
- name: Test .NET binding
shell: pwsh
run: |
bazel build --verbose_failures //jni:dds_shared
if ($LASTEXITCODE -ne 0) { exit 1 }
$binDir = bazel info bazel-bin
$env:DDS_LIBRARY_PATH = Join-Path $binDir "jni\dds.dll"
if (-not (Test-Path $env:DDS_LIBRARY_PATH)) {
Write-Host "native library not found at $env:DDS_LIBRARY_PATH"
exit 1
}
# Both configurations: ThrowIfError is unconditional, so a regression to
# [Conditional("DEBUG")] only surfaces in Release. See dotnet-binding.md.
dotnet test dotnet/DDS_Core.Tests/ --verbosity normal
if ($LASTEXITCODE -ne 0) { exit 1 }
dotnet test dotnet/DDS_Core.Tests/ -c Release --verbosity normal
if ($LASTEXITCODE -ne 0) { exit 1 }

# Upload test logs
- name: Upload test logs - Windows
if: always()
Expand Down
2 changes: 1 addition & 1 deletion MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions docs/dotnet_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ The library exposes the full DDS functionality through idiomatic `.Net` structur

The goal is to provide a stable, fast, and fully documented .NET interface to the DDS engine.

It runs on **macOS, Linux, and Windows** from the same assembly — see
[Building and loading the native library](#building-and-loading-the-native-library).
The bindings mirror how the other language bindings work; see
[jni_interface.md](jni_interface.md), [python_interface.md](python_interface.md),
and [wasm_build.md](wasm_build.md).

---

## Table of Contents
0. [Building and loading the native library](#building-and-loading-the-native-library)
1. [Introduction](#introduction)
2. [Legacy vs. Modern DDS API](#legacy-vs-modern-dds-api)
3. [Basic Usage](#basic-usage)
Expand Down Expand Up @@ -39,6 +46,65 @@ The goal is to provide a stable, fast, and fully documented .NET interface to th

---

# Building and loading the native library

`DDS_Core` is a managed wrapper; the solver itself lives in one self-contained
native library built by Bazel:

```bash
bazel build //jni:dds_shared
```

| OS | Artifact | Location |
| ------- | -------------- | ---------------- |
| Linux | `libdds.so` | `bazel-bin/jni/` |
| macOS | `libdds.dylib` | `bazel-bin/jni/` |
| Windows | `dds.dll` | `bazel-bin/jni/` |

Despite living under `//jni`, this is the shared native artifact for every
binding — the JVM one uses it too.

## Finding the library at runtime

The binding imports the library under the single name **`dds`**; .NET's probing
supplies the `lib` prefix and the per-OS extension, so one name resolves all
three artifacts above. There are two ways to point it at a library:

- **Default probing** — place the library where the runtime already looks (next
to the application, or under `runtimes/<rid>/native/` in a package). No
configuration needed.
- **`DDS_LIBRARY_PATH`** — set this environment variable to the *full path* of a
library file to override probing. This is the counterpart of the JVM binding's
`-Ddds.library.path`, and is how the tests bind against a freshly-built
library:

```bash
export DDS_LIBRARY_PATH="$(bazel info bazel-bin)/jni/libdds.dylib"
dotnet test dotnet/DDS_Core.Tests/
```

Use `bazel info bazel-bin` rather than the `bazel-bin` symlink: that symlink is
configuration-dependent and moves when you build with a different `--config`.
If the variable is set but the library cannot be loaded, the error names the
path you gave rather than reporting a missing entry point later.

## Which native symbols are used

The modern context API binds the pure-C shim (`dds_c_*`, from
`library/src/api/dds_c_api.h`); the legacy flat API binds `dll.h` directly. The
reference-taking `dds_*` functions in `dds_api.hpp` are deliberately **not**
used — they are not exported on Linux or macOS, and binding them is what
previously made this wrapper Windows-only.

## Requirements

.NET 8.0 or later (`.NET Framework` is not supported). If you build the test
project on a machine that has only a newer major runtime installed, note it sets
`<RollForward>Major</RollForward>` so it still runs; the library itself targets
`net8.0`.

---

# Introduction

`DDS_Core` provides a managed .NET interface to the DDS engine.
Expand Down
16 changes: 10 additions & 6 deletions docs/jni_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@ and ctypes can all bind to:

- The solver handle is opaque: `typedef void* DDS_C_SOLVER_CTX`.
- Every struct is passed by pointer; no non-POD C++ type crosses the boundary.

Shim entry points: `dds_c_create_solvercontext_default`,
`dds_c_destroy_solvercontext`, `dds_c_solve_board`, `dds_c_calc_dd_table`,
`dds_c_calc_par`. The flat legacy C API from `dll.h` (`SolveBoard`,
`CalcDDtable`, `GetDDSInfo`, `ErrorMessage`, …) is exported unchanged and is
also callable from FFM.
`SolverConfig` is decomposed into scalar arguments and `TTKind` crosses as an
`int`, so nothing is passed by value either.

The shim covers the modern API's full surface: context lifecycle (default and
config-based creation, destroy), `dds_c_solve_board`, `dds_c_calc_dd_table` and
its `_pbn` twin, `dds_c_calc_par`, transposition-table configure/resize/clear,
both resets, and the logging passthroughs. The Java bindings here use a subset;
the .NET binding uses all of it ([dotnet_interface.md](dotnet_interface.md)).
The flat legacy C API from `dll.h` (`SolveBoard`, `CalcDDtable`, `GetDDSInfo`,
`ErrorMessage`, …) is exported unchanged and is also callable from FFM.

## Using the FFM bindings

Expand Down
160 changes: 160 additions & 0 deletions dotnet/DDS_Core.Tests/ContextLifecycleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using DDS_Core;

namespace DDS_Core.Tests;

/// <summary>
/// Covers the context-management entry points added to the C shim by this work:
/// config-based construction, TT configuration, the resets, logging, and
/// SafeHandle-driven disposal. Between these and <see cref="SmokeTests"/>, every
/// one of the fourteen retargeted P/Invokes is exercised — so a missing
/// <c>EntryPoint</c> fails here rather than in a consumer.
/// </summary>
public class ContextLifecycleTests
{
[Theory]
[InlineData(TTKind.Small)]
[InlineData(TTKind.Large)]
public void ConstructedFromConfig_SolvesReferenceDeal(TTKind kind)
{
// Exercises dds_c_create_solvercontext, whose SolverConfig is unpacked
// into scalars at the ABI boundary.
using var ctx = new SolverContext(new SolverConfig(kind, 0, 0));

ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);

Assert.Equal(TestDeals.ExpectedTricks, fut.Score[0]);
}

/// <summary>
/// A successfully created context must hold a valid native handle. The
/// creation guard cannot test the reference for null — the SafeHandle
/// marshaller never returns one — so it tests IsInvalid, and this pins that
/// the successful path actually produces a live pointer rather than
/// IntPtr.Zero silently sailing through.
/// </summary>
[Theory]
[InlineData(TTKind.Small)]
[InlineData(TTKind.Large)]
public void Construction_YieldsValidHandle(TTKind kind)
{
using var fromConfig = new SolverContext(new SolverConfig(kind, 0, 0));
Assert.False(fromConfig.Handle.IsInvalid);

using var fromDefault = new SolverContext();
Assert.False(fromDefault.Handle.IsInvalid);
}

/// <summary>
/// Failures must throw at the public surface in every build configuration,
/// not only in DEBUG. `solutions = 4` is out of range and the solver returns
/// RETURN_SOLNS_WRONG_HI (-9); if ThrowIfError is ever made conditional
/// again, this test fails in Release.
/// </summary>
[Fact]
public void SolveBoard_WithInvalidSolutions_Throws()
{
using var ctx = new SolverContext();

Assert.Throws<InvalidOperationException>(
() => ctx.SolveBoard(TestDeals.Reference(), -1, 4, 1, out FutureTricks _));
}

[Fact]
public void TtReconfiguration_LeavesContextUsable()
{
using var ctx = new SolverContext();
ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks _);

ctx.ConfigureTT(TTKind.Small, 1, 2);
ctx.ResizeTT(1, 2);
ctx.ClearTT();

ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);
Assert.Equal(TestDeals.ExpectedTricks, fut.Score[0]);
}

[Fact]
public void Resets_LeaveContextUsable()
{
using var ctx = new SolverContext();
ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks _);

ctx.ResetForSolve();
ctx.ResetBestMovesLite();

ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);
Assert.Equal(TestDeals.ExpectedTricks, fut.Score[0]);
}

/// <summary>
/// A Small-TT context survives ClearTT() followed by ResetForSolve() and
/// still solves. ClearTT() disposes the transposition table, so the
/// following solve rebuilds one lazily from the context's configuration.
/// This exercises the managed TT-lifecycle path; the native
/// TransTableS::reset_memory() guard is covered by
/// //library/tests/trans_table:trans_table.
/// </summary>
[Fact]
public void SmallTt_ClearThenResetForSolve_StillSolves()
{
using var ctx = new SolverContext();
ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks _);

ctx.ConfigureTT(TTKind.Small, 1, 2);
ctx.ClearTT();
ctx.ResetForSolve();

ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);
Assert.Equal(TestDeals.ExpectedTricks, fut.Score[0]);
}

[Fact]
public void Logging_DoesNotDisturbSolving()
{
using var ctx = new SolverContext();

ctx.LogAppend("DDS_Core.Tests");
ctx.LogAppend(string.Empty);
ctx.LogClear();

ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);
Assert.Equal(TestDeals.ExpectedTricks, fut.Score[0]);
}

/// <summary>
/// Disposal must release the native context through SafeHandle without
/// faulting, and must be safe to repeat.
/// </summary>
[Fact]
public void Dispose_ReleasesHandleAndIsIdempotent()
{
var ctx = new SolverContext();
ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks _);

ctx.Dispose();
ctx.Dispose();

Assert.True(ctx.Handle.IsClosed || ctx.Handle.IsInvalid);
}

/// <summary>
/// Contexts are single-threaded, so concurrent use means one context per
/// thread. This is the arrangement the docs prescribe; if it regressed,
/// multi-threaded consumers would corrupt results rather than fail loudly.
/// </summary>
[Fact]
public void OneContextPerThread_SolvesConcurrently()
{
const int threads = 4;
var results = new int[threads];

Parallel.For(0, threads, i =>
{
using var ctx = new SolverContext();
ctx.SolveBoard(TestDeals.Reference(), -1, 1, 1, out FutureTricks fut);
results[i] = fut.Score[0];
});

Assert.All(results, r => Assert.Equal(TestDeals.ExpectedTricks, r));
}
}
Loading
Loading