C# 12 ref readonly Metadata Explained

Résumé

Pixel’s announcement explores how C# 12 ref readonly parameters and in parameters share the same compiled signature, differing by a single custom attribute. It explains how that attribute preserves the distinction for decompilers like Glass.NET, while the compiler’s warning about passing temporary values leaves no trace in the parameter shape.

DEV Community

Decompiling ref readonly parameters: one custom attribute away from in

ref readonly parameters, added in C# 12, look like they should be a big deal in metadata. They are a new direction for passing arguments, a new rule about what you are allowed to pass, and a fourth member of the by-reference family alongside ref, out and in. So it is a little surprising that in the compiled assembly a ref readonly parameter is byte-for-byte identical to an in parameter except for a single custom attribute. That one attribute is the entire job a decompiler has to do here, and it is also exactly the piece a decompiler that only reads signatures will miss — quietly turning every ref readonly back into in. This post walks the metadata for both and shows what Glass.NET reconstructs. Two keywords, one binary shape Start with two methods that are deliberately the same except for the parameter direction: public static class Geometry
{
public static double LengthIn(in Vector3 v) =>
Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);

public static double LengthReq(ref readonly Vector3 v) =>
    Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);

}
in has been around since C# 7.2; ref readonly is the newer, stricter cousin. The practical difference in source is about what the caller may pass. An in parameter accepts anything silently — a variable, a field, even a literal — and the compiler conjures a hidden temporary for the cases that are not already a location. A ref readonly parameter wants an actual location and warns when it does not get one, which is the point: it is for APIs that take a reference to avoid copying a large struct, where passing a throwaway temporary is almost always a mistake the author wants flagged. That is a compile-time rule, enforced by the compiler and reported as a warning, and — crucially — it leaves no trace in the parameter's shape at all. Reading the parameter metadata Here is where the two methods stop looking different. Decompile LengthIn to IL and the parameter is a managed reference with a directional flag and a required modifier: .method public hidebysig static float64 LengthIn (
[in] valuetype Vector3& modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) v
) cil managed
{
.param [1]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 )
// ...
}
Three things encode the in. The [in] flag is the parameter's direction (ParamAttributes.In). The modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) is a required custom modifier welded onto the Vector3& type in the signature — modreq, not modopt, so it is part of the signature and a consumer that does not understand it must refuse to bind. And the .param [1] block carries a System.Runtime.CompilerServices.IsReadOnlyAttribute, the marker that says "this reference is read-only." Now LengthReq, the ref readonly version: .method public hidebysig static float64 LengthReq (
[in] valuetype Vector3& modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) v
) cil managed
{
.param [1]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.RequiresLocationAttribute::.ctor() = ( 01 00 00 00 )
// ...
}
Read the two signatures side by side. The return type is the same, the [in] flag is the same, and the modreq(InAttribute) is the same required modifier. The only difference in the entire method is the name of the custom attribute in the .param block: IsReadOnlyAttribute for in, RequiresLocationAttribute for ref readonly. That is the whole distinction, and it is a plain custom attribute — not a modifier, not a flag, not anything that participates in signature matching. Why the shape is shared on purpose This is not an accident of implementation; it is the feature's defining constraint. The language team wanted two things at once. First, changing an existing in parameter in a published library to ref readonly must not break any caller that was compiled against the old signature — it is meant to be a saf...
C# 12 ref readonly Metadata Explained
Powered by Readybot.io

Les dernières actus de Pixel

Fix GitHub Pages: Missing Artifact Error

Pixel's announcement explains why the GitHub Pages deploy action reports that no github-pages artifact was found. It covers skipped or failed build jobs, missing uploads, …

Rotate DB and Redis Passwords Without Downtime

Pixel shares a practical guide to rotating exposed database and Redis passwords without interrupting application connections. The approach updates Secrets, changes service credentials, and gradually …

AI Agent Receipts Prove What Actually Executed

Pixel’s announcement examines how public Zambo receipts verify what an AI agent actually did. A BTC price fetch succeeded with a recomputable output hash, while …