Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
155010 stories
·
33 followers

WPF Hot Reload Is Here: Edit Your XAML and Watch It Update Live in Rider

1 Share

WPF Hot Reload is now available in Rider, starting with the 2026.2 EAP 2 build. You can edit your XAML while your app is running under the debugger and see the changes immediately, with no rebuild, no restart, and no losing your place in the application. Together with the C# Hot Reload support that’s already in Rider, this completes the Edit and Continue workflow for WPF.

This one has been a long time coming, and we want to be straight about that. The request for WPF Hot Reload is one of the most upvoted issues in Rider’s entire history. We read every word of your comments, and the feature we’re shipping today is shaped directly by that feedback. 

A quick, honest note before we go further: this is a beta. The surface area of WPF is large, and some scenarios are not covered yet. We’ll lay those out plainly below so you know exactly what to expect. 

What WPF Hot Reload does

When you’re running a WPF app under the Rider debugger, you can now modify your XAML and have the saved changes reflected in the live, running application. Adjust a margin, restyle a button, tweak a DataTemplate, change a color, rework a layout – then save, and the UI updates in place. The application keeps its current state. You don’t need to navigate back through five screens to get to the view you were working on. You don’t have to rebuild. You just keep iterating.

Why this matters for the way you work

WPF UI development has a natural rhythm: Change something, see how it looks, adjust. Without Hot Reload, that rhythm keeps getting interrupted by a rebuild and a click-path back to the screen you were on. A few situations make that friction especially frustrating, and they are unfortunately not uncommon.

Large, long-lived WPF applications. For many teams, WPF isn’t legacy. It’s the present and the roadmap, with a decade or more of active development ahead across applications maintained by many developers at once. In a codebase like that, UI iteration speed isn’t a nicety; it compounds across every developer, every day. Hot Reload takes the most repetitive loop in WPF UI work – the change, rebuild, navigate back, and check cycle – and collapses it.

Applications with complex UI structures. This is where it gets interesting. Hot Reload has held up across genuinely non-trivial setups, including XAML resource dictionaries that hold control templates for custom controls in a shared WPF library. That’s exactly the kind of structure that tends to be fragile under hot-reload implementations elsewhere. If your UI is built from layered styles, templated custom controls, and shared resource dictionaries, there’s a good chance Hot Reload was a crucial missing piece of your workflow.

Dual-IDE setups. It’s common for teams to run Rider for most of their work while keeping Visual Studio open specifically for the live XAML loop. Maintaining two IDEs for one task is friction nobody wants. Hot Reload removes the need to keep switching, and for a lot of developers, it’s the only remaining reason to keep a second IDE installed.

Supported target frameworks

The current EAP build supports the latest versions of .NET and .NET Framework. net9.0-windows and net10.0-windows work as expected, and .NET Framework targets are supported as well.

How to try it

  1. Download Rider 2026.2 EAP 2 or later.
  2. Open your WPF project and start a Debug session from Rider.
  3. Edit and save your XAML changes, whether styles, templates, layout, or resources.
  4. Watch the running app update in place.
In this example we’re using changes to a weather app UI to illustrate the seamless Hot Reload experience for a WPF project in Rider 2026.2

That’s the whole loop. No extra configuration, no separate mode to enable.

Known limitations

Here’s the part worth reading carefully: These are the cases where Hot Reload won’t apply a change in place today, along with possible workarounds. For some of these limitations, we don’t have any immediate plans for development, while others will be resolved in upcoming releases. Here is where the beta stands now.

  • Adding, removing, or updating NuGet packages. Restore packages, then restart the debugging session.
  • Adding new controls, windows, pages, or other files to your project while the app is running. Restart the debugging session to pick them up.
  • Changing the root type or x:Class of an already-loaded XAML file (for example, turning a Window into a Page). 
  • Making changes to runtime-created resources or runtime-switched theme dictionaries. Restart the debugging session to apply them.
  • Adding new WPF class members that rely on static registration, such as a DependencyProperty, an attached property, or a RoutedEvent. Note: This only applies when registration happens in a static field initializer; assigning the field later in a method may also work.
  • Adding new x:Name values in XAML. This one is partial: The XAML update itself is applied live, but the new names only become available from your C# code-behind after you restart the debugging session.
  • Changing animations started by one-time triggers, such as an EventTrigger on Loaded. The updated animation won’t restart until the view is loaded again. 

Help us prioritize 

Most of the limitations above are tracked in YouTrack with plans to address them in upcoming releases. Each has its own ticket where you can upvote and add details from your setup. The more signal we have on which ones are blocking your work, the easier it is to prioritize them:

  • [RIDER-138349] Hot Reload after Attach to Process
  • [RIDER-138659] Changes to runtime-created resources or runtime-switched theme dictionaries
  • [RIDER-138874] Adding new WPF class members that rely on static registration
  • [RIDER-138348] Changing animations started by one-time triggers

A few scenarios sit further out, including Hot Reload after attaching to an already-running process. These require a bit more research before we can commit to a particular approach in implementation. If your architecture relies on one of those, please open a ticket with concrete repro details from your real setup.

Tell us how it goes

WPF Hot Reload in Rider exists because developers repeatedly and specifically told us what they needed, so the best thing you can do now is keep that feedback coming. Try it on your real projects, not just a sample, and tell us how it’s working for you here in the comments, over on X, or via our issue tracker.

We’re going to keep expanding framework coverage and chipping away at the limitations above through the EAP cycle and beyond. Thanks to everyone who voted, commented, and waited, and welcome to everyone trying this for the first time. 

Read the whole story
alvinashcraft
22 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Pointers in C# and Memory Safety: Span vs. C# 16 unsafe

1 Share
June 3, 2026 9 minutes read

Pointers-in-C#-and-Memory-Safety Span-vs.-C# 16-unsafe

For most of its life, C# has kept you away from raw memory. You get garbage collection, bounds-checked arrays, and a type system that refuses to read past a buffer. That is a feature, not a limitation. But every few years the safety net costs too much, and you go looking for the trapdoor. In C#, that trapdoor is the unsafe keyword and pointers.

Lately, the reasons to open it have mostly evaporated. Span<T> arrived in 2018 and took over the niche unsafe code once owned: slicing memory at near-pointer speed. C# 16 now goes further and tightens what unsafe even means. This post walks both ends of that spectrum.

Why pointers existed in the first place

Pointers in C# are no accident. The language designers added them on purpose, for three jobs:

  1. Talking to native code that expects a raw address and a length.
  2. Squeezing out the last bit of performance in a tight loop by skipping the array bounds check.
  3. Reading and reinterpreting bytes directly, for example when parsing a binary format.

You opt in with an unsafe context and the AllowUnsafeBlocks switch. That unlocks pointer types, the address-of operator (&), pointer arithmetic, fixed, and stackalloc.

Here is the classic example: copying bytes from one array to another by hand.

static unsafe void Copy(byte[] source, int sourceOffset,
                        byte[] target, int targetOffset, int count)
{
    if (source == null || target == null)
        throw new ArgumentException("source or target is null");
    if (sourceOffset < 0 || targetOffset < 0 || count < 0)
        throw new ArgumentException("offset or count is negative");
    if (source.Length - sourceOffset < count ||
        target.Length - targetOffset < count)
        throw new ArgumentException("not enough room to copy");

    // Pin both arrays so the GC cannot move them while we hold pointers.
    fixed (byte* pSource = source, pTarget = target)
    {
        for (int i = 0; i < count; i++)
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
    }
}

Notice everything around the actual copy. The fixed statement pins the arrays, because the GC moves managed objects and a pointer to a moving target corrupts memory. Pinning also isn’t free: it blocks heap compaction, and long-held pins fragment memory. And you wrote those three guard clauses by hand, because nothing checks your indices anymore. Get that arithmetic wrong and you get no exception, just a buffer overrun.

That is the deal with pointers: fast and flexible, but now you are the bounds checker, the lifetime manager, and the safety reviewer.

Enter Span<T>

Span<T> is a small value type (a ref struct) representing a contiguous region of memory: a reference to the first element, plus a length. The crucial parts are:

  • Span<T> doesn’t care where that memory lives. It can point into a managed array, into a stackalloc block, at a field inside an object, a character inside a string or into memory handed to it by native code. One type, one API, over several very different backing stores.
  • This works because of a specific feature of the .NET runtime: managed pointers (called byrefs in the CLR, surfaced as ref in C#). Unlike a raw unmanaged pointer (T*), a managed pointer can point into the interior of a GC object, and the GC knows about it — when a compacting collection moves the object, it rewrites the pointer. That’s why a Span<T> over a managed array needs no fixed/pinning: the GC keeps the reference valid as objects move.
  • The catch is that managed pointers are confined to the stack (and registers). A byref can point at the stack just as easily as at the heap, so letting one be stored on the heap would risk it outliving what it points to — a dangling reference. The type system enforces this confinement (not the GC), which is also why Span<T> is a ref struct: it can’t be boxed, stored in a field of a class, captured by a lambda, or persist across an await.

Building and slicing a Span

You can build one from an array with a plain assignment:

var data = new byte[10];
Span<byte> bytes = data;          // implicit conversion from T[]

Slicing is where it starts to pay off. A slice is just another Span pointing into the same memory with a different start and length. No allocation, no copy:

Span<byte> middle = bytes.Slice(start: 2, length: 4);
middle[0] = 42;                   // writes straight into data[2]
// middle[4] = 1;                 // throws IndexOutOfRangeException

That index is bounds-checked, which is the first big difference from a pointer. Step outside the slice and you get a clean exception instead of silent corruption.

Span-Memory-Safety

The companion type ReadOnlySpan<T> gives you the same view with no write access, which is what lets you slice a string without allocating:

string text = "hello, world";
ReadOnlySpan<char> world = text.AsSpan().Slice(start: 7, length: 5);
// text.Substring(7, 5) would have allocated a brand new string here.

stackalloc without unsafe

The same Span can wrap stack memory through stackalloc, and — this is the surprising part — you do not need an unsafe block to do it:

Span<int> numbers = stackalloc int[3];
for (int i = 0; i < numbers.Length; i++)
    numbers[i] = i;

Before C# 7.2, the result of stackalloc could only land in a pointer, which dragged the whole unsafe requirement along with it. Targeting a Span<T> instead keeps the speed of stack allocation while staying inside safe code. A common pattern is to pick the stack for small buffers and the heap for large ones, all in a single expression:

const int StackLimit = 256;
Span<byte> buffer = length <= StackLimit
    ? stackalloc byte[length]
    : new byte[length];
// ... work on buffer the same way regardless of where it came from.

Before Span, that decision forced you to either duplicate the logic or pin a heap buffer and fall back to pointers. Now it’s one ternary.

A Span can’t leave the stack: compiler safety

We saw earlier why a Span<T> can’t leave the stack: it’s a ref struct wrapping a managed interior pointer, and the type system won’t let you box it, store it in a field, capture it in a lambda, or carry it across an await. The thing worth adding here is that the rule isn’t merely documentation — the compiler enforces it. Try to return a span over stack memory and the build fails outright:

Span-Compiler-Check

That error is the compiler performing the safety review you used to do in your head with pointers. When you genuinely need to hold a slice on the heap or carry it across an await, that’s what Memory<T> is for: a heap-friendly cousin (not a ref struct, so it has none of those restrictions) that hands you a Span on demand when you’re ready to do synchronous work.

Benchmark: Reference vs. Span vs. Pointer

Is span faster than reference and pointer faster than span? This is the question that decides everything, because if Span were merely “safe but slow” nobody serious would have adopted it. The fair way to answer it is to pick workloads where a raw pointer has something genuine to win, and the clearest of those is reinterpreting raw bytes at a wider width — the thing binary parsing, checksums, and interop do all day. Here are three such workloads you can run yourself with BenchmarkDotNet. Each reads a byte[] three ways that all compute the same result:

  • Reference — the only thing a plain byte[] and safe indexing allow: rebuild each value from individual bytes with shifts, which is several bounds-checked narrow reads plus assembly per value.
  • Span — one safe, bounds-checked wide read per value through BinaryPrimitives.
  • Pointer — one unchecked wide read, *(T*)p.

The first example decodes and sums little-endian Int32 records out of a buffer — the heart of any binary deserializer. The other two examples keep this exact three-way shape and only change the width. The second folds a buffer into a 64-bit checksum eight bytes at a time (BinaryPrimitives.ReadUInt64LittleEndian versus *(ulong*)p); the third sums signed 16-bit PCM audio samples (ReadInt16LittleEndian versus *(short*)p). The full source for all three is in the companion project.

// dotnet add package BenchmarkDotNet
// Build and run in Release: dotnet run -c Release
// The project must set <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

using System.Buffers.Binary;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

// ---------------------------------------------------------------------------
// Three workloads where raw pointers fairly beat Span, which fairly beats a
// plain safe array. The common lever is reinterpreting raw bytes at a WIDER
// width -- the niche where pointers still genuinely win:
//
//   Reference : assemble the value byte-by-byte with shifts (N narrow,
//               bounds-checked reads + OR assembly per value).
//   Span      : ONE safe wide read via BinaryPrimitives (one bounds-checked
//               slice per value).
//   Pointer   : ONE unchecked wide read (*(T*)p) -- no slice, no bounds check.
//
// Every method computes the SAME result over the SAME bytes, so the console
// guard below proves correctness before we trust the timings.
// ---------------------------------------------------------------------------

// Correctness guard: all three paths must agree, at both sizes.
foreach (var n in new[] { 1024, 65536 })
{
    var a = new ParseInt32Sum { Count = n }; a.Setup();
    if (a.Reference() != a.Span() || a.Span() != a.Pointer())
        throw new Exception($"ParseInt32Sum mismatch at {n}");

    var b = new XorFoldUInt64 { Count = n }; b.Setup();
    if (b.Reference() != b.Span() || b.Span() != b.Pointer())
        throw new Exception($"XorFoldUInt64 mismatch at {n}");

    var c = new SumInt16Samples { Count = n }; c.Setup();
    if (c.Reference() != c.Span() || c.Span() != c.Pointer())
        throw new Exception($"SumInt16Samples mismatch at {n}");
}
Console.WriteLine("Correctness guard passed.");

BenchmarkRunner.Run<ParseInt32Sum>();
BenchmarkRunner.Run<XorFoldUInt64>();
BenchmarkRunner.Run<SumInt16Samples>();

// ===========================================================================
// 1. Binary parsing: decode and sum little-endian Int32 records from a byte[].
// ===========================================================================
[MemoryDiagnoser]
public class ParseInt32Sum
{
    private byte[] _data = null!;

    [Params(1024, 65536)]
    public int Count;            // number of 32-bit values in the buffer

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Count * 4];
        new Random(123).NextBytes(_data);
    }

    // Reference: read each 4-byte little-endian int by hand. Four bounds-checked
    // byte loads plus shift/OR assembly for every value.
    [Benchmark(Baseline = true)]
    public long Reference()
    {
        byte[] data = _data;
        int count = Count;
        long sum = 0;
        for (int i = 0; i < count; i++)
        {
            int j = i * 4;
            int v = data[j]
                  | (data[j + 1] << 8)
                  | (data[j + 2] << 16)
                  | (data[j + 3] << 24);
            sum += v;
        }
        return sum;
    }

    // Span: one safe, bounds-checked wide read per value.
    [Benchmark]
    public long Span()
    {
        ReadOnlySpan<byte> data = _data;
        int count = Count;
        long sum = 0;
        for (int i = 0; i < count; i++)
            sum += BinaryPrimitives.ReadInt32LittleEndian(data.Slice(i * 4, 4));
        return sum;
    }

    // Pointer: reinterpret 4 bytes as an int directly -- no slice, no check.
    [Benchmark]
    public unsafe long Pointer()
    {
        int count = Count;
        long sum = 0;
        fixed (byte* basePtr = _data)
        {
            for (int i = 0; i < count; i++)
                sum += *(int*)(basePtr + i * 4);
        }
        return sum;
    }
}

// ===========================================================================
// 2. Checksum: fold a byte[] into a 64-bit accumulator, 8 bytes at a time.
// ===========================================================================
[MemoryDiagnoser]
public class XorFoldUInt64
{
    private byte[] _data = null!;

    [Params(1024, 65536)]
    public int Count;            // number of 64-bit words in the buffer

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Count * 8];
        new Random(163).NextBytes(_data);
    }

    // Reference: build each 64-bit word from eight bounds-checked byte loads.
    [Benchmark(Baseline = true)]
    public ulong Reference()
    {
        byte[] data = _data;
        int count = Count;
        ulong acc = 0;
        for (int i = 0; i < count; i++)
        {
            int j = i * 8;
            ulong w = (ulong)data[j]
                    | (ulong)data[j + 1] << 8
                    | (ulong)data[j + 2] << 16
                    | (ulong)data[j + 3] << 24
                    | (ulong)data[j + 4] << 32
                    | (ulong)data[j + 5] << 40
                    | (ulong)data[j + 6] << 48
                    | (ulong)data[j + 7] << 56;
            acc ^= w;
        }
        return acc;
    }

    // Span: one safe, bounds-checked 8-byte read per word.
    [Benchmark]
    public ulong Span()
    {
        ReadOnlySpan<byte> data = _data;
        int count = Count;
        ulong acc = 0;
        for (int i = 0; i < count; i++)
            acc ^= BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(i * 8, 8));
        return acc;
    }

    // Pointer: reinterpret 8 bytes as a ulong directly.
    [Benchmark]
    public unsafe ulong Pointer()
    {
        int count = Count;
        ulong acc = 0;
        fixed (byte* basePtr = _data)
        {
            for (int i = 0; i < count; i++)
                acc ^= *(ulong*)(basePtr + i * 8);
        }
        return acc;
    }
}

// ===========================================================================
// 3. Audio: sum signed 16-bit PCM samples packed in a byte[].
// ===========================================================================
[MemoryDiagnoser]
public class SumInt16Samples
{
    private byte[] _data = null!;

    [Params(1024, 65536)]
    public int Count;            // number of 16-bit samples in the buffer

    [GlobalSetup]
    public void Setup()
    {
        _data = new byte[Count * 2];
        new Random(99).NextBytes(_data);
    }

    // Reference: rebuild each signed 16-bit sample from two bounds-checked bytes.
    [Benchmark(Baseline = true)]
    public long Reference()
    {
        byte[] data = _data;
        int count = Count;
        long sum = 0;
        for (int i = 0; i < count; i++)
        {
            int j = i * 2;
            short v = (short)(data[j] | (data[j + 1] << 8));
            sum += v;
        }
        return sum;
    }

    // Span: one safe, bounds-checked 2-byte read per sample.
    [Benchmark]
    public long Span()
    {
        ReadOnlySpan<byte> data = _data;
        int count = Count;
        long sum = 0;
        for (int i = 0; i < count; i++)
            sum += BinaryPrimitives.ReadInt16LittleEndian(data.Slice(i * 2, 2));
        return sum;
    }

    // Pointer: reinterpret 2 bytes as a short directly.
    [Benchmark]
    public unsafe long Pointer()
    {
        int count = Count;
        long sum = 0;
        fixed (byte* basePtr = _data)
        {
            for (int i = 0; i < count; i++)
                sum += *(short*)(basePtr + i * 2);
        }
        return sum;
    }
}

Running all three in Release produces the same ordering every time — Pointer fastest, Span in the middle, the plain array baseline slowest:

BenchmarkDotNet v0.13.12, Windows 10, .NET 8.0.27, X64 RyuJIT AVX2
Intel Xeon E-2176M CPU 2.70GHz, 6 physical cores

// 1. Parse + sum little-endian Int32 records
| Method    | Count | Mean         | Ratio | Allocated |
|---------- |------ |-------------:|------:|----------:|
| Reference | 1024  |   2,157.9 ns |  1.00 |         - |
| Span      | 1024  |     609.0 ns |  0.26 |         - |
| Pointer   | 1024  |     439.2 ns |  0.19 |         - |
|           |       |              |       |           |
| Reference | 65536 | 128,867.7 ns |  1.00 |         - |
| Span      | 65536 |  38,266.2 ns |  0.30 |         - |
| Pointer   | 65536 |  27,203.2 ns |  0.21 |         - |

// 2. Fold a buffer into a 64-bit checksum, 8 bytes at a time
| Method    | Count | Mean         | Ratio | Allocated |
|---------- |------ |-------------:|------:|----------:|
| Reference | 1024  |   3,861.7 ns |  1.00 |         - |
| Span      | 1024  |     780.1 ns |  0.20 |         - |
| Pointer   | 1024  |     491.8 ns |  0.13 |         - |
|           |       |              |       |           |
| Reference | 65536 | 267,130.2 ns |  1.00 |         - |
| Span      | 65536 |  49,796.0 ns |  0.20 |         - |
| Pointer   | 65536 |  31,794.8 ns |  0.13 |         - |

// 3. Sum signed 16-bit PCM audio samples
| Method    | Count | Mean        | Ratio | Allocated |
|---------- |------ |------------:|------:|----------:|
| Reference | 1024  |  1,027.3 ns |  1.00 |         - |
| Span      | 1024  |    562.1 ns |  0.55 |         - |
| Pointer   | 1024  |    430.5 ns |  0.42 |         - |
|           |       |             |       |           |
| Reference | 65536 | 65,226.9 ns |  1.00 |         - |
| Span      | 65536 | 36,199.6 ns |  0.55 |         - |
| Pointer   | 65536 | 27,288.3 ns |  0.42 |         - |

Reading the numbers

The ordering is the same in all three, and the reason is mechanical. The reference path issues several bounds-checked byte loads and shifts them together; Span replaces all of that with a single intrinsic read behind one bounds check; the pointer drops the bounds check too. The gap even tracks the width: at 16 bits the reference only assembles two bytes and trails by roughly 2x, while at 64 bits it assembles eight and falls behind by 6-8x.

Three things keep this in perspective.

  • First, the safe Span version is already 2-5x faster than the naive array code, allocates nothing, and cannot overrun the buffer — for most parsing work that is the win you were actually after.
  • Second, this advantage is specific to per-element widening reads. On a plain linear scan — just walking an array and adding up the elements — the JIT recognizes the Span indexer as an intrinsic and eliminates the bounds check, so Span and the pointer compile down to nearly identical instructions and finish in a dead heat. And even in the benchmarks above, if you reinterpret the whole buffer once with MemoryMarshal.Cast<byte, int> instead of slicing per value, Span closes most of the remaining distance to the pointer. The pointer’s edge is real, but it lives in a corner — tight, byte-level reinterpretation and interop — not across slicing and buffer work in general.
  • Third, the table can’t show the full cost of the pointer path. Its fixed block pins the buffer for the whole loop, and pinning carries a price this microbenchmark never charges it: a pinned buffer can’t move, so it blocks the GC from compacting the heap and, held often or for long, fragments it. That cost lands as GC pressure across the wider application, not as nanoseconds in a tight loop — so it stays invisible in the numbers above even though it is real.

unsafe Changes in C# 16

So far this has been the state of play for a few years. The newer development is that Microsoft is reworking what unsafe means as part of a broader memory-safety push. The work is already accepted in the official .NET design docs, lands as a preview in .NET 11 and a production release follows in .NET 12. It borrows openly from Rust. You mark unsafe operations explicitly, scope them narrowly, and tie each one to obligations the caller must satisfy.

Why revisiting unsafe almost two decades after its creation?

Security. The design doc puts it plainly — “Security is our top priority” — and memory-safety bugs like buffer overruns, dangling pointers, and type confusion are one of the largest sources of exploitable vulnerabilities. Government and industry have converged on the point. Agencies such as CISA, the NSA, and the FBI have urged a move to memory-safe languages, and an international partnership of cybersecurity agencies, together with the OpenSSF, already classifies C# as one of them. The gap is unsafe code. It is the one place where the compiler steps back and the developer alone carries responsibility for soundness. Making that boundary explicit, scoped, and auditable is the whole goal — and it matters more now that AI tools generate code faster than humans can review it.

Concrete changes

The whole model is opt-in, behind a new MSBuild property the design calls EnableRequiresUnsafe. It sits alongside the existing AllowUnsafeBlocks switch: AllowUnsafeBlocks still decides whether unsafe is allowed at all, while EnableRequiresUnsafe turns on the stricter contract rules below.

The headline change is that unsafe moves from being a context you turn on to a contract you advertise — and the compiler enforces it. Call an unsafe member outside an unsafe context and the build fails, with an error rather than a warning:

void Caller()
{
    M();            // error: the call to M() is not in an unsafe context
}

unsafe void M() { }

A few of the other concrete shifts:

  • The unsafe modifier now applies per member — on methods and local functions, properties, and fields — rather than to a whole type. The marker moves down to the specific member where the unverifiable access actually happens. Mark a field unsafe and even reading it requires an unsafe context of its own.
  • Merely using a pointer type in a signature stops being an unsafe operation on its own. Pointer types are no longer unsafe; only pointer dereferences are. Passing a byte* around is not the dangerous part; reading through it is.
  • A new safe keyword is planned for extern and LibraryImport declarations, so you can attest that a particular native call is sound without that unsafety leaking out to everyone who calls you.

The model pulls in documentation. Unsafe members must carry a /// <safety> section spelling out what the caller must guarantee, and an analyzer flags the ones that don’t. Inside the implementation, // SAFETY: comments explain how each unsafe step keeps its invariants — again, a habit lifted straight from the Rust community.

What good unsafe code looks like

Well-behaved unsafe code under the new model has three parts: an unsafe field with its invariant written down, unsafe blocks at the boundary that discharge it, and a public surface callers use without an unsafe block of their own.

/// <safety>
/// Callers must pass a non-null pointer to at least <paramref name="length"/>
/// readable bytes. The memory must stay alive for the duration of the call.
/// </safety>
static unsafe int SumBytes(byte* data, int length)
{
    // SAFETY: length is validated by the public wrapper below, and the
    // caller's contract guarantees 'data' covers that many bytes.
    int total = 0;
    unsafe
    {
        for (int i = 0; i < length; i++)
            total += data[i];
    }
    return total;
}

To ease the transition, you adopt all of this project by project, the way nullable reference types rolled out, and the runtime libraries migrate first. A planned dotnet format fixer can wrap call sites in unsafe blocks and move modifiers from types down to members for you. What the tooling explicitly cannot do is infer your safety obligations or write the documentation for you — that part is still a human judgment call, which is rather the point.

So which one should you reach for?

The honest answer for most code is: neither, until you have measured. Plain arrays and the high-level APIs are fine for the vast majority of work.

When you do need to get closer to the metal, Span<T> and ReadOnlySpan<T> should be the default. They give you slicing, stack allocation, and byte reinterpretation at near-pointer speed. And the compiler and runtime keep you clear of the buffer overruns, dangling references, and pinning headaches that come free with raw pointers. Microsoft rebuilt the whole framework around them — Parse overloads, Stream.ReadAsync, the UTF-8 formatters — so you are swimming with the current.

Reach for unsafe and pointers only when Span genuinely cannot express what you need: certain interop scenarios, fixed-size buffers in structs, function pointers via delegate*, or a measured hot path where pinning-once-and-looping really does beat everything else. And when you do, the C# 16 model is worth welcoming rather than resenting. Making the safety contract explicit, scoped, and documented costs you nothing that matters. It just turns the assumptions you were already making in your head into something the compiler and the next reader can see.

Pointers are not going away. They are simply being put back where they belong — behind a clearly marked door, used on purpose, by people who can say out loud why it is safe.

Further reading

The post Pointers in C# and Memory Safety: Span vs. C# 16 unsafe appeared first on NDepend Blog.

Read the whole story
alvinashcraft
34 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

How to Deploy a Vibe Coded Project

1 Share
How to Deploy a Vibe Coded Project - deploying a web site, app, or game that you vibe coded
Read the whole story
alvinashcraft
48 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Breaking Change: The .On() Method in the Latest Copilot SDK

1 Share

If you're upgrading to the latest version of the Copilot SDK beta, there's a syntax change you need to know about. The .On() method now requires a generic type parameter. As the SDK is still in beta, breaking changes are not unexpected. But this one should be handled with extra care to avoid resource leaks.

This post walks you through what changed, why it matters, and how to update your code.

What changed

In previous versions of the SDK, registering an event handler looked something like this:

// Old syntax
app.On(evt =>
{
    if (ev is AssistantMessageDeltaEvent deltaEvent)
    {
        Console.Write(deltaEvent.Data.DeltaContent);
    }); 
}

In the latest version, the .On() method is now generic. You must explicitly specify the activity or event type you're handling:

// New syntax
using var subscription = app.On<AssistantMessageDeltaEvent>(evt =>
{
    Console.Write(deltaEvent.Data.DeltaContent);
});

The method signature has changed from:

// Before

IDisposable On(SessionEventHandler handler)

// After
IDisposable On<TSessionEvent>(Action<TSessionEvent> handler)

Why you must capture and dispose the return value

This is the part where you can get into trouble when using the updated API

.On<T>() returns an IDisposable — a subscription handle. If you don't hold onto this reference and dispose it properly, you risk:

  • Memory leaks: The handler stays registered indefinitely, holding references that prevent garbage collection.
  • Duplicate handlers: If .On<T>() is called multiple times (e.g., on reconnects or re-initialization), handlers accumulate and fire multiple times per event.
  • Unexpected behavior after shutdown: Handlers may continue firing even after you intend the component to be inactive.

This was also the case with the non-generic.On() method. But I noticed some team members refactor this original implementation (that handles the subscription correctly):

To this(which no longer handles the subscription)

The correct pattern

Use using declarations (C# 8+) or using blocks to ensure deterministic cleanup:

Option 1

// Option 1: using declaration (recommended for scoped lifetime)
using var subscription = app.On<AssistantMessageDeltaEvent>(evt =>
{
    Console.Write(deltaEvent.Data.DeltaContent);
});

Option 2

If you need to capture multiple events in one subscription you can subscribe to the base SessionEvent and still use pattern matching:

// Option 2: using declaration (recommended for scoped lifetime)
using var subscription = app.On<SessionEvent>(evt =>
{
  switch(evt){//Add your pattern matching here}   
}

Summary

The new generic API is a clear improvement and helps to align with the syntax of other language versions(like TypeScript), but it does require a small amount of care around subscription management. The good news is that the pattern is standard C# — if you're already using IDisposable elsewhere in your project, this will feel familiar.

Take the time to audit your existing .On() calls before shipping to production.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

The agent always cost this much

1 Share

GitHub moved Copilot from Premium Request Units to AI Credits this week, and developers on Reddit were reporting within hours that a single Claude Sonnet prompt had burned half a month of credits, and that a $100 Max plan could be exhausted inside a day. The clearest framing of why came from Mario Rodriguez, GitHub's Chief Product Officer, writing on the GitHub blog:

Today, a quick chat question and a multi-hour autonomous coding session can cost the user the same amount. GitHub has absorbed much of the escalating inference cost behind that usage, but the current premium request model is no longer sustainable.

The capital outlay behind frontier models is enormous, running to multi-billion-dollar training runs, inference fleets on the most expensive GPUs. I have written before about whether the math even works at the macro level, and the answer at trillion-dollar CapEx scale is no. Charging ten dollars a month for unbounded access to that supply chain was never sustainable.

Every serious AI tooling vendor is converging on usage-based pricing for the same reason: basic capitalism. Frontier inference is expensive, parallel agents multiply the expense, and no flat fee survives that kind of unbounded scale. Developers are learning what it took cloud engineers a good 10 years to accept: flat-rate predictability is the last thing to arrive, not the first.

The pattern emerging seems to be Frontier at the edges, cheap in the middle. Planning is where the frontier models earn their cost. Letting a strong model think hard about what the work actually is, before any code gets written, prevents the kind of misdirected implementation that becomes expensive. The implementation itself, once the plan is established, is largely mechanical, and a cheaper model can do most of the hard labor. Verification can then swing back to the frontier.

Underneath all of that, the ambient AI in the IDE continues to run on cheaper models, handling Next Edit Suggestions, whole-line completions, call-stack analysis, and the constant background help that makes the editor feel intelligent. These calls are high-frequency, latency-sensitive, and low-stakes per individual invocation, and that cheap floor may get even cheaper. At BUILD this week, Satya Nadella pointed out that "the amount of compute you have at the edge is actually astounding," and framed the destination as "unmetered intelligence to every desk and every home." The Windows side of the answer is real on-device inference support, NPU acceleration across the major silicon vendors, and small-model families tuned to run on the chip rather than the cloud. The direction is hybrid by default: route what needs the frontier to a paid API, run what can run locally on silicon the developer already owns.

The era of agents running wild on someone else's dime is over. What replaces it is already visible in outline, and it rewards developers who think before they fan out, match the model and where it runs to the task, and treat context as a resource worth caching.

I work on Visual Studio at Microsoft. The views here are mine alone.

An open Gutenberg Bible on display in a museum case, both pages showing dense two-column Latin type with hand-illuminated initials in blue and red.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Rotation revisited: Another unidirectional algorithm

1 Share

Some time ago, we looked at the problem of swapping two blocks of memory that reside inside a larger block, in constant memory, and along the way, we learned about std::rotate which swaps two adjacent blocks of memory (not necessarily the same size).

I noted in a postscript that clang’s libcxx and gcc’s libstdc++ contain specializations of std::rotate for random-access iterators that view the operation as a permutation and decomposes the permutation into cycles.

I was mistaken.

The implementation in gcc’s libstdc++ has special cases for single-element rotations, but in the general case, it uses a different algorithm.

Let’s call the blocks of memory to be exchanged A and B, where A is made up of elements A1, A2, A3, and so on; and block B has elements B1, B2, B3, and so on. Without loss of generality, suppose the A block is smaller. (If not, we can just mirror the algorithm.) And for concreteness let’s say that the elements are A1, A2, A3, B1, B2, B3, B4, B5.

A1 A2 A3 B1 B2 B3 B4 B5
           
first     mid         last

Exchange elements at first and mid, then move both iterators forward. After the first step, we have this:

B1 A2 A3 A1 B2 B3 B4 B5
           
  first     mid       last

After three steps, we have moved all of the A’s out and replaced them with an equal number of B’s.

B1 B2 B3 A1 A2 A3 B4 B5
           
      first     mid   last

But don’t stop. Keep on going until mid reaches last.

B1 B2 B3 B4 B5 A3 A1 A2
             
          first         mid
last

All of the B’s have been swapped to their final positions, but the A’s are jumbled.

But you can predict the exact nature of the jumbling. The A block is in two chunks. If we let n be the total number of elements |A| + |B| and a be the number of elements in A, then the first chunk has the final n % a elements, and the second chunk has the initial a − (n % a) elements.

Therefore, we can recursively rotate the two pieces of the A block to finish the job. Move mid to first + (n % a) and restart the algorithm.

This algorithm performs n − 1 swaps. You can calculate this inductively by observing that we perform |B| swaps, and then recursively rotate |A|. Or you can calculate this directly by observing that each swap moves one element to its final position, except that the final swap moves two elements to their final position.

The locality of this algorithm fairly good. The first iterator moves steadily forward, and the mid iterator moves forward most of the time, with at most O(log (min(|A|, |B|)) backward resets.

Next time, we’ll make a shocking discovery about this algorithm.

The post Rotation revisited: Another unidirectional algorithm appeared first on The Old New Thing.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories