#stackalloc
Decompiling C# 12 inline arrays and stackalloc: fixed buffers without the unsafe
C# 12 added inline arrays, and they are easy to misread even in source: a fixed-size buffer that lives _inside_ its containing struct, value-type all the way down, no heap allocation, no array object. It is the managed, safe answer to the C programmer's `int buf[8]` — the thing you previously could only get with an `unsafe` fixed-size buffer. Paired with `stackalloc`, which has been quietly modernised to hand you a `Span<T>` instead of a raw pointer, you can now write buffer-heavy, allocation-free code without a single `unsafe` keyword. That safety is a source-level illusion built by the compiler, and the illusion is exactly what a decompiler has to reconstruct. Underneath, an inline array is not an array — it is a one-field struct with an attribute that tells the _runtime_ to lay out N copies of the field. Element access is not `ldelem` — it is a call to a runtime intrinsic that returns a managed reference. And `stackalloc` is not safe at all underneath — it is a `localloc`, the rawest stack allocation the CLR has, wrapped in a `Span` constructor so the pointer never reaches your eyes. A decompiler that lowers to the metal and stops shows you ref arithmetic, helper calls with names you cannot type, and `unsafe` blocks that were never in the source. The code it emits often will not even recompile. This post walks the IL for both features and shows what a decompiler must recognise to put the source back together. ## An inline array is a struct wearing an attribute Here is the declaration and a trivial use: [System.Runtime.CompilerServices.InlineArray(8)] public struct Buffer8 { private int _element0; } public static int Sum(Buffer8 buf) { int total = 0; for (int i = 0; i < 8; i++) total += buf[i]; return total; } `Buffer8` has one field, `_element0`, and one attribute, `[InlineArray(8)]`. That is the whole trick: the attribute instructs the runtime to allocate the field eight times contiguously, so a `Buffer8` is 32 bytes and `buf[3]` means "the fourth `int` in that block." There is no array object and no length word — the length is baked into the attribute, known at compile time, and never stored at runtime. In metadata the type is a plain `sealed value type` with a single field. Nothing in the type definition says "array"; the only signal is the custom attribute: .class public sequential ansi sealed beforefieldinit Buffer8 extends [System.Runtime]System.ValueType { .custom instance void [System.Runtime]System.Runtime.CompilerServices.InlineArrayAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) // InlineArray(8) .field private int32 _element0 } A decompiler that classifies types by their shape sees a one-field struct and, unless it reads that attribute, prints it as a one-field struct — losing the entire meaning. So the first recognition rule is: **a value type carrying`[InlineArray(N)]` is an inline array of its single field's type, length N**, and it must be rendered with the attribute and the single backing field exactly as the compiler emitted them. ## Indexing lowers to a runtime intrinsic, not ldelem Now the access. `buf[i]` compiles not to an array-load but to a call that produces a managed reference to element `i`. The compiler routes it through a generated helper in `<PrivateImplementationDetails>` that wraps `RuntimeHelpers.InlineArrayElementRef` (or builds a `Span<T>` over the buffer with `InlineArrayAsSpan` and indexes that, depending on context). The read inside the loop lowers roughly to: // total += buf[i]; ldarga.s buf ldloc i call !!1& [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::InlineArrayElementRef<valuetype Buffer8, int32>(!!0&, int32) ldind.i4 add The `InlineArrayElementRef` call takes a reference to the buffer and an index, and returns a _byref_ to the element; `ldind.i4` then dereferences it to load the `int`. For a write you would see `stind.i4` against the same kind of ref. There is no `ldelem`, no bounds-check opcode pattern, nothing that looks like an array — just an intrinsic call returning a `ref`. A naive decompiler renders that literally: a call to `RuntimeHelpers.InlineArrayElementRef<Buffer8, int>(ref buf, i)` followed by a dereference, or worse, a reference to a `<PrivateImplementationDetails>` helper whose name contains characters you cannot write in C#. Either way the output does not say `buf[i]` and does not recompile. The recognition rule is: **a call to the inline-array element-ref intrinsic (or the generated helper around it) against an`[InlineArray]` type, followed by a load or store through the returned ref, is an indexer access — render it as `buf[i]`.** The same applies to the `InlineArrayAsSpan` form, which a decompiler should fold back either into element access or into a `Span<T>` view of the buffer, depending on how the result is used. ## stackalloc is a localloc wearing a Span Now the second feature. Modern `stackalloc` assigned to a `Span<T>` looks completely safe in source: public static int SumFirstFour(ReadOnlySpan<int> input) { Span<int> scratch = stackalloc int[4]; for (int i = 0; i < 4; i++) scratch[i] = input[i] * input[i]; int total = 0; foreach (int v in scratch) total += v; return total; } No `unsafe`, no pointer. But `stackalloc` has exactly one lowering: the `localloc` instruction, which allocates a block on the current method's stack frame and pushes a native pointer to it. The compiler sizes the block (`4 * sizeof(int)`), allocates, then constructs a `Span<int>` over the raw pointer and the element count using the `Span<T>(void*, int)` constructor: // Span<int> scratch = stackalloc int[4]; ldc.i4.4 conv.u ldc.i4.4 mul.ovf.un // 4 elements * 4 bytes localloc // -> native int (pointer to the stack block) ldc.i4.4 newobj instance void valuetype [System.Runtime]System.Span`1<int32>::.ctor(void*, int32) stloc.0 // scratch That is the signature: an element-count `ldc`, a size multiply (`conv.u` / `mul.ovf.un`), a `localloc`, then a `newobj` on `Span<T>::.ctor(void*, int32)`. A decompiler that renders those instructions literally produces an `unsafe` method with a `void*`, a multiplication, a `localloc` intrinsic (which C# cannot even express directly), and a `Span` constructor taking a pointer. None of that is in the source, and the `void*` forces an `unsafe` context the author deliberately avoided. The recognition rule folds the whole shape into one expression: **a`localloc` whose size is `count * sizeof(T)`, consumed by `Span<T>` or `ReadOnlySpan<T>`'s `(void*, int)` constructor, is `stackalloc T[count]`.** When the element type's size is a compile-time constant and the count is too, Glass reconstructs `stackalloc int[4]`; when the count is a variable, it reconstructs `stackalloc int[n]`. The raw pointer, the multiply, and the constructor all disappear back into the single `stackalloc` the author typed, and the method stays safe — no `unsafe` keyword, because the source had none. There is a subtlety worth naming: not every `stackalloc` becomes a `Span`. The older form, `int* p = stackalloc int[4];`, assigns the pointer directly and _is_ genuinely unsafe — there the `void*` and `unsafe` belong in the output, because they were in the source. The decompiler distinguishes the two by what consumes the `localloc`: a `Span`/`ReadOnlySpan` constructor means the safe form, a direct pointer assignment means the unsafe form. Getting that distinction right is the difference between faithfully reproducing a safe method and falsely accusing it of being unsafe. ## Why faithful recovery matters here specifically For most language features, a decompiler that gets the details slightly wrong produces code that is ugly but still builds. These two are different, because both features exist precisely to give you low-level performance _while staying in safe, high-level C#_ — and the incorrect decompilation destroys exactly that property. An inline array rendered as `InlineArrayElementRef` calls references helpers in `<PrivateImplementationDetails>`, a compiler-internal type whose members have names containing `<` and `>` that are illegal in C# source. That output cannot be pasted back and compiled; it is a description of the IL, not a reconstruction of the program. A `stackalloc` rendered as `localloc` plus a `void*` turns a safe method into one that requires `unsafe`, misrepresenting the method's safety contract and, again, often failing to compile without edits the author never made. Glass reads both patterns at the level the author worked at. It classifies `[InlineArray(N)]` structs as inline arrays and renders their element access as indexers; it recognises the `localloc`-plus-`Span`-constructor shape as `stackalloc` and keeps the method safe, while still distinguishing the genuinely unsafe pointer form. The output reads like the source — `buf[i]` and `stackalloc int[4]` — and, just as importantly, recompiles like the source, which is the real test of whether a decompiler understood the program or merely transcribed its bytes. ## Summary Inline arrays and modern `stackalloc` are two of the clearest cases where the IL is dramatically lower-level than the C#. An inline array is a one-field struct whose `[InlineArray(N)]` attribute is the only evidence it is a buffer, indexed through runtime intrinsics that return byrefs rather than any array opcode. A `Span`-typed `stackalloc` is a raw `localloc` dressed in a `Span<T>(void*, int)` constructor so the pointer never shows. A decompiler earns its keep by recovering the author's intent: the inline-array declaration and its indexers, the single `stackalloc` expression, and the safe, `unsafe`-free method body that made these features worth using in the first place.
dev.to
October 11, 2026 at 8:09 AM
Microsoft outlines C#/.NET changes to make memory bugs rarer: tighter rules around stackalloc, refs and Span, plus better diagnostics to catch unsafe patterns earlier. Safer defaults for everyday code. #CSharp #dotnet #MemorySafety

devblogs.microsoft.com/dotnet/impro...
June 22, 2026 at 2:04 PM
HDISBS bé, compilar c# a wasm és fàcil i al final si cal fer-ho en c no seria gaire diferent perquè de moment ho estic fent en un sol fil així que mentre tot sigui suficientment ràpid no cal enganxar-se els dits amb OMP ni amb la memòria (de fet faig servir stackalloc on puc a C# i de moment tot bé)
June 22, 2026 at 11:22 AM
Span is a stack-only view, not a free speedup. It wins on stackalloc, no-copy slicing, and bounds-check elision. Nowhere else.
https://startdebugging.net/2026/06/what-is-span-and-when-does-it-make-my-code-faster/?utm_source=bluesky&utm_medium=social&utm_campaign=auto
June 20, 2026 at 11:22 AM
C# can actually be quite low-level! 🔥

I saw this awesome blog that shows how powerful C# can get almost like C/C++

It dives deep into Span, unsafe code, stackalloc, and high-performance patterns

Really interesting read!

#CSharp #GameDev #DotNet
Is C# a low-level language?
I’m a massive fan of everything Fabien Sanglard does, I love his blog and I’ve read both his books cover-to-cover (for more info on his books, check out the recent Hansleminutes podcast).
mattwarren.org
May 27, 2026 at 9:04 AM
Szymon Kulec on using unsafe code in .NET: "Give me the chainsaw so that I can cut down the tree very quickly, but also so that if I slip, you're going to call me Lefty from now on."

Great episode on knowing when you need the chainsaw and when you don't 🎯

🔗
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 16, 2026 at 9:00 AM
CVE-2026-44375 - Nerdbank.MessagePack: Attacker-controlled stackalloc in DateTime decoding causes process-terminating StackOverflowException
CVE ID : CVE-2026-44375

Published : May 14, 2026, 3:16 p.m. | 1 hour, 14 minutes ago

Description : Nerdbank.MessagePack is a Nativ...
CVE-2026-44375 - Nerdbank.MessagePack: Attacker-controlled stackalloc in DateTime decoding causes process-terminating StackOverflowException
Nerdbank.MessagePack is a NativeAOT-compatible MessagePack serialization library. Prior to 1.1.62, Nerdbank.MessagePack contains an uncontrolled stack allocation vulnerability in DateTime decoding. A malicious MessagePack payload can declare an oversized timestamp extension length, causing the reader to allocate an attacker-controlled number of bytes on the stack. This can trigger a StackOverflowException, which …
cvefeed.io
May 14, 2026 at 5:06 PM
𝗣𝗼𝘀𝘁 𝟭:

🎧 New episode of The Modern .NET Show!

Szymon Kulec joins me for a deep dive into systems programming in .NET: profiling, benchmarking, and the rare cases where unsafe code is justified.

🔗
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 13, 2026 at 11:31 AM
“Measure Twice, Cut Once” goes deep: profiling hot paths, BenchmarkDotNet micro‑benchmarks, spans/refs/stackalloc, Blittable JSON, and when unsafe code is the right tool. A sharp tour of real .NET systems programming.
👉 Listen: buff.ly/FPSZdW0
May 12, 2026 at 5:40 PM
𝗣𝗼𝘀𝘁 𝟭:

Szymon Kulec joins me for a deep dive into systems programming in .NET: profiling, benchmarking, and the rare cases where unsafe code is justified.

🔗 dotnetcore.show/season-8/measure-twice-cut-once-benchmarking-hot-paths-and-the-chainsaw-of-unsafe-code-with-szymon-kulec/
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code.
dotnetcore.show
May 7, 2026 at 4:00 PM
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec

Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and k
S08E18 - Measure Twice, Cut Once: Benchmarking, Hot Paths and the Chainsaw of Unsafe Code with Szymon Kulec
Szymon Kulec joins Jamie to explore systems programming in .NET: profiling with dotTrace, micro-benchmarking with BenchmarkDotNet, taking ownership of memory with stackalloc and refs, and knowing when (and when not) to reach for unsafe code
dotnetcore.show
May 1, 2026 at 8:31 AM
Node.js addons… in C#. Native AOT lets you export napi_register_module_v1, call N‑API via LibraryImport("node"), use spans + stackalloc for zero‑alloc UTF‑8, and ship shared libs without node‑gyp or Python. Clean interop, faster onboarding, fewer toolchains.
Read → ift.tt/2mz8B1E
April 29, 2026 at 10:05 PM
Optimize your C# code by reducing memory allocations! Discover how using Span and stackalloc can boost performance and efficiency. #CSharp #CodingTips
dev.to
March 12, 2026 at 6:15 AM
I recently ported some C code to C# using really super unsafe everything, pointers everywhere, stackalloc spans, etc. and realized that these is this whole other world of possibility if you want it. It made me respect modern C# even more because of how easy it let me bridge the two allocation styles
November 14, 2025 at 5:01 AM
As someone who studied programming in the 80s when memory and compute power were weeny 1) Use stackalloc 2) Even if you don't, it is still wicked fast. If you need speed, see rule 1

(started on punch cards in 70s on a 120 baud (no ks, or ms) acoustic coupler modem in high school.
November 14, 2025 at 12:40 AM
Wow, I had no idea stackalloc was a thing (I've been out of the C# loop since 2011); cool!!

Sorry if this is a stupid question, but: can you alloc multiple points in a single contiguous block (ie like an array of structs in C)? This is something that's missing from Haxe and it's pretty annoying. :/
November 13, 2025 at 9:21 PM
IMO older C# - eg like 4.x era - was much more limited and people just do not realize what it's capable of now. Both in terms of perf (stackalloc, InlineArray, ref, etc) and in terms of dynamic/untyped code (eg 'dynamic'/etc). Even Python interop...so good now.
November 13, 2025 at 9:09 PM
How do you stackalloc a class or arrays of classes in C#? The worst thing about C# for me is that I need to decide whether or not a given class/struct is a reference type or a value type when I define it instead of when I use it, even though I almost always want to do both.
November 13, 2025 at 5:37 PM
I'm such a C# shill but,

any time I hear someone say "the garbage collector slows things down too much" I feel like they're not aware of just how many tools you have to avoid heap allocations

it seems to me whatever work that entails, it's almost always less work than using an unmanaged language
November 13, 2025 at 1:21 PM
今日のZennトレンド

C#:バイナリ操作でよく使うAPIたち
この記事では、C#でバイナリ操作を行う際に役立つ様々なAPIを紹介しています。
メモリの確保と解放、エンディアンを指定した値の読み書き、Spanを使った効率的な操作、アラインメント対応、高速な連続領域コピーなどが解説されています。
また、System.IO.Pipelinesなどを用いた高パフォーマンスなI/O関連のAPIも紹介されており、効率的なバイナリ処理の実装に貢献する情報がまとめられています。
C#:バイナリ操作でよく使うAPIたち
C# でバイナリ操作を行う際によく使う API をまとめています。 確保/解放 スタック領域を確保stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。Span&lt;byte&gt; buffer = stackalloc byte[1024];スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBと
zenn.dev
May 14, 2025 at 9:16 PM
C#:バイナリ操作でよく使うAPIたち

C# でバイナリ操作を行う際によく使う API をまとめています。 確保/解放 スタック領域を確保 stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。 Span buffer = stackalloc byte; スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBとかになります。stackalloc での確保量が大きいと、メソッド呼び出しの深さなどによってはスタックオーバーフローを起こす可能性があるので注意が必要です。…
C#:バイナリ操作でよく使うAPIたち
C# でバイナリ操作を行う際によく使う API をまとめています。 確保/解放 スタック領域を確保 stackalloc でスタック領域にバッファを確保します。この確保・解放コストは非常に低く抑えられます。 Span&lt;byte&gt; buffer = stackalloc byte; スタック領域の大きさは実行環境や実装によりますが、例えば Windows ではスレッドあたり1MBとかになります。stackalloc での確保量が大きいと、メソッド呼び出しの深さなどによってはスタックオーバーフローを起こす可能性があるので注意が必要です。 配列プールからの確... Source link
inmobilexion.com
May 14, 2025 at 1:03 PM
Fields are the most direct form of memory storage. There’s zero indirection. Properties are methods.

C# lets you do low-level operations with fields that are hard or impossible with properties: e.g., ref locals and ref returns, stackalloc scenarios, custom memory layout (StructLayout), pinning, etc
May 5, 2025 at 1:29 PM
Stackalloc, span, várias coisas com ref, são exemplos dessas. Se você olhar um código de aplicações de negócios, não vê elas sendo usadas.
Mas no código do CLR, por exemplo, tá cheio. No aspnet core, também. No EF Core... E por aí vai.
April 11, 2025 at 1:48 PM
what's the equivalent C# code for this? you'd have to use stackalloc, no? does it have a simple way to repeat a sequence of user-defined values on the stack?
April 11, 2025 at 6:54 AM