Compartilhar via


Gerenciamento de memória 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 realiza marshaling de 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 realiza marshaling dos dados de parâmetro marcados com o atributo [out] e os retorna ao aplicativo cliente.

O formato de dados com marshaling de 32 bits usado pelo MSRPC é uma versão em conformidade da sintaxe de transferência NDR (Representação de Dados de Rede). Para obter mais informações sobre esse formato, consulte O site do Open Group. Para plataformas de 64 bits, uma sintaxe de transferência de 64 bits do Microsoft para NDR chamada NDR64 pode ser usada para melhorar o desempenho.

Cancelar a inalação de dados de entrada

No MSRPC, o stub do cliente realiza marshaling de 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 realiza marshaling de todos os dados marcados com o atributo [out] em um buffer contínuo para retornar ao stub do cliente. Embora a camada de protocolo de rede abaixo do RPC possa fragmentar e colocar o buffer em pacote para transmissão, a fragmentação é transparente para os stubs de 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 supõe-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ção desnecessária. A regra geral é que, se o formato de dados marshaled for o mesmo que o formato de memória, o RPC usará ponteiros para os dados com marshaling 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 marshaled é idêntico ao 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 "robusto" (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 sejam válidos.

Lembre-se de que a memória é alocada para plOutStructure, pois 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âmetro marcados com os atributos [in] ou [in, out] . Isso ocorre quando o formato de dados marshaled difere do formato de memória ou quando as estruturas que compõem os dados empacotados são complexas suficientes e devem ser lidas atomicamente pelo stub do servidor RPC. Veja abaixo vários casos comuns em que a memória deve ser alocada para os dados recebidos pelo stub do servidor.

  • Os dados são uma matriz variável ou uma matriz variável compatível. São matrizes (ou ponteiros para matrizes) que têm o atributo [length_is()] ou [first_is()] definido neles. Na NDR, somente o primeiro elemento dessas matrizes é realizado em marshaling e transmitido. Por exemplo, no snippet 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 dimensionada ou uma cadeia de caracteres não compatível. Essas cadeias de caracteres geralmente são ponteiros para dados de caractere marcados com o atributo [size_is()] . No exemplo a seguir, a cadeia de caracteres passada para a função do lado do servidor SizedString 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 tamanho de marshaling, como enum16 e __int3264.

  • Os dados são definidos por uma estrutura cujo alinhamento de memória é menor que o alinhamento natural, contém qualquer um dos tipos de dados acima ou tem preenchimento de bytes à direita. Por exemplo, a estrutura de dados complexa a seguir forçou o alinhamento de 2 bytes e tem preenchimento no final.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; alignment é forçado no segundo byte char c2; haverá um painel 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 [range] ; elementos de matrizes definidas com os atributos [wire_marshal], [user_marshal], [transmit_as] e [represent_as] ; e estruturas de dados complexas inseridas.
  • Os dados contêm uma união, uma estrutura que contém uma união ou uma matriz de uniões. Somente a ramificação específica da união é empacotada na transmissão.
  • 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 [allocate(all_nodes)] aplicado a um ponteiro.
  • Os dados têm um atributo [byte_count] aplicado a um ponteiro.

Sintaxe de transferência NDR64 e dados de 64 bits

Conforme mencionado anteriormente, os dados de 64 bits têm marshaling 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 em 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 marshaled não corresponde a um de 64 bits e a alocação de memória ocorrerá invariavelmente. 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 esse problema é o seguinte:

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

Essa estrutura, quando empacotada, será reutilizado 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 com marshaling de 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á. O NDR64 resolve esse problema fazendo com que o tamanho realizado em marshaling de um ponteiro de 64 bits.

Em contraste com a NDR de 32 bits, os blocos de dados simples, como enum16 e __int3264 , não tornam uma estrutura ou matriz complexa em 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 que contêm 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 desmarcados, 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 marshaled e é um ponteiro [ref] implícito que deve ser disponibilizado para a implementação da função de servidor. O stub do servidor RPC inicializa e zerada todos os ponteiros somente de referência de nível superior com o atributo [out] . Todos os ponteiros de referência [out] abaixo dele também são inicializados recursivamente. A recursão para em qualquer ponteiro com os atributos [exclusivo] 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. Em seguida, o stub do servidor 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 da 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 tamanho de entrada.

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

Depois de desmarcar os dados de entrada, incluindo o 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 zerá o buffer. Observe que, nesse caso específico, o stub aloca a memória com MIDL_user_allocate(), já que o tamanho do buffer é determinado em runtime.

Lembre-se de que, no caso de interfaces DCOM, os stubs gerados por MIDL poderão não estar envolvidos se o cliente e o servidor compartilharem o mesmo apartment 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 unmarshalling em 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 marcados especificamente 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âmetro marshalled são simples: o servidor só pode alocar memória nova 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 reside em um espaço de endereço 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 "rascunho" sem afetar os endereços de memória do cliente. Dito isto, o servidor não deve tentar realocar ou liberar [in] dados, deixando o controle desses espaços para o próprio stub do servidor RPC.

Em geral, 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 precisa realocar a memória usada pelos dados marcados com o atributo [in, out] , é inteiramente 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 transmissão marshaled. Para contornar esse problema, o MS RPC pode garantir que não ocorra vazamento de memória ou corrupção se o atributo [force_allocate] estiver definido nos dados. Quando [force_allocate] for 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 realiza marshaling dos dados marcados com o atributo [out] e os envia ao cliente. Lembre-se de que o stub não realizará marshaling dos 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 de pilha depois que a chamada tiver retornado da função do lado do servidor, independentemente de ocorrer ou não uma exceçã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 sempre deve 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 a população de estruturas de dados complicadas, ela deverá garantir que todos os ponteiros apontem para dados válidos ou sejam definidos como NULL.

Durante essa passagem, o stub do servidor libera toda a memória que não faz parte do buffer com marshaling que contém os dados [in] . 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 liberar a memória alocada pelo stub e pela implementação da função, o stub chamará uma função de notificação específica se o atributo [notify_flag] for especificado para dados específicos.

Realizando marshaling de uma lista vinculada por 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 transmissão marshaled. Como resultado, o stub do servidor não aloca memória para toda a cadeia de ponteiros de dados em pIn. Em vez disso, o RPC reutiliza o buffer de transmissão para toda a lista vinculada. Da mesma forma, o stub não aloca memória para pInOut, mas reutiliza o buffer de transmissão realizado 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 zerado, com pNext definido como NULL. O aplicativo pode alocar a memória para uma nova lista vinculada e apontar pOut-pNext> para ela. O pIn 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 próprio link de nível superior. Se o aplicativo decidir reduzir a lista vinculada, ele não poderá saber se um determinado ponteiro pNext vincula t a um buffer interno RPC ou a um buffer especificamente alocado com MIDL_user_allocate(). Para contornar esse problema, adicione uma declaração de tipo específica para ponteiros de lista vinculados que forçam a alocação do usuário, conforme 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(). Em seguida, o aplicativo pode definir com segurança o ponteiro pNext no final da lista vinculada recém-abreviada como NULL.