Skip to content
← Tous les articles
· Delta1 Labs DecompilationGlass

Décompiler les paramètres ref readonly : à un seul attribut de in

ref readonly et in compilent vers des métadonnées de paramètre identiques octet pour octet : même flag [in], même modreq(InAttribute). La seule différence est un attribut personnalisé. Voici ce que lit Glass.NET.

Les paramètres ref readonly, ajoutés dans C# 12, semblent devoir être une grande affaire dans les métadonnées. Ils sont une nouvelle direction pour passer des arguments, une nouvelle règle sur ce qu”on a le droit de passer, et un quatrième membre de la famille par référence aux côtés de ref, out et in. Il est donc un peu surprenant que dans l”assembly compilé, un paramètre ref readonly soit identique octet pour octet à un paramètre in à un seul attribut personnalisé près. Cet attribut est tout le travail qu”un décompilateur doit faire ici, et c”est aussi exactement la pièce qu”un décompilateur qui ne lit que les signatures manquera, retransformant silencieusement chaque ref readonly en in. Ce billet parcourt les métadonnées des deux et montre ce que Glass.NET reconstruit.

Deux mots-clés, une forme binaire

Commençons par deux méthodes délibérément identiques à la direction du paramètre près :

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 existe depuis C# 7.2 ; ref readonly est le cousin plus récent et plus strict. La différence pratique dans le code source concerne ce que l”appelant peut passer. Un paramètre in accepte tout en silence — une variable, un champ, voire un littéral — et le compilateur fabrique un temporaire caché pour les cas qui ne sont pas déjà un emplacement. Un paramètre ref readonly veut un véritable emplacement et avertit quand il n”en reçoit pas, ce qui est précisément le but : il est destiné aux API qui prennent une référence pour éviter de copier une grande structure, où passer un temporaire jetable est presque toujours une erreur que l”auteur veut voir signalée. C”est une règle à la compilation, appliquée par le compilateur et signalée comme un avertissement, et — point crucial — elle ne laisse aucune trace dans la forme du paramètre.

Lire les métadonnées du paramètre

C”est là que les deux méthodes cessent de paraître différentes. Décompilez LengthIn en IL et le paramètre est une référence managée avec un flag directionnel et un modificateur requis :

.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 )
  // ...
}

Trois choses encodent le in. Le flag [in] est la direction du paramètre (ParamAttributes.In). Le modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) est un modificateur personnalisé requis soudé au type Vector3& dans la signature — modreq, pas modopt, de sorte qu”il fait partie de la signature et qu”un consommateur qui ne le comprend pas doit refuser de se lier. Et le bloc .param [1] porte un System.Runtime.CompilerServices.IsReadOnlyAttribute, la marque qui dit « cette référence est en lecture seule ».

Maintenant LengthReq, la version ref readonly :

.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 )
  // ...
}

Lisez les deux signatures côte à côte. Le type de retour est le même, le flag [in] est le même, et le modreq(InAttribute) est le même modificateur requis. La seule différence dans toute la méthode est le nom de l”attribut personnalisé dans le bloc .param : IsReadOnlyAttribute pour in, RequiresLocationAttribute pour ref readonly. C”est toute la distinction, et c”est un attribut personnalisé ordinaire — ni un modificateur, ni un flag, ni rien qui participe à la correspondance de signatures.

Comment in et ref readonly diffèrent dans les métadonnées du paramètrein vs ref readonly : octets identiques, un attribut d''écartin int xref readonly int x[in] (ParamAttributes.In)[in] (ParamAttributes.In)int32& modreq(InAttribute)int32& modreq(InAttribute).custom IsReadOnlyAttribute.custom RequiresLocationAttributemêmemêmein Vector3 vref readonly Vector3 vPourquoi la forme est partagéeUn modreq qu''un ancien compilateur ne peut analyser rend la méthode inappelable — donc ref readonly réutilisela forme de in pour la compatibilité binaire, et se marque d''un attribut personnalisé que les anciens compilateurs ignorent.Le mot-clé se reconstruit depuis cet attribut, jamais depuis le modreq, car le modreq est identique pour les deux.

Pourquoi la forme est partagée à dessein

Ce n”est pas un hasard d”implémentation ; c”est la contrainte qui définit la fonctionnalité. L”équipe du langage voulait deux choses à la fois. D”abord, remplacer un paramètre in existant dans une bibliothèque publiée par ref readonly ne doit casser aucun appelant compilé contre l”ancienne signature — c”est censé être un durcissement sûr, compatible au niveau source et binaire. Ensuite, un compilateur qui comprend ref readonly doit appliquer la règle plus stricte du « passer un emplacement ».

Un modificateur requis ne peut remplir le premier objectif. Tout le sens de modreq est « refuse de te lier si tu ne comprends pas ceci », donc un modreq(RequiresLocation) dédié ferait échouer la compilation de tout appelant antérieur à C# 12 — l”exact contraire de compatible. C”est pourquoi ref readonly réutilise l”encodage binaire existant de in, [in] plus modreq(InAttribute), que les anciens compilateurs traitent déjà comme un paramètre in ordinaire, et superpose le nouveau sens via un attribut personnalisé — RequiresLocationAttribute — que les anciens compilateurs ignorent tout simplement, comme ils ignorent tout attribut qu”ils ne reconnaissent pas. Le modreq sert à la compatibilité ; l”attribut sert à la nouvelle sémantique. C”est cette division qui fait que le bon mot-clé ne peut être récupéré que depuis l”attribut.

D”où vient RequiresLocationAttribute

Si vous cherchez System.Runtime.CompilerServices.RequiresLocationAttribute et qu”il n”est pas dans le framework que vous avez ciblé, c”est attendu — le compilateur le génère. Lorsque le framework cible ne définit pas le type, Roslyn intègre une copie privée directement dans l”assembly en cours de construction, marquée Microsoft.CodeAnalysis.EmbeddedAttribute et CompilerGenerated, exactement comme il le fait pour IsReadOnlyAttribute, IsExternalInit et les attributs de contexte de nullabilité lorsqu”ils sont absents. Glass.NET reconnaît les types d”attributs intégrés pour ce qu”ils sont — de la plomberie du compilateur — et utilise RequiresLocationAttribute pour reconstruire ref readonly sans faire apparaître le type intégré lui-même comme du code que vous auriez écrit.

Le site d”appel est identique lui aussi

Il est tentant d”espérer que l”appel lève l”ambiguïté du paramètre, mais ce n”est pas le cas. in comme ref readonly sont passés par référence en lecture seule, donc le compilateur prend une adresse et appelle :

    // Geometry.LengthReq(in v);   — v est une variable locale
    ldloca.s   v
    call       float64 Geometry::LengthReq(valuetype Vector3& modreq([System.Runtime]System.Runtime.InteropServices.InAttribute))

C”est le même ldloca + call qu”un appel in produit, et le type du paramètre dans le token de l”appel est le même Vector3& modreq(InAttribute). Si vous passez une rvalue à la méthode ref readonly — ce que le compilateur accepte avec un avertissement tel que CS9193 (« l”argument devrait être une variable ») — il déverse simplement la valeur dans un temporaire en lecture seule d”abord et prend son adresse ; c”est toujours une adresse, toujours la même forme d”appel. Rien au site d”appel n”enregistre si la méthode appelée a déclaré in ou ref readonly. Glass.NET restitue donc le mot-clé de chaque argument d”après le paramètre auquel il se lie dans la définition de la méthode appelée, et non à partir de quoi que ce soit estampillé sur l”appel.

Ce qu”un décompilateur qui ne regarde que la signature fait de travers

Un décompilateur qui reconstruit les paramètres à partir de la seule signature — le flag [in] et le modreq(InAttribute) — n”a aucun moyen de distinguer les deux, car ils sont identiques. La sortie honnête de cette approche est de restituer tous ces paramètres comme in, ce qui est faux pour chaque ref readonly de l”assembly. C”est un genre d”erreur silencieux : le code compile encore et s”exécute encore, donc rien ne la signale, mais l”API récupérée revendique désormais un contrat plus lâche que celui que l”auteur a écrit, et quiconque la lit pour comprendre l”intention de la bibliothèque — pourquoi cette méthode prend-elle une référence, et un temporaire la dérange-t-il ? — est induit en erreur.

Glass.NET lit les attributs personnalisés de .param et en déduit la direction : RequiresLocationAttribute donne ref readonly, IsReadOnlyAttribute donne in, et un modreq(InAttribute) nu sans aucun des deux attributs — ce que peuvent produire de l”IL écrit à la main ou un autre langage — retombe sur in, le sens natif de la forme. Le résultat est le code source dont vous êtes parti :

public static double LengthIn(in Vector3 v) => /* ... */;
public static double LengthReq(ref readonly Vector3 v) => /* ... */;

Vous pouvez le confirmer des deux manières dans Glass : la vue C# décompilée montre ref readonly sur le paramètre, et la vue IL à côté montre le RequiresLocationAttribute dans le bloc .param [1] qui l”a produit, avec le modreq(InAttribute) partagé juste là sur la signature pour que vous voyiez que le mot-clé est bien venu de l”attribut et de nulle part ailleurs.

À retenir

ref readonly est l”une de ces fonctionnalités où l”histoire intéressante des métadonnées est le peu de métadonnées qu”il y a. Le paramètre est encodé comme un in ordinaire — flag [in], modreq(System.Runtime.InteropServices.InAttribute) — précisément pour que les compilateurs plus anciens continuent de s”y lier, et toute la distinction repose sur un unique attribut personnalisé, RequiresLocationAttribute, que le compilateur doit souvent intégrer dans l”assembly lui-même. Lisez l”attribut et vous récupérez ref readonly ; lisez seulement la signature et vous obtenez in pour tout, d”apparence correcte et subtilement faux. Glass.NET lit l”attribut, ne retombe sur in que lorsqu”il n”y a vraiment rien d”autre sur quoi s”appuyer, et vous montre l”IL à côté pour que vous voyiez de quelle moitié des métadonnées est venu le mot-clé.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.