Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 17 additions & 9 deletions .github/instructions/cs-debug-binding.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,34 @@ description: Use when changing the C ABI (foundry_local_c.h) and validating C# t
applyTo: sdk_v2/cs/**
---

# C# Tests Auto-Load the C++ **Debug** Build
# C# Tests Auto-Load the C++ **RelWithDebInfo** Build

The C# project (`sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj`) auto-detects
`sdk_v2/cpp/build/Windows/Debug/bin/Debug/foundry_local.dll` for inner-loop dev
and writes a `foundry_local.native.cfg` redirect file. The test project copies
this redirect to its output, so `dotnet test` always loads the **Debug** native
binary, never RelWithDebInfo.
`sdk_v2/cpp/build/Windows/RelWithDebInfo/bin/RelWithDebInfo/foundry_local.dll`
for inner-loop dev and writes a `foundry_local.native.cfg` redirect file. The
test project copies this redirect to its output, so `dotnet test` always loads
the **RelWithDebInfo** native binary. The auto-detect block only checks the
RelWithDebInfo config (one entry per OS) — it never probes a Debug build.

To override (e.g. point at a Debug build or a NuGet runtime package), set
`FoundryLocalNativeBinDir` or `FoundryLocalRuntimeVersion` explicitly; otherwise
the RelWithDebInfo auto-detect wins.

## Implication

When you change the C ABI surface (e.g. `foundry_local_c.h`, struct layouts,
function signatures in `c_api.cc`), you **must rebuild Debug** before running
C# tests, even if you've already built RelWithDebInfo for the C++ test suite:
function signatures in `c_api.cc`), you **must rebuild RelWithDebInfo** before
running C# tests:

```powershell
cd sdk_v2/cpp
python build.py --build --config Debug
python build.py --build --config RelWithDebInfo
```

## Symptoms of a stale Debug DLL
This is the same config the C++ integration test suite uses, so a single
RelWithDebInfo build serves both.

## Symptoms of a stale RelWithDebInfo DLL

- C# tests fail with native errors that don't match current C++ source line numbers
- Dispatch falls through unexpectedly (e.g. JSON pass-through stops working
Expand Down
13 changes: 13 additions & 0 deletions memories/repo/cs-item-ownership-transfer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# C# Item Ownership Transfer & CA2000

- `Item : IDisposable` owns a native handle. Hand it to a container via the **generic** `ItemQueue.Push(Item)` or `Request.AddItem(Item)` — that is the only intended API.
- **Do NOT add per-type push/add helpers** (`PushBytes`, `PushText`, etc.) — they don't scale. Use `queue.Push(BytesItem.CreateOwned(chunk))` inline instead.
- Transfer order is leak-safe: native call first, then `item.ReleaseOwnership()` only on success (a throwing push under `using` won't leak).
- **Owned** items (`CreateOwned`, `AudioItem.CreateFormatDescriptor`): `_pinContext` is **never assigned** (not "set to null on transfer" — the owned factories use a private ctor that only allocates the native handle, and keep the pin in a local). The pin lives behind a `GCHandle` and is freed by the native deleter (`BytesDeleter` → `PinContext.ReleaseFromNative`) when the native item is destroyed via `flItem_Release`.
- Owned factories pin + root in one shot via **`PinContext.PinForNativeDeleter(data, out ptr, out len)`** (returns the `deleter_user_data` IntPtr). Do NOT pin-then-alloc as two separate locals in the factory — `PinForNativeDeleter` is the single ownership-transfer chokepoint and also guards the pin→GCHandle gap. New owned Item types should route through it.
- `Item.Dispose()` does exactly two things: `OnDisposing()` then release-native-`if (_ownsHandle)`. For an owned item *after transfer* both short-circuit — `OnDisposing` runs `_pinContext?.Dispose()` (no-op, field is null) and `_ownsHandle == false` skips the native release. So Dispose is fully inert; whoever now owns the handle triggers the deleter. Disposing an owned item *before* pushing still cleans up correctly: `_ownsHandle == true` → `flItem_Release` → same deleter → unpin.
- **Borrowed** items (`new BytesItem(ReadOnlyMemory)`, borrowed `AudioItem`) store the pin in `_pinContext` (no native deleter) and unpin in `OnDisposing` → unsafe to dispose after a queue push (native still references the buffer). For streaming/queue use the **owned** factories.
- See `sdk_v2/cs/docs/item-data-ownership.md` (States 1-4) for the full pin/ownership model.
- **CA2000 is intentionally disabled project-wide** via `<NoWarn>$(NoWarn);CA2000</NoWarn>` in both csprojs (+ `.editorconfig` severity `none`). It can't model ownership transfer and fires false positives on every hand-off. **IDisposableAnalyzers** (SDK project, `PrivateAssets=all` so it doesn't flow to tests) is the disposal-correctness authority. Do NOT re-add CA2000 pragmas or per-site suppressions.
- **IDISP001** (IDisposableAnalyzers) follows data flow into *same-assembly* callees, so passing a created disposable to a helper that roots it in a `GCHandle` (instead of disposing/returning it) does NOT suppress the warning — the analyzer fundamentally can't model ownership handed to unmanaged code freed by a native callback. The single justified `#pragma warning disable IDISP001` lives at the one transfer boundary in `PinContext.PinForNativeDeleter`; do not scatter suppressions across the owned factories.

15 changes: 14 additions & 1 deletion sdk_v2/cpp/src/spdlog_logger.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,20 @@ SpdlogLogger::SpdlogLogger(LogLevel min_level, const std::string& logs_dir) {
spdlog::async_overflow_policy::block);

logger_->set_level(ToSpdlogLevel(min_level));
logger_->flush_on(spdlog::level::warn);

// Flush policy: by default we buffer low-severity logs and only force a flush at warning and
// above (plus on destruction), avoiding a per-message flush on the hot path. We flush on every
// message only when running in a debug build, or when the caller has explicitly opted into
// Debug/Verbose diagnostics -- in those cases prompt, crash-safe output is worth the cost.
#ifndef NDEBUG
constexpr bool debug_build = true;
#else
constexpr bool debug_build = false;
#endif

const bool flush_every_message = debug_build || min_level <= LogLevel::Debug;

logger_->flush_on(flush_every_message ? spdlog::level::trace : spdlog::level::warn);

spdlog::register_logger(logger_);
}
Expand Down
8 changes: 6 additions & 2 deletions sdk_v2/cs/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,12 @@ dotnet_diagnostic.IDE0090.severity = error
## Suppressing this particular case due to issues in the analyzer's understanding of pattern matching.
dotnet_diagnostic.IDE0072.severity = suggestion

# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = warning
# CA2000: Dispose objects before losing scope.
# Downgraded to 'none' — see the NoWarn + rationale in the .csproj files. The Item ownership model
# transfers a native handle into a container (Request/ItemQueue) and the C# wrapper's Dispose() is a
# no-op afterwards (docs/item-data-ownership.md), which CA2000 can't model. IDisposableAnalyzers is
# our authority for disposal correctness instead.
dotnet_diagnostic.CA2000.severity = none

# IDE0046: Convert to conditional expression
dotnet_diagnostic.IDE0046.severity = silent
Expand Down
1 change: 0 additions & 1 deletion sdk_v2/cs/src/Catalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

namespace Microsoft.AI.Foundry.Local;

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

Expand Down
3 changes: 2 additions & 1 deletion sdk_v2/cs/src/Detail/DllLoader.Modern.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// --------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
Expand All @@ -12,6 +12,7 @@
namespace Microsoft.AI.Foundry.Local.Detail;

using System.Runtime.InteropServices;

using Microsoft.AI.Foundry.Local.Detail.Interop;

internal static partial class DllLoader
Expand Down
3 changes: 2 additions & 1 deletion sdk_v2/cs/src/Detail/DllLoader.NetStandard.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// --------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
Expand All @@ -16,6 +16,7 @@
namespace Microsoft.AI.Foundry.Local.Detail;

using System.Runtime.InteropServices;

using Microsoft.AI.Foundry.Local.Detail.Interop;

internal static partial class DllLoader
Expand Down
3 changes: 2 additions & 1 deletion sdk_v2/cs/src/Detail/DllLoader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// --------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
Expand All @@ -7,6 +7,7 @@
namespace Microsoft.AI.Foundry.Local.Detail;

using System.Runtime.InteropServices;

using Microsoft.AI.Foundry.Local.Detail.Interop;

/// <summary>
Expand Down
Loading