Gerenciamento de memória de stub do servidor
Uma introdução ao gerenciamento de memória Server-Stub
Os stubs gerados por MIDL atuam como a interface entre um processo de cliente e um processo de servidor. Um stub de cliente controla todos os dados passados para parâmetros marcados com o atributo [in] e os envia para o stub do servidor. O stub do servidor, ao receber esses dados, reconstrói a pilha de chamadas e, em seguida, executa a função de servidor implementada pelo usuário correspondente. O stub do servidor também organiza os dados do parâmetro marcados com o atributo [out] e os retorna ao aplicativo cliente.
O formato de dados empacotados de 32 bits usado pelo MSRPC é uma versão compatível da sintaxe de transferência de NDR (Network Data Representation). Para obter mais informações sobre esse formato, consulte site The Open Group. Para plataformas de 64 bits, uma extensão de 64 bits da Microsoft para sintaxe de transferência de NDR chamada NDR64 pode ser usada para um melhor desempenho.
Desempacotamento de dados de entrada
No MSRPC, o stub do cliente marshals todos os dados de parâmetro marcados como [in] em um buffer contínuo para transmissão para o stub do servidor. Da mesma forma, o stub do servidor controla todos os dados marcados com o atributo [out] em um buffer contínuo para retornar ao stub do cliente. Enquanto a camada de protocolo de rede abaixo do RPC pode fragmentar e empacotar o buffer para transmissão, a fragmentação é transparente para os stubs RPC.
A alocação de memória para criar o quadro de chamada do servidor pode ser uma operação cara. O stub do servidor tentará minimizar o uso desnecessário de memória quando possível, e presume-se que a rotina do servidor não liberará ou realocará dados marcados com os atributos [in] ou [in, out]. O stub do servidor tenta reutilizar dados no buffer sempre que possível para evitar duplicações desnecessárias. A regra geral é que, se o formato de dados empacotados for o mesmo que o formato de memória, o RPC usará ponteiros para os dados empacotados em vez de alocar memória adicional para dados formatados de forma idêntica.
Por exemplo, a chamada RPC a seguir é definida com uma estrutura cujo formato empacotado é idêntico ao seu formato na memória.
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
Nesse caso, o RPC não aloca memória adicional para os dados referenciados por plInStructure; em vez disso, ele simplesmente passa o ponteiro para os dados empacotados para a implementação da função do lado do servidor. O stub do servidor RPC verifica o buffer durante o processo de unmarshaling se o stub for compilado usando o sinalizador "-robust" (que é uma configuração padrão na versão mais recente do compilador MIDL). O RPC garante que os dados passados para a implementação da função do lado do servidor são válidos.
Esteja ciente de que a memória é alocada para plOutStructure, uma vez que nenhum dado é passado para o servidor para ele.
Alocação de memória para dados de entrada
Podem surgir casos em que o stub do servidor aloca memória para dados de parâmetros marcados com os atributos [in] ou [in, out]. Isso ocorre quando o formato de dados empacotados difere do formato de memória ou quando as estruturas que compõem os dados empacotados são suficientemente complexas e devem ser lidas atomicamente pelo stub do servidor RPC. Abaixo estão listados vários casos comuns em que a memória deve ser alocada para dados recebidos pelo stub do servidor.
Os dados são uma matriz variável ou uma matriz variável conforme. Estas são matrizes (ou ponteiros para matrizes) que têm a [length_is(()] ou [first_is()] atributo definido nelas. Na notificação de falha na entrega, apenas o primeiro elemento dessas matrizes é empacotado e transmitido. Por exemplo, no trecho de código abaixo, os dados passados no parâmetro pv terão memória alocada para ele.
void RpcFunction ( [in] long size, [in, out] long *pLength, [in, out, size_is(size), length_is(*pLength)] long *pv );
Os dados são uma cadeia de caracteres de tamanho ou cadeia de caracteres não conforme. Essas cadeias de caracteres geralmente são ponteiros para dados de caracteres marcados com o atributo[size_is()]. No exemplo abaixo, a cadeia de caracteres passada para o SizedString função do lado do servidor terá memória alocada, enquanto a cadeia de caracteres passada para a função NormalString será reutilizada.
void SizedString ( [in] long size, [in, size_is(size), string] char *str ); void NormalString ( [in, string] char str );
Os dados são um tipo simples cujo tamanho de memória difere do seu tamanho empacotado, como enum16 e __int3264.
Os dados são definidos por uma estrutura cujo alinhamento de memória é menor do que o alinhamento natural, contém qualquer um dos tipos de dados acima ou tem preenchimento de bytes à direita. Por exemplo, a seguinte estrutura de dados complexa forçou o alinhamento de 2 bytes e tem preenchimento no final.
#pragma pack(2) typedef struct ComplexPackedStructure { char c;
L longo; o alinhamento é forçado no segundo byte char c2; haverá um teclado de um byte à direita para manter o alinhamento de 2 bytes } '''
- Os dados contêm uma estrutura que deve ser empacotada campo por campo. Esses campos incluem ponteiros de interface definidos em interfaces DCOM; ponteiros ignorados; valores inteiros definidos com o atributo[intervalo]; elementos de matrizes definidas com o [wire_marshal], [user_marshal], [transmit_as] e [represent_as] atributos; e estruturas de dados complexas incorporadas.
- Os dados contêm uma união, uma estrutura contendo uma união ou uma matriz de sindicatos. Apenas o ramo específico do sindicato é marshaled no arame.
- Os dados contêm uma estrutura com uma matriz de conformidade multidimensional que tem pelo menos uma dimensão não fixa.
- Os dados contêm uma matriz de estruturas complexas.
- Os dados contêm uma matriz de tipos de dados simples, como enum16 e __int3264.
- Os dados contêm uma matriz de ponteiros ref e interface.
- Os dados têm um atributo [force_allocate] aplicado a um ponteiro.
- Os dados têm um atributo[alocar(all_nodes)] aplicado a um ponteiro.
- Os dados têm um atributo de[byte_count] aplicado a um ponteiro.
Dados de 64 bits e sintaxe de transferência NDR64
Como mencionado anteriormente, os dados de 64 bits são empacotados usando uma sintaxe de transferência específica de 64 bits chamada NDR64. Essa sintaxe de transferência foi desenvolvida para resolver o problema específico que surge quando os ponteiros são empacotados sob NDR de 32 bits e transmitidos para um stub de servidor em uma plataforma de 64 bits. Nesse caso, um ponteiro de dados de 32 bits empacotado não corresponde a um ponteiro de 64 bits e a alocação de memória invariavelmente ocorrerá. Para criar um comportamento mais consistente em plataformas de 64 bits, a Microsoft desenvolveu uma nova sintaxe de transferência chamada NDR64.
Um exemplo que ilustra este problema é o seguinte:
typedef struct PtrStruct
{
long l;
long *pl;
}
Essa estrutura, quando empacotada, será reutilizada pelo stub do servidor em um sistema de 32 bits. No entanto, se o stub do servidor residir em um sistema de 64 bits, os dados empacotados por NDR terão 4 bytes de comprimento, mas o tamanho de memória necessário será 8. Como resultado, a alocação de memória é forçada e a reutilização do buffer raramente ocorrerá. A NDR64 resolve esse problema tornando o tamanho empacotado de um ponteiro de 64 bits.
Em contraste com a notificação de falha na entrega de 32 bits, vinculações de dados simples, como enum16 e __int3264 não tornam uma estrutura ou matriz complexa sob NDR64. Da mesma forma, os valores do painel à direita não tornam uma estrutura complexa. Os ponteiros de interface são tratados como ponteiros exclusivos no nível superior; Como resultado, estruturas e matrizes contendo ponteiros de interface não são consideradas complexas e não exigirão alocação de memória específica para seu uso.
Inicializando dados de saída
Depois que todos os dados de entrada tiverem sido desempacotados, o stub do servidor precisará inicializar os ponteiros somente de saída marcados com o atributo[out].
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
Na chamada acima, o stub do servidor deve inicializar plOutStructure porque ele não estava presente nos dados empacotados e é um ponteiro implícito [ref] que deve ser disponibilizado para a implementação da função de servidor. O stub do servidor RPC inicializa e zera todos os ponteiros somente de referência de nível superior com o atributo[out]. Qualquer [fora] ponteiros de referência abaixo dele também são inicializados recursivamente. A recursão para em qualquer ponteiro com os atributos [unique] ou [ptr] definidos neles.
A implementação da função de servidor não pode alterar diretamente os valores de ponteiro de nível superior e, portanto, não pode realocá-los. Por exemplo, na implementação de ProcessRpcStructure acima, o código a seguir é inválido:
void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
Process(plOutStructure);
}
plOutStructure é um valor de pilha e sua alteração não é propagada de volta para RPC. A implementação da função de servidor pode tentar evitar a alocação tentando liberar plOutStructure, o que pode resultar em corrupção de memória. O stub do servidor irá então alocar espaço para o ponteiro de nível superior na memória (no caso de ponteiro para ponteiro) e uma estrutura simples de nível superior cujo tamanho na pilha é menor do que o esperado.
O cliente pode, em determinadas circunstâncias, especificar o tamanho de alocação de memória do lado do servidor. No exemplo a seguir, o cliente especifica o tamanho dos dados de saída no parâmetro de de de entrada.
void VariableSizeData
(
[in] long size,
[out, size_is(size)] char *pv
);
Depois de desempacotar os dados de entrada, incluindo tamanho, o stub do servidor aloca um buffer para pv com um tamanho de "sizeof(char)*size". Depois que o espaço for alocado, o stub do servidor zera o buffer. Observe que, neste caso específico, o stub aloca a memória com MIDL_user_allocate(), uma vez que o tamanho do buffer é determinado em tempo de execução.
Esteja ciente de que, no caso de interfaces DCOM, stubs gerados por MIDL podem não estar envolvidos se o cliente e o servidor compartilharem o mesmo apartamento COM, ou se ICallFrame for implementado. Nesse caso, o servidor não pode depender do comportamento de alocação e precisa verificar independentemente a memória do tamanho do cliente.
Implementações de função do lado do servidor e marshaling de dados de saída
Imediatamente após o desempacotamento de dados de entrada e a inicialização da memória alocada para conter dados de saída, o stub do servidor RPC executa a implementação do lado do servidor da função chamada pelo cliente. Neste momento, o servidor pode modificar os dados especificamente marcados com o atributo [in, out] e pode preencher a memória alocada para dados somente de saída (os dados marcados com [out]).
As regras gerais para a manipulação de dados de parâmetros empacotados são simples: o servidor só pode alocar nova memória ou modificar a memória especificamente alocada pelo stub do servidor. Realocar ou liberar memória existente para dados pode ter um impacto negativo nos resultados e no desempenho da chamada de função e pode ser muito difícil de depurar.
Logicamente, o servidor RPC vive em um espaço de endereçamento diferente do cliente, e geralmente pode-se supor que eles não compartilham memória. Como resultado, é seguro para a implementação da função de servidor usar os dados marcados com o atributo [in] como memória "scratch" sem afetar os endereços de memória do cliente. Dito isso, o servidor não deve tentar realocar ou liberar [em] dados, deixando o controle desses espaços para o próprio stub do servidor RPC.
Geralmente, a implementação da função de servidor não precisa realocar ou liberar dados marcados com o atributo [in, out]. Para dados de tamanho fixo, a lógica de implementação da função pode modificar diretamente os dados. Da mesma forma, para dados de tamanho variável, a implementação da função também não deve modificar o valor do campo fornecido ao atributo[size_is()]. Alterar o valor do campo usado para dimensionar os dados resulta em um buffer menor ou maior retornado ao cliente, que pode estar mal equipado para lidar com o comprimento anormal.
Se ocorrerem circunstâncias em que a rotina do servidor tenha que realocar a memória usada pelos dados marcados com o atributo [in, out], é totalmente possível que a implementação da função do lado do servidor não saiba se o ponteiro fornecido pelo stub é para a memória alocada com MIDL_user_allocate() ou o buffer de fio empacotado. Para contornar esse problema, MS RPC pode garantir que nenhum vazamento de memória ou corrupção ocorre se o [force_allocate] atributo está definido nos dados. Quando [force_allocate] é definido, o stub do servidor sempre alocará memória para o ponteiro, embora a ressalva seja que o desempenho diminuirá para cada uso dele.
Quando a chamada retorna da implementação da função do lado do servidor, o stub do servidor controla os dados marcados com o atributo [out] e os envia para o cliente. Lembre-se de que o stub não organiza os dados se a implementação da função do lado do servidor gerar uma exceção.
Liberando memória alocada
O stub do servidor RPC liberará a memória da pilha depois que a chamada for retornada da função do lado do servidor, independentemente de ocorrer uma exceção ou não. O stub do servidor libera toda a memória alocada pelo stub, bem como qualquer memória alocada com MIDL_user_allocate(). A implementação da função do lado do servidor deve sempre dar ao RPC um estado consistente, seja lançando uma exceção ou retornando um código de erro. Se a função falhar durante o preenchimento de estruturas de dados complicadas, ela deve garantir que todos os ponteiros apontem para dados válidos ou estejam definidos como NULL .
Durante essa passagem, o stub do servidor libera toda a memória que não faz parte do buffer empacotado que contém os [em] dados. Uma exceção a esse comportamento são os dados com o atributo [allocate(dont_free)] definido neles - o stub do servidor não libera nenhuma memória associada a esses ponteiros.
Depois que o stub do servidor libera a memória alocada pelo stub e a implementação da função, o stub chama uma função de notificação específica se o atributo[notify_flag] for especificado para dados específicos.
Marshalling a Linked List over RPC -- Um exemplo
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
);
No exemplo acima, o formato de memória para LINKEDLIST será idêntico ao formato de fio empacotado. Como resultado, o stub do servidor não aloca memória para toda a cadeia de ponteiros de dados sob pIn. Em vez disso, o RPC reutiliza o buffer de fio para toda a lista vinculada. Da mesma forma, o stub não aloca memória para pInOut, mas reutiliza o buffer de fio empacotado pelo cliente.
Como a assinatura de função contém um parâmetro de saída, pOut, o stub do servidor aloca memória para conter os dados retornados. A memória alocada é inicialmente zerada, com pNext definido como NULL. O aplicativo pode alocar a memória para uma nova lista vinculada e apontar pOut->pNext para ele. de pinos e a lista vinculada que ele contém podem ser usados como uma área de rascunho, mas o aplicativo não deve alterar nenhum dos ponteiros pNext.
O aplicativo pode alterar livremente o conteúdo da lista vinculada apontada por pInOut, mas não deve alterar nenhum dos ponteiros pNext, muito menos o link de nível superior em si. Se o aplicativo decidir encurtar a lista vinculada, ele não poderá saber se um determinado ponteiro pNext vincula a um buffer interno RPC ou a um buffer especificamente alocado com MIDL_user_allocate(). Para contornar esse problema, você adiciona uma declaração de tipo específico para ponteiros de lista vinculada que força a alocação do usuário, como visto no código abaixo.
typedef [force_allocate] PLINKEDLIST;
Esse atributo força o stub do servidor a alocar cada nó da lista vinculada separadamente, e o aplicativo pode liberar a parte abreviada da lista vinculada chamando MIDL_user_free(). O aplicativo pode então definir com segurança o ponteiro pNext no final da lista vinculada recém-encurtada para NULL .