Partager via


Gestion de la mémoire stub du serveur

Présentation de la gestion de la mémoire Server-Stub

Les stubs générés par MIDL servent d’interface entre un processus client et un processus serveur. Un stub client marshale toutes les données passées aux paramètres marqués avec l’attribut [in] et les envoie au stub du serveur. Lors de la réception de ces données, le stub du serveur reconstruit la pile des appels, puis exécute la fonction serveur correspondante implémentée par l’utilisateur. Le stub serveur marshale également les données de paramètre marquées avec l’attribut [out] et les retourne à l’application cliente.

Le format de données marshalées 32 bits utilisé par MSRPC est une version conforme de la syntaxe de transfert NDR (Network Data Representation). Pour plus d’informations sur ce format, consultez Le site web Open Group. Pour les plateformes 64 bits, une extension Microsoft 64 bits vers la syntaxe de transfert NDR64 appelée NDR64 peut être utilisée pour de meilleures performances.

Démarshalation des données entrantes

Dans MSRPC, le stub client marshale toutes les données de paramètre étiquetées comme [in] dans une mémoire tampon continue pour la transmission au stub du serveur. De même, le stub serveur marshale toutes les données marquées avec l’attribut [out] dans une mémoire tampon continue pour retourner au stub client. Bien que la couche de protocole réseau située sous RPC puisse fragmenter et paqueter la mémoire tampon pour la transmission, la fragmentation est transparente pour les stubs RPC.

L’allocation de mémoire pour la création du frame d’appel de serveur peut être une opération coûteuse. Le stub serveur tente de réduire l’utilisation inutile de la mémoire lorsque cela est possible, et il est supposé que la routine du serveur ne libère pas ou ne réalloue pas les données marquées avec les attributs [in] ou [in, out]. Le stub du serveur tente de réutiliser les données dans la mémoire tampon chaque fois que possible pour éviter la duplication inutile. La règle générale est que si le format de données marshalé est le même que le format de mémoire, RPC utilise des pointeurs vers les données marshalées au lieu d’allouer de la mémoire supplémentaire pour les données au format identique.

Par exemple, l’appel RPC suivant est défini avec une structure dont le format marshalé est identique à son format en mémoire.

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

Dans ce cas, RPC n’alloue pas de mémoire supplémentaire pour les données référencées par plInStructure ; au lieu de cela, il transmet simplement le pointeur vers les données marshalées vers l’implémentation de la fonction côté serveur. Le stub du serveur RPC vérifie la mémoire tampon pendant le processus de démarshalation si le stub est compilé à l’aide de l’indicateur « -robuste » (qui est un paramètre par défaut dans la version la plus récente du compilateur MIDL). RPC garantit que les données transmises à l’implémentation de la fonction côté serveur sont valides.

N’oubliez pas que la mémoire est allouée pour plOutStructure, car aucune donnée n’est transmise au serveur pour celle-ci.

Allocation de mémoire pour les données entrantes

Des cas peuvent se produire lorsque le stub du serveur alloue de la mémoire pour les données de paramètre marquées avec les attributs [in] ou [in, out]. Cela se produit lorsque le format de données marshalé diffère du format de mémoire, ou lorsque les structures qui composent les données marshalées sont suffisamment complexes et doivent être lues de manière atomique par le stub du serveur RPC. Vous trouverez ci-dessous plusieurs cas courants où la mémoire doit être allouée pour les données reçues par le stub du serveur.

  • Les données sont un tableau variable ou un tableau variable conforme. Il s’agit de tableaux (ou de pointeurs vers des tableaux) dont l’attribut [length_is()] ou [first_is()] est défini dessus. Dans NDR, seul le premier élément de ces tableaux est marshalé et transmis. Par exemple, dans l’extrait de code ci-dessous, la mémoire est allouée aux données transmises dans le paramètre pv .

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Les données sont une chaîne de taille ou une chaîne non conforme. Ces chaînes sont généralement des pointeurs vers des données de caractères étiquetées avec l’attribut [size_is()] . Dans l’exemple ci-dessous, la chaîne passée à la fonction côté serveur SizedString aura de la mémoire allouée, tandis que la chaîne passée à la fonction NormalString sera réutilisée.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Les données sont un type simple dont la taille de mémoire diffère de sa taille marshalée, comme enum16 et __int3264.

  • Les données sont définies par une structure dont l’alignement de la mémoire est inférieur à l’alignement naturel, contient l’un des types de données ci-dessus ou possède un remplissage d’octets de fin. Par exemple, la structure de données complexe suivante a forcé l’alignement de 2 octets et a un remplissage à la fin.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; l’alignement est forcé au deuxième octet char c2 ; il y aura un pavé d’un octet de fin pour conserver l’alignement de 2 octets } ''''

  • Les données contiennent une structure qui doit être marshalée champ par champ. Ces champs incluent les pointeurs d’interface définis dans les interfaces DCOM ; pointeurs ignorés ; valeurs entières définies avec l’attribut [range] ; éléments de tableaux définis avec les attributs [wire_marshal],[user_marshal], [transmit_as] et [represent_as] ; et des structures de données complexes incorporées.
  • Les données contiennent une union, une structure contenant une union ou un tableau d’unions. Seule la branche spécifique de l’union est marshalée sur le fil.
  • Les données contiennent une structure avec un tableau conforme multidimensionnel qui a au moins une dimension non fixe.
  • Les données contiennent un tableau de structures complexes.
  • Les données contiennent un tableau de types de données simples tels que enum16 et __int3264.
  • Les données contiennent un tableau de points de référence et d’interface.
  • Un attribut [force_allocate] est appliqué aux données à un pointeur.
  • Un attribut [allocate(all_nodes)] est appliqué aux données à un pointeur.
  • Un attribut [byte_count] est appliqué aux données à un pointeur.

Syntaxe de transfert de données 64 bits et NDR64

Comme mentionné précédemment, les données 64 bits sont marshalées à l’aide d’une syntaxe de transfert 64 bits spécifique appelée NDR64. Cette syntaxe de transfert a été développée pour résoudre le problème spécifique qui se pose lorsque les pointeurs sont marshalés sous 32 bits NDR et transmis à un stub de serveur sur une plateforme 64 bits. Dans ce cas, un pointeur de données 32 bits marshalé ne correspond pas à un pointeur 64 bits, et l’allocation de mémoire se produit invariablement. Pour créer un comportement plus cohérent sur les plateformes 64 bits, Microsoft a développé une nouvelle syntaxe de transfert appelée NDR64.

Voici un exemple illustrant ce problème :

typedef struct PtrStruct
{
  long l;
  long *pl;
}

Cette structure, lorsqu’elle est marshalée, est réutilisée par le stub du serveur sur un système 32 bits. Toutefois, si le stub du serveur réside sur un système 64 bits, les données marshalées par remise sont de 4 octets, mais la taille de mémoire requise est de 8. Par conséquent, l’allocation de mémoire est forcée et la réutilisation de la mémoire tampon se produit rarement. NDR64 résout ce problème en rendant la taille marshalée d’un pointeur 64 bits.

Contrairement à la NDR 32 bits, les tys de données simples telles qu’enum16 et __int3264 ne complexifient pas une structure ou un tableau sous NDR64. De même, les valeurs de panneau de fin ne complexifient pas une structure. Les pointeurs d’interface sont traités comme des pointeurs uniques au niveau supérieur ; par conséquent, les structures et les tableaux contenant des pointeurs d’interface ne sont pas considérés comme complexes et ne nécessitent pas d’allocation de mémoire spécifique pour leur utilisation.

Initialisation des données sortantes

Une fois que toutes les données entrantes ont été démarshallées, le stub du serveur doit initialiser les pointeurs sortants uniquement marqués avec l’attribut [out].

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

Dans l’appel ci-dessus, le stub du serveur doit initialiser plOutStructure , car il n’était pas présent dans les données marshalées, et il s’agit d’un pointeur [ref] implicite qui doit être mis à la disposition de l’implémentation de la fonction serveur. Le stub du serveur RPC initialise et supprime tous les pointeurs de référence de niveau supérieur avec l’attribut [out]. Tous les pointeurs de référence [out] en dessous sont également initialisés de manière récursive. La récursion s’arrête à n’importe quel pointeur avec les attributs [unique] ou [ptr] définis dessus.

L’implémentation de la fonction serveur ne peut pas modifier directement les valeurs de pointeur de niveau supérieur et ne peut donc pas les réallouer. Par exemple, dans l’implémentation de ProcessRpcStructure ci-dessus, le code suivant n’est pas valide :

void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
    plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
    Process(plOutStructure);
}

plOutStructure est une valeur de pile et sa modification n’est pas propagée au RPC. L’implémentation de la fonction serveur peut tenter d’éviter l’allocation en tentant de libérer plOutStructure, ce qui peut entraîner une altération de la mémoire. Le stub du serveur alloue ensuite de l’espace pour le pointeur de niveau supérieur en mémoire (dans le cas du pointeur à pointeur) et une structure simple de niveau supérieur dont la taille sur la pile est plus petite que prévu.

Le client peut, dans certaines circonstances, spécifier la taille d’allocation de mémoire côté serveur. Dans l’exemple suivant, le client spécifie la taille des données sortantes dans le paramètre de taille entrante.

void VariableSizeData
(
    [in] long size,
    [out, size_is(size)] char *pv
);

Après avoir démarshallé les données entrantes, y compris la taille, le stub du serveur alloue une mémoire tampon pour pv avec une taille de « sizeof(char)*size ». Une fois l’espace alloué, le stub du serveur vide la mémoire tampon. Notez que dans ce cas particulier, le stub alloue la mémoire avec MIDL_user_allocate(), car la taille de la mémoire tampon est déterminée au moment de l’exécution.

N’oubliez pas que dans le cas des interfaces DCOM, les stubs générés par MIDL peuvent ne pas être impliqués du tout si le client et le serveur partagent le même appartement COM ou si ICallFrame est implémenté. Dans ce cas, le serveur ne peut pas dépendre du comportement d’allocation et doit vérifier indépendamment la mémoire de taille client.

Implémentations de fonctions côté serveur et marshaling des données sortantes

Immédiatement après le démarshalling sur les données entrantes et l’initialisation de la mémoire allouée pour contenir les données sortantes, le stub du serveur RPC exécute l’implémentation côté serveur de la fonction appelée par le client. À ce stade, le serveur peut modifier les données spécifiquement marquées avec l’attribut [in, out] et il peut remplir la mémoire allouée pour les données sortantes uniquement (les données étiquetées avec [out]).

Les règles générales pour la manipulation des données de paramètres marshalées sont simples : le serveur peut uniquement allouer une nouvelle mémoire ou modifier la mémoire spécifiquement allouée par le stub du serveur. La réaffectation ou la libération de la mémoire existante pour les données peut avoir un impact négatif sur les résultats et les performances de l’appel de fonction, et peut être très difficile à déboguer.

Logiquement, le serveur RPC vit dans un espace d’adressage différent de celui du client, et on peut généralement supposer qu’il ne partage pas de mémoire. Par conséquent, il est sécurisé pour l’implémentation de la fonction serveur d’utiliser les données marquées avec l’attribut [in] comme mémoire « scratch » sans affecter les adresses mémoire client. Cela dit, le serveur ne doit pas tenter de réallouer ou de libérer [dans] les données, laissant le contrôle de ces espaces au stub du serveur RPC lui-même.

En règle générale, l’implémentation de la fonction serveur n’a pas besoin de réallouer ou de libérer des données marquées avec l’attribut [in, out]. Pour les données de taille fixe, la logique d’implémentation de fonction peut modifier directement les données. De même, pour les données de taille variable, l’implémentation de la fonction ne doit pas non plus modifier la valeur de champ fournie à l’attribut [size_is()] . Modifier la valeur de champ utilisée pour dimensionner les données aboutit à un tampon plus petit ou plus grand renvoyé au client qui peut être mal équipé pour traiter la longueur anormale.

Si des circonstances se produisent où la routine serveur doit réallouer la mémoire utilisée par les données marquées avec l’attribut [in, out], il est tout à fait possible que l’implémentation de la fonction côté serveur ne sache pas si le pointeur fourni par le stub est vers la mémoire allouée avec MIDL_user_allocate() ou la mémoire tampon de câble marshalée. Pour contourner ce problème, MS RPC peut s’assurer qu’aucune fuite ou altération de la mémoire ne se produit si l’attribut [force_allocate] est défini sur les données. Lorsque [force_allocate] est défini, le stub du serveur alloue toujours de la mémoire pour le pointeur, même si la mise en garde est que les performances diminuent pour chaque utilisation de celui-ci.

Lorsque l’appel revient à partir de l’implémentation de la fonction côté serveur, le stub du serveur marshale les données marquées avec l’attribut [out] et les envoie au client. N’oubliez pas que le stub ne marshale pas les données si l’implémentation de la fonction côté serveur lève une exception.

Libération de la mémoire allouée

Le stub du serveur RPC libère la mémoire de la pile une fois l’appel retourné par la fonction côté serveur, qu’une exception se produise ou non. Le stub du serveur libère toute la mémoire allouée par le stub, ainsi que toute mémoire allouée avec MIDL_user_allocate(). L’implémentation de la fonction côté serveur doit toujours donner à RPC un état cohérent, soit en lisant une exception, soit en retournant un code d’erreur. Si la fonction échoue pendant la population de structures de données compliquées, elle doit s’assurer que tous les pointeurs pointent vers des données valides ou sont définis sur NULL.

Pendant cette passe, le stub du serveur libère toute la mémoire qui ne fait pas partie de la mémoire tampon marshalée contenant les données [in]. Une exception à ce comportement est les données avec l’attribut [allocation(dont_free)] défini sur celles-ci : le stub du serveur ne libère pas de mémoire associée à ces pointeurs.

Une fois que le stub du serveur libère la mémoire allouée par le stub et l’implémentation de la fonction, le stub appelle une fonction de notification spécifique si l’attribut [notify_flag] est spécifié pour des données particulières.

Marshaling a Linked List over RPC - Un exemple

typedef struct _LINKEDLIST
{
    long lSize;
    [size_is(lSize)] char *pData;
    struct _LINKEDLIST *pNext;
} LINKEDLIST, *PLINKEDLIST;

void Test
(
    [in] LINKEDLIST *pIn,
    [in, out] PLINKEDLIST *pInOut,
    [out] LINKEDLIST *pOut
);

Dans l’exemple ci-dessus, le format de mémoire pour LINKEDLIST sera identique au format filaire marshalé. Par conséquent, le stub du serveur n’alloue pas de mémoire pour l’ensemble de la chaîne de pointeurs de données sous pIn. Au lieu de cela, RPC réutilise la mémoire tampon de câble pour l’ensemble de la liste liée. De même, le stub n’alloue pas de mémoire pour pInOut, mais réutilise la mémoire tampon filaire marshalée par le client.

Étant donné que la signature de fonction contient un paramètre sortant, pOut, le stub du serveur alloue de la mémoire pour contenir les données retournées. La mémoire allouée est initialement nulle, avec pNext défini sur NULL. L’application peut allouer la mémoire pour une nouvelle liste liée et pointer pOut-pNext vers celle-ci>.pIn et la liste liée qu’il contient peuvent être utilisés comme zone de travail, mais l’application ne doit pas modifier les pointeurs pNext.

L’application peut modifier librement le contenu de la liste liée pointée par pInOut, mais elle ne doit pas modifier les pointeurs pNext , et encore moins le lien de niveau supérieur lui-même. Si l’application décide de raccourcir la liste liée, elle ne peut pas savoir si des liens de pointeur pNext donnés constituent une mémoire tampon interne RPC ou une mémoire tampon spécifiquement allouée avec MIDL_user_allocate(). Pour contourner ce problème, vous ajoutez une déclaration de type spécifique pour les pointeurs de liste liée qui force l’allocation des utilisateurs, comme indiqué dans le code ci-dessous.

typedef [force_allocate] PLINKEDLIST;

Cet attribut force le stub du serveur à allouer séparément chaque nœud de la liste liée, et l’application peut libérer la partie abrégée de la liste liée en appelant MIDL_user_free(). L’application peut ensuite définir en toute sécurité le pointeur pNext à la fin de la liste liée nouvellement raccourcie sur NULL.