Gestione della memoria Stub del server
Introduzione alla gestione della memoria Server-Stub
Gli stub generati da MIDL fungono da interfaccia tra un processo client e un processo server. Uno stub client effettua il marshalling di tutti i dati passati ai parametri contrassegnati con l'attributo [in] e lo invia allo stub del server. Lo stub del server, dopo aver ricevuto questi dati, ricostruisce lo stack di chiamate e quindi esegue la funzione server implementata dall'utente corrispondente. Lo stub del server esegue anche il marshalling dei dati dei parametri contrassegnati con l'attributo [out] e lo restituisce all'applicazione client.
Il formato di dati con marshalling a 32 bit usato da MSRPC è una versione conforme della sintassi di trasferimento NDR (Network Data Representation). Per altre informazioni su questo formato, vedere sito Web Open Group. Per le piattaforme a 64 bit, è possibile usare una sintassi di trasferimento NDR a 64 bit di Microsoft denominata NDR64 per migliorare le prestazioni.
Annullamento dell'associazione dei dati in ingresso
In MSRPC il client esegue il marshalling di tutti i dati dei parametri contrassegnati come [in] in un buffer continuo per la trasmissione allo stub del server. Analogamente, lo stub del server esegue il marshalling di tutti i dati contrassegnati con l'attributo [out] in un buffer continuo per tornare allo stub del client. Mentre il livello del protocollo di rete sottostante RPC può frammentare e creare pacchetti del buffer per la trasmissione, la frammentazione è trasparente per gli stub RPC.
L'allocazione di memoria per la creazione del frame di chiamata del server può essere un'operazione costosa. Lo stub del server tenterà di ridurre al minimo l'utilizzo della memoria non necessario quando possibile e si presuppone che la routine del server non rilascerà o riallocherà i dati contrassegnati con l'[in] o [in, out] attributi. Lo stub del server tenta di riutilizzare i dati nel buffer quando possibile per evitare la duplicazione non necessaria. La regola generale è che se il formato dei dati con marshalling è uguale al formato di memoria, RPC userà puntatori ai dati con marshalling anziché allocare memoria aggiuntiva per i dati formattati in modo identico.
Ad esempio, la chiamata RPC seguente viene definita con una struttura il cui formato di marshalling è identico al formato in memoria.
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
In questo caso, RPC non alloca memoria aggiuntiva per i dati a cui fa riferimento plInStructure; piuttosto, passa semplicemente il puntatore ai dati di marshalling all'implementazione della funzione lato server. Lo stub del server RPC verifica il buffer durante il processo di annullamento delmarshaling se lo stub viene compilato usando il flag "affidabile", ovvero un'impostazione predefinita nella versione più recente del compilatore MIDL. RPC garantisce che i dati passati all'implementazione della funzione lato server siano validi.
Tenere presente che la memoria viene allocata per plOutStructure, poiché non vengono passati dati al server.
Allocazione di memoria per i dati in ingresso
I casi possono verificarsi quando lo stub del server alloca memoria per i dati dei parametri contrassegnati con gli attributio [in, out]. Ciò si verifica quando il formato dei dati di marshalling è diverso dal formato di memoria o quando le strutture che costituiscono i dati di marshalling sono sufficienti e devono essere lette in modo atomico dallo stub del server RPC. Di seguito sono elencati diversi casi comuni in cui la memoria deve essere allocata per i dati ricevuti dallo stub del server.
I dati sono una matrice variabile o una matrice variabile conforme. Si tratta di matrici (o puntatori a matrici) con l'attributo [length_is()] o [first_is()]. In NDR, solo il primo elemento di queste matrici viene sottoposto a marshalling e trasmesso. Nel frammento di codice seguente, ad esempio, i dati passati nel parametro pv avranno memoria allocata.
void RpcFunction ( [in] long size, [in, out] long *pLength, [in, out, size_is(size), length_is(*pLength)] long *pv );
I dati sono una stringa di dimensioni o una stringa non conforme. Queste stringhe sono in genere puntatori ai dati di tipo carattere contrassegnati con l'attributo [size_is()]. Nell'esempio seguente, la stringa passata alla funzione SizedString lato server avrà memoria allocata, mentre la stringa passata alla funzione NormalString verrà riutilizzata.
void SizedString ( [in] long size, [in, size_is(size), string] char *str ); void NormalString ( [in, string] char str );
I dati sono un tipo semplice le cui dimensioni della memoria differiscono dalle dimensioni di marshalling, ad esempio enum16 e __int3264.
I dati sono definiti da una struttura il cui allineamento della memoria è inferiore all'allineamento naturale, contiene uno dei tipi di dati precedenti o ha riempimento dei byte finali. Ad esempio, la struttura di dati complessa seguente ha forzato l'allineamento a 2 byte e ha riempimento alla fine.
#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; l'allineamento viene forzato al secondo carattere di byte c2; ci sarà un riquadro finale a un byte per mantenere l'allineamento a 2 byte } '''
- I dati contengono una struttura che deve essere sottoposto a marshalling per campo. Questi campi includono puntatori di interfaccia definiti nelle interfacce DCOM; puntatori ignorati; valori integer impostati con l'attributo [intervallo]; elementi di matrici definite con [wire_marshal], [user_marshal], [transmit_as] e [represent_as]; e strutture di dati complesse incorporate.
- I dati contengono un'unione, una struttura contenente un'unione o una matrice di unioni. Solo il ramo specifico dell'unione viene sottoposto a marshalling sul filo.
- I dati contengono una struttura con una matrice conforme multidimensionale con almeno una dimensione non fissa.
- I dati contengono una matrice di strutture complesse.
- I dati contengono una matrice di tipi di dati semplici, ad esempio enum16 e __int3264.
- I dati contengono una matrice di puntatori di riferimento e interfaccia.
- I dati hanno un attributo [force_allocate] applicato a un puntatore.
- I dati hanno un [allocate(all_nodes)] attributo applicato a un puntatore.
- I dati hanno un attributo [byte_count] applicato a un puntatore.
Sintassi di trasferimento dei dati a 64 bit e NDR64
Come accennato in precedenza, il marshalling dei dati a 64 bit viene eseguito usando una sintassi di trasferimento a 64 bit specifica denominata NDR64. Questa sintassi di trasferimento è stata sviluppata per risolvere il problema specifico che si verifica quando i puntatori vengono sottoposti a marshalling in NDR a 32 bit e trasmessi a uno stub server in una piattaforma a 64 bit. In questo caso, un puntatore dati a 32 bit con marshalling non corrisponde a un puntatore a 64 bit e l'allocazione di memoria si verificherà invariabilmente. Per creare un comportamento più coerente sulle piattaforme a 64 bit, Microsoft ha sviluppato una nuova sintassi di trasferimento denominata NDR64.
Un esempio che illustra questo problema è il seguente:
typedef struct PtrStruct
{
long l;
long *pl;
}
Questa struttura, in caso di marshalling, verrà riutilizzata dallo stub del server in un sistema a 32 bit. Tuttavia, se lo stub del server risiede in un sistema a 64 bit, i dati con marshalling NDR sono di 4 byte, ma le dimensioni di memoria necessarie saranno 8. Di conseguenza, l'allocazione di memoria è forzata e il riutilizzo del buffer si verificherà raramente. NDR64 risolve questo problema rendendo le dimensioni di marshalling di un puntatore a 64 bit.
A differenza di NDR a 32 bit, i tipi di dati semplici, ad esempio enum16 e __int3264 non rendono una struttura o una matrice complessa in NDR64. Analogamente, i valori finali del riquadro non rendono complessa una struttura. I puntatori di interfaccia vengono considerati puntatori univoci al livello superiore; di conseguenza, le strutture e le matrici contenenti puntatori di interfaccia non sono considerate complesse e non richiedono un'allocazione di memoria specifica per il loro uso.
Inizializzazione dei dati in uscita
Dopo che tutti i dati in ingresso sono stati annullati, lo stub del server deve inizializzare i puntatori solo in uscita contrassegnati con l'attributo [out].
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
Nella chiamata precedente, lo stub del server deve inizializzare plOutStructure perché non era presente nei dati di marshalling ed è un puntatore implicito [ref] che deve essere reso disponibile per l'implementazione della funzione server. Lo stub del server RPC inizializza e zero tutti i puntatori di riferimento di primo livello con l'attributo [out]. Qualsiasi [out] puntatori di riferimento sotto di esso vengono inizializzati in modo ricorsivo. La ricorsione si arresta in qualsiasi puntatore con gli attributi [unique] o [ptr] impostati su di essi.
L'implementazione della funzione server non può modificare direttamente i valori del puntatore di primo livello e pertanto non può riallocarli. Ad esempio, nell'implementazione di ProcessRpcStructure precedente, il codice seguente non è valido:
void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
Process(plOutStructure);
}
plOutStructure è un valore dello stack e la modifica non viene propagata nuovamente a RPC. L'implementazione della funzione server può tentare di evitare l'allocazione tentando di liberare plOutStructure, che può causare un danneggiamento della memoria. Lo stub del server allocherà quindi lo spazio per il puntatore di primo livello in memoria (nel caso puntatore a puntatore) e una struttura semplice di primo livello la cui dimensione nello stack è inferiore al previsto.
Il client può, in determinate circostanze, specificare le dimensioni di allocazione della memoria del lato server. Nell'esempio seguente il client specifica le dimensioni dei dati in uscita nel parametro dimensione in ingresso.
void VariableSizeData
(
[in] long size,
[out, size_is(size)] char *pv
);
Dopo aver scollegato i dati in ingresso, incluse le dimensioni , lo stub del server alloca un buffer per pv con dimensioni "sizeof(char)*". Dopo l'allocazione dello spazio, lo stub del server esce dal buffer. Si noti che in questo caso specifico, lo stub alloca la memoria con MIDL_user_allocate(), poiché le dimensioni del buffer vengono determinate in fase di esecuzione.
Tenere presente che nel caso di interfacce DCOM, gli stub generati da MIDL potrebbero non essere coinvolti affatto se il client e il server condividono lo stesso apartment COM o se viene implementato ICallFrame. In questo caso, il server non può dipendere dal comportamento di allocazione e deve verificare in modo indipendente la memoria ridimensionata dal client.
Implementazioni di funzioni lato server e marshalling dei dati in uscita
Immediatamente dopo l'annullamento delmarshalling sui dati in ingresso e l'inizializzazione della memoria allocata per contenere dati in uscita, lo stub del server RPC esegue l'implementazione lato server della funzione chiamata dal client. Al momento, il server può modificare i dati contrassegnati in modo specifico con l'attributo [in, out] e può popolare la memoria allocata per i dati solo in uscita (i dati contrassegnati con [out]).
Le regole generali per la manipolazione dei dati dei parametri con marshalling sono semplici: il server può allocare solo nuova memoria o modificare la memoria allocata specificamente dallo stub del server. La riallocazione o il rilascio di memoria esistente per i dati possono avere un impatto negativo sui risultati e sulle prestazioni della chiamata di funzione e possono essere molto difficili da eseguire per il debug.
Logicamente, il server RPC si trova in uno spazio indirizzi diverso rispetto al client e in genere può essere considerato che non condividono memoria. Di conseguenza, è sicuro che l'implementazione della funzione server usi i dati contrassegnati con l'attributo [in] come memoria "scratch" senza influire sugli indirizzi di memoria client. Detto questo, il server non deve tentare di riallocare o rilasciare [in] dati, lasciando il controllo di tali spazi allo stub del server RPC stesso.
In genere, l'implementazione della funzione server non deve riallocare o rilasciare i dati contrassegnati con l'attributo [in, out]. Per i dati a dimensione fissa, la logica di implementazione della funzione può modificare direttamente i dati. Analogamente, per i dati di dimensioni variabili, l'implementazione della funzione non deve modificare il valore del campo fornito all'attributo [size_is()]. Modificare il valore del campo usato per ridimensionare i dati in un buffer più piccolo o maggiore restituito al client che potrebbe non essere dotato di problemi per gestire la lunghezza anomala.
Se si verificano circostanze in cui la routine del server deve riallocare la memoria utilizzata dai dati contrassegnati con l'attributo [in, out], è del tutto possibile che l'implementazione della funzione lato server non saprà se il puntatore fornito dallo stub è allocato con MIDL_user_allocate() o il buffer di collegamento con marshalling. Per risolvere questo problema, MS RPC può garantire che non si verifichi alcuna perdita di memoria o danneggiamento se l'attributo[force_allocate]è impostato sui dati. Quando si imposta [force_allocate], lo stub del server allocherà sempre la memoria per il puntatore, anche se le prestazioni diminuiranno per ogni uso.
Quando la chiamata viene restituita dall'implementazione della funzione lato server, lo stub del server effettua il marshalling dei dati contrassegnati con l'attributo [out] e lo invia al client. Tenere presente che lo stub non effettua il marshalling dei dati se l'implementazione della funzione lato server genera un'eccezione.
Rilascio della memoria allocata
Lo stub del server RPC rilascia la memoria dello stack dopo che la chiamata è stata restituita dalla funzione lato server, indipendentemente dal fatto che si verifichi o meno un'eccezione. Lo stub del server libera tutta la memoria allocata dallo stub e qualsiasi memoria allocata con MIDL_user_allocate(). L'implementazione della funzione lato server deve sempre fornire uno stato coerente RPC, generando un'eccezione o restituendo un codice di errore. Se la funzione non riesce durante il popolamento di strutture di dati complesse, deve assicurarsi che tutti i puntatori puntino a dati validi o siano impostati su NULL.
Durante questo passaggio, lo stub del server libera tutta la memoria che non fa parte del buffer di marshalling contenente i dati [in]. Un'eccezione a questo comportamento è data con il [allocate(dont_free)] attributo impostato su di essi. Lo stub del server non libera memoria associata a questi puntatori.
Dopo che lo stub del server rilascia la memoria allocata dallo stub e dall'implementazione della funzione, lo stub chiama una funzione di notifica specifica se l'attributo [notify_flag] viene specificato per dati specifici.
Marshalling di un elenco collegato su RPC - Esempio
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
);
Nell'esempio precedente, il formato di memoria per LINKEDLIST sarà identico al formato di collegamento con marshalling. Di conseguenza, lo stub del server non alloca memoria per l'intera catena di puntatori dati in pIn. Rpc riutilizza invece il buffer di collegamento per l'intero elenco collegato. Analogamente, lo stub non alloca memoria per pInOut, ma riutilizza invece il buffer di collegamento sottoposto a marshalling dal client.
Poiché la firma della funzione contiene un parametro in uscita, pOut, lo stub del server alloca memoria per contenere i dati restituiti. La memoria allocata è inizialmente zero, con pNext impostato su NULL. L'applicazione può allocare la memoria per un nuovo elenco collegato e puntare pOut,>pNext. pIn e l'elenco collegato che contiene possono essere usati come area di lavoro, ma l'applicazione non deve modificare alcun puntatore pNext.
L'applicazione può modificare liberamente il contenuto dell'elenco collegato a cui punta pInOut, ma non deve modificare nessuno dei puntatori pNext, senza che il collegamento di primo livello stesso. Se l'applicazione decide di abbreviare l'elenco collegato, non può sapere se una determinata puntatore collega un buffer interno RPC o un buffer allocato specificamente con MIDL_user_allocate(). Per risolvere questo problema, aggiungere una dichiarazione di tipo specifica per i puntatori di elenco collegati che forzano l'allocazione dell'utente, come illustrato nel codice seguente.
typedef [force_allocate] PLINKEDLIST;
Questo attributo impone allo stub del server di allocare separatamente ogni nodo dell'elenco collegato e l'applicazione può liberare la parte abbreviata dell'elenco collegato chiamando MIDL_user_free(). L'applicazione può quindi impostare in modo sicuro il puntatore pNext alla fine dell'elenco collegato appena abbreviato su NULL.