Dans cet article, les bases du périphérique de type interne seront données, ainsi qu'un exemple dans lequel la mémoire pour le type de référence sera allouée complètement sur la pile (c'est parce que je suis un programmeur à pile complète).

Clause de non-responsabilité
Cet article ne contient pas de matériel qui devrait être utilisé dans des projets réels. Il s'agit simplement d'une extension des frontières dans lesquelles un langage de programmation est perçu.
Avant de commencer l'histoire, je vous recommande fortement de lire le premier post sur
StructLayout , car là, un exemple est analysé qui sera utilisé dans cet article (Cependant, comme toujours).
Contexte
En commençant à écrire le code de cet article, je voulais faire quelque chose d'intéressant en utilisant le langage d'assemblage. Je voulais en quelque sorte casser le modèle d'exécution standard et obtenir un résultat vraiment inhabituel. Et en me souvenant de la fréquence à laquelle les gens disent que le type de référence diffère du type significatif en ce que le premier est situé sur le tas et le second sur la pile, j'ai décidé d'utiliser l'assembleur pour montrer que le type de référence peut vivre sur la pile. Cependant, j’ai commencé à rencontrer toutes sortes de problèmes, par exemple en renvoyant l’adresse souhaitée et en la représentant comme un lien géré (je travaille toujours dessus). J'ai donc commencé à tromper et à faire ce qui ne fonctionne pas en assembleur, en C #. Et au final, l'assembleur n'est pas resté du tout.
Aussi, une recommandation pour la lecture - si vous êtes familier avec l'appareil des types de référence, je vous recommande de sauter la théorie à leur sujet (seules les bases seront données, rien d'intéressant).
Un peu sur la structure interne des types
Je voudrais vous rappeler que la séparation de la mémoire sur la pile et le tas se produit au niveau .NET, et cette division est purement logique, physiquement, il n'y a pas de différence entre les zones de mémoire sous le tas et sous la pile. La différence de productivité est déjà fournie spécifiquement en travaillant avec ces domaines.
Comment alors allouer de la mémoire sur la pile? Pour commencer, voyons comment ce mystérieux type de référence est structuré et ce qu'il contient, qui n'est pas significatif.
Considérez donc l'exemple le plus simple avec la classe Employee.
Code employépublic class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } }
Et regardez comment il est présenté en mémoire.
UPD: Cette classe est considérée sur l'exemple d'un système 32 bits.

Ainsi, en plus de la mémoire des champs, nous avons deux autres champs cachés - l'index du bloc de synchronisation (le mot de titre de l'objet dans l'image) et l'adresse de la table de méthode.
Le premier champ, c'est l'index du bloc de synchronisation, nous ne sommes pas particulièrement intéressés. Lors du placement du type, j'ai décidé de l'omettre. Je l'ai fait pour deux raisons:
- Je suis très paresseux (je n'ai pas dit que les raisons seraient raisonnables)
- Ce champ est facultatif pour le fonctionnement de base de l'objet.
Mais comme nous avons déjà parlé, je pense qu'il est juste de dire quelques mots sur ce domaine. Il est utilisé à différentes fins (code de hachage, synchronisation). Au contraire, le champ lui-même est simplement l'index de l'un des blocs de synchronisation associés à cet objet. Les blocs eux-mêmes sont situés dans le tableau des blocs de synchronisation (à la manière d'un tableau global). La création d'un tel bloc est une opération assez importante, donc il n'est pas créé s'il n'est pas nécessaire. De plus, lors de l'utilisation de verrous fins, l'identifiant du thread qui a reçu le verrou (au lieu de l'index) y sera écrit.
Le deuxième domaine est beaucoup plus important pour nous. Grâce à la table des méthodes de type, un outil aussi puissant que le polymorphisme est possible (qui, incidemment, n'est pas possédé par la structure, les rois de la pile). Supposons que la classe Employee implémente en outre trois interfaces: IComparable, IDisposable et ICloneable.
Ensuite, la table de méthodes ressemblera à ceci

L'image est très cool, là, en principe, tout est peint et compréhensible. Si elle est courte sur les doigts, la méthode virtuelle est appelée non pas directement à l'adresse, mais par le décalage dans la table des méthodes. Dans la hiérarchie, les mêmes méthodes virtuelles seront situées au même décalage dans la table des méthodes. Autrement dit, nous appelons la méthode sur la classe de base à l'offset, sans savoir quel type de table de méthodes sera utilisé, mais sachant qu'à cet offset, il y aura la méthode la plus pertinente pour le type d'exécution.
Il convient également de rappeler que la référence à l'objet pointe vers la table des méthodes.
L'exemple tant attendu
Commençons par des cours qui nous aideront dans notre objectif. En utilisant StructLayout (j'ai vraiment essayé sans, mais cela n'a pas fonctionné), j'ai écrit les mappeurs de pointeurs les plus simples pour les types gérés et vice versa. Il est assez facile d'obtenir un pointeur à partir d'un lien géré, mais la transformation inverse m'a causé des difficultés et sans y réfléchir à deux fois, j'ai appliqué mon attribut préféré. Pour garder le code dans une clé, je l'ai fait dans 2 directions dans un sens.
Code ici // public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; }
Tout d'abord, écrivez une méthode qui prend un pointeur vers une certaine mémoire (pas nécessairement sur la pile, soit dit en passant) et configure le type.
Pour faciliter la recherche de l'adresse de la table des méthodes, je crée un type sur le tas. Je suis sûr que la table des méthodes peut être trouvée autrement, mais je ne me suis pas fixé pour objectif d'optimiser ce code, il était plus intéressant pour moi de le rendre compréhensible. Ensuite, en utilisant les convertisseurs décrits précédemment, nous obtenons un pointeur sur le type créé.
Ce pointeur pointe exactement vers la table des méthodes. Par conséquent, il suffit d'obtenir simplement le contenu de la mémoire vers laquelle il pointe. Ce sera l'adresse de la table des méthodes.
Et puisque le pointeur qui nous est transmis est une sorte de référence à l'objet, nous devons écrire l'adresse de la table de méthode exactement là où elle pointe.
C'est tout, en fait. De façon inattendue, non? Maintenant, notre type est prêt. Pinocchio, qui nous a attribué la mémoire, se chargera de l'initialisation des champs.
Il ne reste plus qu'à utiliser le grand diffuseur pour convertir le pointeur en un lien géré.
public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } }
Maintenant, nous avons un lien sur la pile qui pointe vers la même pile, où, selon toutes les lois des types de référence (enfin, presque) se trouve un objet construit à partir de terre noire et de bâtons. Le polymorphisme est disponible.
Il faut comprendre que si vous passez ce lien en dehors de la méthode, puis après son retour, nous obtiendrons quelque chose de peu clair. On ne peut pas parler d'appels à des méthodes virtuelles; volons par exception. Les méthodes régulières sont appelées directement, dans le code, il y aura simplement des adresses pour les méthodes réelles, donc elles fonctionneront. Et à la place des champs il y aura ... mais personne ne sait ce qu'il y aura.
Puisqu'il est impossible d'utiliser une méthode distincte pour l'initialisation sur la pile (puisque le cadre de pile sera effacé après son retour de la méthode), la mémoire doit être allouée par la méthode qui veut utiliser le type sur la pile. À strictement parler, il n'y a pas une seule façon de procéder. Mais le plus approprié pour nous est stackalloc. Juste le mot-clé parfait pour nos besoins. Malheureusement, c'est elle qui a introduit l'incontrôlabilité dans le code. Avant cela, il y avait une idée d'utiliser Span à ces fins et de se passer de code dangereux. Il n'y a rien de mal à un code dangereux, mais comme partout ailleurs, ce n'est pas une solution miracle et a ses propres domaines d'application.
Ensuite, après avoir reçu un pointeur vers la mémoire de la pile actuelle, nous passons ce pointeur à une méthode qui compose le type en plusieurs parties. C'est tout ce qui a écouté - bravo.
unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } }
Vous ne devez pas utiliser cela dans des projets réels, la méthode qui alloue de la mémoire sur la pile utilise un nouveau T (), qui à son tour utilise la réflexion pour créer le type sur le tas! Cette méthode sera donc plus lente que la création habituelle du type une fois, eh bien, 40-50.
Ici vous pouvez voir l'ensemble du projet.
Source: dans une digression théorique, des exemples ont été utilisés dans le livre Sasha Goldstein - Pro .NET Performace