Migrando código gerenciado de 32 bits para 64 bits
Microsoft Corporation
Atualizado em maio de 2005
Aplica-se a:
Microsoft .NET
Microsoft .NET Framework 2.0
Resumo: Descubra o que está envolvido na migração de aplicativos gerenciados de 32 bits para 64 bits, problemas que podem afetar a migração e as ferramentas disponíveis para ajudá-lo. (17 páginas impressas)
Sumário
Introdução
Código gerenciado em um ambiente de 32 bits
Insira o CLR para o ambiente de 64 bits
Migração e invocação de plataforma
Migração e interoperabilidade COM
Migração e código não seguro
Migração e marshaling
Migração e serialização
Resumo
Introdução
Este white paper discute:
- O que está envolvido na migração de aplicativos gerenciados de 32 bits para 64 bits
- Os problemas que podem afetar a migração
- Quais ferramentas estão disponíveis para ajudá-lo
Essas informações não devem ser prescritivas; em vez disso, destina-se a familiarizá-lo com as diferentes áreas suscetíveis a problemas durante o processo de migração para 64 bits. Neste ponto, não há nenhum "guia" específico de etapas que você pode seguir e garantir que seu código funcionará em 64 bits. As informações contidas neste white paper o familiarizarão com os diferentes problemas e o que deve ser revisado.
Como você verá em breve, se o assembly gerenciado não for um código seguro de tipo 100%, você precisará examinar seu aplicativo e suas dependências para determinar seus problemas com a migração para 64 bits. Muitos dos itens sobre os quais você lerá nas próximas seções podem ser abordados por meio de alterações de programação. Em vários casos, você também precisará reservar tempo para atualizar seu código para ser executado corretamente em ambientes de 32 e 64 bits, se quiser que ele seja executado em ambos.
O Microsoft .NET é um conjunto de tecnologias de software para conectar informações, pessoas, sistemas e dispositivos. Desde sua versão 1.0 em 2002, as organizações conseguiram implantar o . Soluções baseadas em NET, sejam internas, por ISVs (fornecedores independentes de software) ou alguma combinação. Há vários tipos de aplicativos .NET que efetuam push dos limites do ambiente de 32 bits. Esses desafios incluem, mas não se limitam a, a necessidade de memória endereçável mais real e a necessidade de maior desempenho de ponto flutuante. X64 e Itanium oferecem melhor desempenho para operações de ponto flutuante do que você pode obter em x86. No entanto, também é possível que os resultados obtidos em x64 ou Itanium sejam diferentes dos resultados obtidos em x86. A plataforma de 64 bits tem como objetivo ajudar a resolver esses problemas.
Com o lançamento do .NET Framework versão 2.0, a Microsoft inclui suporte para código gerenciado em execução nas plataformas x64 e Itanium de 64 bits.
O código gerenciado é simplesmente "código" que fornece informações suficientes para permitir que o CLR (Common Language Runtime) do .NET forneça um conjunto de serviços principais, incluindo:
- Autodescrição de código e dados por meio de metadados
- Passagem de pilha
- Segurança
- Coleta de lixo
- Compilação Just-In-Time
Além do código gerenciado, há várias outras definições que são importantes de entender à medida que você investiga os problemas de migração.
Dados Gerenciados – dados alocados no heap gerenciado e coletados por meio da coleta de lixo.
Assembly – a unidade de implantação que permite que o CLR entenda totalmente o conteúdo de um aplicativo e imponha as regras de controle de versão e dependência definidas pelo aplicativo.
Digite código seguro — código que usa apenas dados gerenciados e nenhum tipo de dados não verificável ou operações de conversão/coerção de tipo de dados sem suporte (ou seja, uniões não discriminadas ou ponteiros de estrutura/interface). Código C#, Visual Basic .NET e Visual C++ compilados com /clr:safe geram código seguro de tipo.
Código não seguro – código que tem permissão para executar operações de nível inferior como declarar e operar em ponteiros, executar conversões entre ponteiros e tipos integrais e usar o endereço de variáveis. Essas operações permitem a interfacção com o sistema operacional subjacente, o acesso a um dispositivo mapeado na memória ou a implementação de um algoritmo crítico de tempo. O código nativo não é seguro.
Código gerenciado em um ambiente de 32 bits
Para entender as complexidades envolvidas na migração de código gerenciado para o ambiente de 64 bits, vamos examinar como o código gerenciado é executado em um ambiente de 32 bits.
Quando um aplicativo, gerenciado ou não gerenciado, é selecionado para ser executado, o carregador do Windows é invocado e é responsável por decidir como carregar e executar o aplicativo. Parte desse processo envolve espiar dentro do cabeçalho PE (execução portátil) do executável para determinar se o CLR é necessário. Como você já deve ter adivinhado, há sinalizadores no PE que indicam código gerenciado. Nesse caso, o carregador do Windows inicia o CLR que, em seguida, é responsável por carregar e executar o aplicativo gerenciado. (Essa é uma descrição simplificada do processo, pois há muitas etapas envolvidas, incluindo determinar qual versão do CLR executar, configurar o AppDomain 'sandbox', etc.)
Interoperabilidade
Conforme o aplicativo gerenciado é executado, ele pode (supondo que as permissões de segurança apropriadas) interajam com APIs nativas (incluindo a API Win32) e objetos COM por meio dos recursos de interoperabilidade do CLR. Seja chamando uma API de plataforma nativa, fazendo uma solicitação COM ou realizando marshaling de uma estrutura ao executar completamente dentro do ambiente de 32 bits, o desenvolvedor fica isolado de ter que pensar em tamanhos de tipo de dados e alinhamento de dados.
Ao considerar a migração para 64 bits, será essencial pesquisar quais dependências seu aplicativo tem.
Insira o CLR para o ambiente de 64 bits
Para que o código gerenciado seja executado no ambiente de 64 bits consistente com o ambiente de 32 bits, a equipe do .NET desenvolveu o CLR (Common Language Runtime) para os sistemas Itanium e x64 de 64 bits. O CLR precisava cumprir estritamente as regras da CLI (Common Language Infrastructure) e do Common Language Type System para garantir que o código escrito em qualquer uma das linguagens .NET pudesse interoperar como no ambiente de 32 bits. Além disso, veja a seguir uma lista de algumas das outras partes que também tiveram que ser portadas e/ou desenvolvidas para o ambiente de 64 bits:
- Bibliotecas de classes base (System.*)
- Compilador Just-In-Time
- Suporte à depuração
- SDK do .NET Framework
Suporte a código gerenciado de 64 bits
O .NET Framework versão 2.0 dá suporte aos processadores Itanium e x64 de 64 bits em execução:
- Windows Server 2003 SP1
- Versões futuras do cliente windows de 64 bits
(Não é possível instalar o .NET Framework versão 2.0 no Windows 2000. Os arquivos de saída produzidos usando o .NET Framework versões 1.0 e 1.1 serão executados em WOW64 em um sistema operacional de 64 bits.)
Ao instalar o .NET Framework versão 2.0 na plataforma de 64 bits, você não está apenas instalando toda a infraestrutura necessária para executar seu código gerenciado no modo de 64 bits, mas está instalando a infraestrutura necessária para que seu código gerenciado seja executado no subsistema Windows no Windows ou WoW64 (modo de 32 bits).
Uma migração simples de 64 bits
Considere um aplicativo .NET que seja um código seguro de tipo 100%. Nesse cenário, é possível pegar o executável do .NET executado em seu computador de 32 bits e movê-lo para o sistema de 64 bits e executá-lo com êxito. Por que isso funciona? Como o assembly é 100% seguro do tipo, sabemos que não há dependências de código nativo ou objetos COM e que não há nenhum código "não seguro", o que significa que o aplicativo é executado inteiramente sob o controle do CLR. O CLR garante que, embora o código binário gerado como resultado da compilação JIT (Just-In-Time) seja diferente entre 32 e 64 bits, o código executado será semanticamente o mesmo. (Não é possível instalar o .NET Framework versão 2.0 no Windows 2000. Os arquivos de saída produzidos usando .NET Framework versões 1.0 e 1.1 serão executados em WOW64 em um sistema operacional de 64 bits.)
Na realidade, o cenário anterior é um pouco mais complicado da perspectiva de carregar o aplicativo gerenciado. Conforme discutido na seção anterior, o carregador do Windows é responsável por decidir como carregar e executar o aplicativo. No entanto, ao contrário do ambiente de 32 bits, executar em uma plataforma Windows de 64 bits significa que há dois (2) ambientes em que o aplicativo pode ser executado, seja no modo nativo de 64 bits ou no WoW64.
O carregador do Windows agora precisa tomar decisões com base no que descobre no cabeçalho PE. Como você deve ter adivinhado, há sinalizadores configuráveis no código gerenciado que ajudam nesse processo. (Consulte corflags.exe para exibir as configurações em um PE.) A lista a seguir representa informações encontradas no PE que auxiliam no processo de tomada de decisão.
- 64 bits – indica que o desenvolvedor criou o assembly especificamente visando um processo de 64 bits.
- 32 bits – indica que o desenvolvedor criou o assembly especificamente visando um processo de 32 bits. Nesse caso, o assembly será executado no WoW64.
- Independente – indica que o desenvolvedor criou o assembly com o Visual Studio 2005, chamado de código "Whidbey". ou ferramentas posteriores e que o assembly pode ser executado no modo de 64 bits ou 32 bits. Nesse caso, o carregador do Windows de 64 bits executará o assembly em 64 bits.
- Herdado – indica que as ferramentas que criaram o assembly eram "pré-Whidbey". Nesse caso específico, o assembly será executado no WoW64.
Nota Também há informações no PE que informam ao carregador do Windows se o assembly é direcionado para uma arquitetura específica. Essas informações adicionais garantem que os assemblies direcionados para uma arquitetura específica não sejam carregados em uma diferente.
Os compiladores C#, Visual Basic .NET e C++ Whidbey permitem definir os sinalizadores apropriados no cabeçalho PE. Por exemplo, C# e THIRD têm uma opção de compilador /platform:{anycpu, x86, Itanium, x64} .
Nota Embora seja tecnicamente possível modificar os sinalizadores no cabeçalho PE de um assembly depois que ele tiver sido compilado, a Microsoft não recomenda fazer isso.
Se você estiver curioso para saber como esses sinalizadores são definidos em um assembly gerenciado, poderá executar o utilitário ILDASM fornecido no SDK do .NET Framework. A ilustração a seguir mostra um aplicativo "herdado".
Tenha em mente que um desenvolvedor marcando um assembly como Win64 determinou que todas as dependências do aplicativo seriam executadas no modo de 64 bits. Um processo de 64 bits não pode usar um componente de 32 bits em processo (e um processo de 32 bits não pode carregar um componente de 64 bits em processo). Tenha em mente que a capacidade do sistema de carregar o assembly em um processo de 64 bits não significa automaticamente que ele será executado corretamente.
Portanto, agora sabemos que um aplicativo composto por código gerenciado com 100% de segurança de tipo pode ser copiado (ou xcopy implantá-lo) em uma plataforma de 64 bits e tê-lo JIT e ser executado com êxito com o .NET no modo de 64 bits.
No entanto, muitas vezes vemos situações que não são ideais, e isso nos leva ao foco main deste artigo, que é aumentar a conscientização sobre os problemas relacionados à migração.
Você pode ter um aplicativo que não seja 100% fortemente tipado e que ainda seja capaz de ser executado com êxito em 64 bits no .NET. Será importante examinar seu aplicativo com cuidado, tendo em mente os possíveis problemas discutidos nas seções a seguir e determinar se você pode ou não ser executado com êxito em 64 bits.
Migração e invocação de plataforma
Fazer uso dos recursos de invocação de plataforma (ou p/invoke) do .NET refere-se ao código gerenciado que está fazendo chamadas para código não gerenciado ou nativo. Em um cenário típico, esse código nativo é uma DLL (biblioteca de vínculo dinâmico) que faz parte do sistema (API do Windows etc.), parte do aplicativo ou uma biblioteca de terceiros.
Usar código não gerenciado não significa explicitamente que uma migração para 64 bits terá problemas; em vez disso, deve ser considerado um indicador de que uma investigação adicional é necessária.
Tipos de dados no Windows
Cada aplicativo e cada sistema operacional tem um modelo de dados abstrato. Muitos aplicativos não expõem explicitamente esse modelo de dados, mas o modelo orienta a maneira como o código do aplicativo é escrito. No modelo de programação de 32 bits (conhecido como o modelo ILP32), os tipos de dados inteiro, longo e ponteiro têm 32 bits de comprimento. A maioria dos desenvolvedores usou esse modelo sem perceber.
No Microsoft Windows de 64 bits, essa suposição de paridade em tamanhos de tipo de dados é inválida. Tornar todos os tipos de dados de 64 bits de comprimento desperdiçaria espaço, pois a maioria dos aplicativos não precisa do tamanho aumentado. No entanto, os aplicativos precisam de ponteiros para dados de 64 bits e precisam da capacidade de ter tipos de dados de 64 bits em casos selecionados. Essas considerações levaram a equipe do Windows a selecionar um modelo de dados abstrato chamado LLP64 (ou P64). No modelo de dados LLP64, somente os ponteiros se expandem para 64 bits; todos os outros tipos de dados básicos (inteiro e longo) permanecem com 32 bits de comprimento.
O CLR do .NET para plataformas de 64 bits usa o mesmo modelo de dados abstrato LLP64. No .NET, há um tipo de dados integral, não amplamente conhecido, que é designado especificamente para conter informações de 'ponteiro': IntPtr cujo tamanho depende da plataforma (por exemplo, 32 bits ou 64 bits) em que está sendo executado. Considere o seguinte snippet de código:
[C#]
public void SizeOfIntPtr() {
Console.WriteLine( "SizeOf IntPtr is: {0}", IntPtr.Size );
}
Quando executado em uma plataforma de 32 bits, você obterá a seguinte saída no console:
SizeOf IntPtr is: 4
Em uma plataforma de 64 bits, você obterá a seguinte saída no console:
SizeOf IntPtr is: 8
Nota Se você quiser marcar em runtime se está ou não em execução em um ambiente de 64 bits, você pode usar o IntPtr.Size como uma maneira de fazer essa determinação.
Considerações sobre migração
Ao migrar aplicativos gerenciados que usam p/invoke, considere os seguintes itens:
- Disponibilidade de uma versão de 64 bits da DLL
- Uso de tipos de dados
Disponibilidade
Uma das primeiras coisas que precisam ser determinadas é se o código não gerenciado no qual seu aplicativo tem uma dependência está disponível para 64 bits.
Se esse código tiver sido desenvolvido internamente, sua capacidade de sucesso aumentará. É claro que você ainda precisará alocar recursos para portar o código não gerenciado para 64 bits, juntamente com os recursos apropriados para teste, garantia de qualidade etc. (Este white paper não está fazendo recomendações sobre processos de desenvolvimento; em vez disso, ele está tentando apontar que os recursos podem precisar ser alocados para tarefas para portar código.)
Se esse código for de terceiros, você precisará investigar se esse terceiro já tem o código disponível para 64 bits e se o terceiro estaria disposto a disponibilizá-lo.
O problema de maior risco surgirá se o terceiro não fornecer mais suporte para esse código ou se o terceiro não estiver disposto a fazer o trabalho. Esses casos exigem pesquisas adicionais sobre bibliotecas disponíveis que fazem funcionalidades semelhantes, se o terceiro permitirá que o cliente faça a porta por conta própria, etc.
É importante ter em mente que uma versão de 64 bits do código dependente pode ter alterado assinaturas de interface que podem significar trabalho de desenvolvimento adicional e para resolve diferenças entre as versões de 32 e 64 bits do aplicativo.
Tipos de dados
O uso de p/invoke requer que o código desenvolvido no .NET declare um protótipo do método que o código gerenciado está direcionando. Dada a seguinte declaração C:
[C++]
typedef void * HANDLE
HANDLE GetData();
Exemplos de métodos protótipos são mostrados abaixo:
[C#]
[DllImport( "sampleDLL", CallingConvention=CallingConvention.Cdecl )]
public static extern int DoWork( int x, int y );
[DllImport( "sampleDLL", CallingConvention=CallingConvention.Cdecl )]
public unsafe static extern int GetData();
Vamos examinar estes exemplos de olho em problemas de migração de 64 bits:
O primeiro exemplo chama o método DoWork passando dois (2) inteiros de 32 bits e esperamos que um inteiro de 32 bits seja retornado. Embora estejamos executando em uma plataforma de 64 bits, um inteiro ainda é de 32 bits. Não há nada neste exemplo específico que deva dificultar nossos esforços de migração.
O segundo exemplo requer que algumas alterações no código sejam executadas com êxito em 64 bits. O que estamos fazendo aqui é chamar o método GetData e declarar que estamos esperando que um inteiro seja retornado, mas em que a função realmente retorna um ponteiro int. Aqui reside nosso problema: lembre-se de que inteiros são de 32 bits, mas em ponteiros de 64 bits são 8 bytes. Como se vê, um pouco de código no mundo de 32 bits foi escrito supondo que um ponteiro e um inteiro tinham o mesmo comprimento, 4 bytes. No mundo de 64 bits, isso não é mais verdade.
Nesse último caso, o problema pode ser resolvido alterando a declaração de método para usar um IntPtr no lugar do int.
public unsafe static extern IntPtr GetData();
Fazer essa alteração funcionará nos ambientes de 32 e 64 bits. Lembre-se de que o IntPtr é específico da plataforma.
Usar p/invoke em seu aplicativo gerenciado não significa que a migração para a plataforma de 64 bits não será possível. Também não significa que haverá problemas. O que isso significa é que você deve examinar as dependências do código não gerenciado que seu aplicativo gerenciado tem e determinar se haverá algum problema.
Migração e interoperabilidade COM
A interoperabilidade COM é uma funcionalidade assumida da plataforma .NET. Como a discussão anterior sobre invocação de plataforma, usar a interoperabilidade COM significa que o código gerenciado está fazendo chamadas para código não gerenciado. No entanto, ao contrário da invocação de plataforma, a interoperabilidade COM também significa ter a capacidade de código não gerenciado chamar código gerenciado como se fosse um componente COM.
Mais uma vez, usar o código COM não gerenciado não significa que uma migração para 64 bits terá problemas; em vez disso, deve ser considerado um indicador de que uma investigação adicional é necessária.
Considerações sobre migração
É importante entender que, com o lançamento do .NET Framework versão 2.0, não há suporte para interoperabilidade entre arquiteturas. Para ser mais sucinto, você não pode usar a interoperabilidade COM entre 32 bits e 64 bits no mesmo processo. Mas você pode usar a interoperabilidade COM entre 32 e 64 bits se tiver um servidor COM fora do processo. Se não for possível usar um servidor COM fora do processo, você desejará marcar seu assembly gerenciado como Win32 em vez de Win64 ou Independente para que seu programa seja executado no WoW64 para que ele possa interoperar com o objeto COM de 32 bits.
Veja a seguir uma discussão sobre as diferentes considerações que devem ser fornecidas para fazer uso da interoperabilidade COM em que o código gerenciado faz chamadas COM em um ambiente de 64 bits. Especificamente:
- Disponibilidade de uma versão de 64 bits da DLL
- Uso de tipos de dados
- Bibliotecas de tipo
Disponibilidade
A discussão na seção p/invoke sobre a disponibilidade de uma versão de 64 bits do código dependente também é relevante para esta seção.
Tipos de dados
A discussão na seção p/invoke sobre tipos de dados de uma versão de 64 bits do código dependente também é relevante para esta seção.
Bibliotecas de tipo
Ao contrário dos assemblies, as bibliotecas de tipos não podem ser marcadas como 'neutras'; eles devem ser marcados como Win32 ou Win64. Além disso, a biblioteca de tipos deve ser registrada para cada ambiente no qual o COM será executado. Use tlbimp.exe para gerar um assembly de 32 bits ou 64 bits de uma biblioteca de tipos.
Usar a interoperabilidade COM em seu aplicativo gerenciado não significa que a migração para a plataforma de 64 bits não será possível. Também não significa que haverá problemas. O que isso significa é que você deve examinar as dependências que seu aplicativo gerenciado tem e determinar se haverá algum problema.
Migração e código não seguro
A linguagem C# principal difere notavelmente de C e C++ em sua omissão de ponteiros como um tipo de dados. Em vez disso, o C# fornece referências e a capacidade de criar objetos gerenciados por um coletor de lixo. Na linguagem C# principal, simplesmente não é possível ter uma variável não inicializada, um ponteiro "pendente" ou uma expressão que indexa uma matriz além de seus limites. Categorias inteiras de bugs que assolam rotineiramente programas C e C++ são, portanto, eliminadas.
Embora praticamente cada constructo de tipo de ponteiro em C ou C++ tenha uma contraparte de tipo de referência em C#, há situações em que o acesso a tipos de ponteiro se torna uma necessidade. Por exemplo, a interfação com o sistema operacional subjacente, o acesso a um dispositivo mapeado na memória ou a implementação de um algoritmo crítico de tempo podem não ser possíveis ou práticos sem acesso a ponteiros. Para atender a essa necessidade, o C# fornece a capacidade de escrever código não seguro.
No código não seguro, é possível declarar e operar em ponteiros, executar conversões entre ponteiros e tipos integrais, usar o endereço das variáveis e assim por diante. De certa forma, escrever código não seguro é muito parecido com escrever código C em um programa C#.
O código não seguro é, na verdade, um recurso "seguro" da perspectiva de desenvolvedores e usuários. O código não seguro deve ser claramente marcado com o modificador não seguro, para que os desenvolvedores não possam usar recursos não seguros acidentalmente.
Considerações sobre migração
Para discutir os possíveis problemas com código não seguro, vamos explorar o exemplo a seguir. Nosso código gerenciado faz chamadas para uma DLL não gerenciada. Em particular, há um método chamado GetDataBuffer que retorna 100 itens (neste exemplo, estamos retornando um número fixo de itens). Cada um desses itens consiste em um inteiro e um ponteiro. O código de exemplo abaixo é um trecho do código gerenciado mostrando a função não segura responsável por lidar com esses dados retornados.
[C#]
public unsafe int UnsafeFn() {
IntPtr * inputBuffer = sampleDLL.GetDataBuffer();
IntPtr * ptr = inputBuffer;
int result = 0;
for ( int idx = 0; idx < 100; idx ++ ) {
// Add 'int' from DLL to our result
result = result + ((int) *ptr);
// Increment pointer over int (
ptr = (IntPtr*)( ( (byte *) ptr ) + sizeof( int ) );
// Increment pointer over pointer (
ptr = (IntPtr*)( ( (byte *) ptr ) + sizeof( int ) );
}
return result;
}
Nota Este exemplo específico poderia ter sido feito sem o uso de código não seguro. Mais especificamente, há outras técnicas, como marshaling, que poderiam ter sido usadas. Mas, para essa finalidade, estamos usando código não seguro.
O UnsafeFn percorre os 100 itens e soma os dados inteiros. À medida que estamos percorrendo um buffer de dados, o código precisa passar por cima do inteiro e do ponteiro. No ambiente de 32 bits, esse código funciona bem. No entanto, como discutimos anteriormente, os ponteiros são de 8 bytes no ambiente de 64 bits e, portanto, o segmento de código (mostrado abaixo) não funcionará corretamente, pois está fazendo uso de uma técnica de programação comum, por exemplo, tratando um ponteiro como equivalente a um inteiro.
// Increment pointer over pointer (
ptr = (IntPtr*)( ( (byte *) ptr ) + sizeof( int ) );
Para que esse código funcione no ambiente de 32 bits e 64 bits, seria necessário alterar o código para o seguinte.
// Increment pointer over pointer (
ptr = (IntPtr*)( ( (byte *) ptr ) + sizeof( IntPtr ) );
Como acabamos de ver, há instâncias em que o uso de código não seguro é necessário. Na maioria dos casos, ele é necessário como resultado da dependência do código gerenciado em alguma outra interface. Independentemente dos motivos pelos quais o código não seguro existe, ele precisa ser revisado como parte do processo de migração.
O exemplo que usamos acima é relativamente simples e a correção para fazer o programa funcionar em 64 bits foi simples. Claramente, há muitos exemplos de código não seguro que são mais complexos. Alguns exigirão uma revisão profunda e talvez retroceder e repensar a abordagem que o código gerenciado está usando.
Para repetir o que você já leu, usar código não seguro em seu aplicativo gerenciado não significa que a migração para a plataforma de 64 bits não será possível. Também não significa que haverá problemas. O que isso significa é que você deve examinar todo o código não seguro que seu aplicativo gerenciado tem e determinar se haverá algum problema.
Migração e marshaling
O marshaling fornece uma coleção de métodos para alocar memória não gerenciada, copiar blocos de memória não gerenciados e converter tipos gerenciados em tipos não gerenciados, bem como outros métodos diversos usados ao interagir com código não gerenciado.
O marshaling é manifestado por meio da classe marshal do .NET. Os métodos estáticos ou compartilhados no Visual Basic definidos na classe Marshal são essenciais para trabalhar com dados não gerenciados. Desenvolvedores avançados que criam marshalers personalizados que precisam fornecer uma ponte entre os modelos de programação gerenciados e não gerenciados normalmente usam a maioria dos métodos definidos.
Considerações sobre migração
O marshaling apresenta alguns dos desafios mais complexos associados à migração de aplicativos para 64 bits. Dada a natureza do que o desenvolvedor está tentando realizar com marshaling, ou seja, transferir informações estruturadas para, de ou para e de código gerenciado e não gerenciado, veremos que estamos fornecendo informações, às vezes de baixo nível, para ajudar o sistema.
Em termos de layout, há duas declarações específicas que podem ser feitas pelo desenvolvedor; essas declarações normalmente são feitas por meio do uso de atributos de codificação.
LayoutKind.Sequential
Vamos examinar a definição conforme fornecido na Ajuda do SDK do .NET Framework:
"Os membros do objeto são dispostos sequencialmente, na ordem em que aparecem quando exportados para memória não gerenciada. Os membros são dispostos de acordo com o empacotamento especificado em StructLayoutAttribute.Pack e podem ser não contíguos."
Estamos sendo informados de que o layout é específico para a ordem em que ele é definido. Em seguida, tudo o que precisamos fazer é garantir que as declarações gerenciadas e não gerenciadas sejam semelhantes. Mas, também estamos sendo informados de que empacotar é um ingrediente crítico, também. Neste ponto, você não ficará surpreso ao saber que, sem intervenção explícita do desenvolvedor, há um valor de pacote padrão. Como você já deve ter adivinhado, o valor do pacote padrão não é o mesmo entre sistemas de 32 bits e 64 bits.
A instrução na definição sobre membros não contíguos está se referindo ao fato de que, como há tamanhos de pacote padrão, os dados dispostos na memória podem não estar no byte 0, byte 1, byte2 etc. Em vez disso, o primeiro membro estará no byte 0, mas o segundo membro pode estar no byte 4. O sistema faz esse empacotamento padrão para permitir que o computador obtenha acesso aos membros sem precisar lidar com problemas de desalinhamento.
Aqui está uma área que precisamos prestar muita atenção ao empacotamento e, ao mesmo tempo, tentar deixar o sistema agir em seu modo preferido.
Veja a seguir um exemplo de uma estrutura definida no código gerenciado, bem como a estrutura correspondente definida em código não gerenciado. Você deve tomar nota de como este exemplo demonstra como definir o valor do pacote em ambos os ambientes.
[C#]
[StructLayout(LayoutKind.Sequential, Pack=1)]
public class XYZ {
public byte arraysize = unchecked((byte)-1);
[MarshalAs(UnmanagedType.ByValArray, SizeConst=52)]
public int[] padding = new int[13];
};
[unmanaged c++]
#pragma pack(1)
typedef struct{
BYTE arraysize; // = (byte)-1;
int padding[13];
} XYZ;
LayoutKind.Explicit
Vamos examinar a definição conforme fornecido na Ajuda do .NET FrameworkSDK:
"A posição precisa de cada membro de um objeto na memória não gerenciada é controlada explicitamente. Cada membro deve usar FieldOffsetAttribute para indicar a posição desse campo dentro do tipo."
Estamos sendo informados aqui que o desenvolvedor fornecerá deslocamentos exatos para ajudar no marshaling de informações. Portanto, é essencial que o desenvolvedor especifique corretamente as informações no atributo FieldOffset .
Então, onde estão os possíveis problemas? Tendo em mente que os deslocamentos de campo são definidos sabendo o tamanho do membro de dados em andamento, é importante lembrar que nem todos os tamanhos de tipo de dados são iguais entre 32 bits e 64 bits. Especificamente, os ponteiros têm 4 ou 8 bytes de comprimento.
Agora temos um caso em que talvez seja necessário atualizar nosso código-fonte gerenciado para direcionar os ambientes específicos. O exemplo a seguir mostra uma estrutura que inclui um ponteiro. Embora tenhamos feito do ponteiro um IntPtr, ainda há uma diferença ao passar para 64 bits.
[C#]
[StructLayout(LayoutKind.Explicit)]
internal struct FooValue {
[FieldOffset(0)] public int dwType;
[FieldOffset(4)] public IntPtr pType;
[FieldOffset(8)] public int typeValue;
}
Para 64 bits, precisamos ajustar o deslocamento de campo para o último membro de dados na estrutura, pois ele realmente começa no deslocamento 12 em vez de 8.
[C#]
[StructLayout(LayoutKind.Explicit)]
internal struct FooValue {
[FieldOffset(0)] public int dwType;
[FieldOffset(4)] public IntPtr pType;
[FieldOffset(12)] public int typeValue;
}
O uso do marshaling é uma realidade quando é necessária uma interoperabilidade complexa entre código gerenciado e não gerenciado. Usar essa funcionalidade avançada não é um indicador de que você pode migrar seu aplicativo de 32 bits para o ambiente de 64 bits. No entanto, devido às complexidades associadas ao uso do marshaling, essa é uma área em que é necessária atenção cuidadosa aos detalhes.
A análise do código indicará se binários separados são necessários para cada uma das plataformas e se você também precisará fazer modificações em seu código não gerenciado para resolver problemas como empacotamento.
Migração e serialização
A serialização é o processo de conversão do estado de um objeto em um formulário que possa ser persistido ou transportado. O complemento de serialização é desserialização, que converte um fluxo em um objeto. Juntos, esses processos permitem que os dados sejam facilmente armazenados e transferidos.
O .NET Framework apresenta duas tecnologias de serialização:
- A serialização binária preserva a fidelidade do tipo, que é útil para preservar o estado de um objeto entre diferentes chamadas de um aplicativo. Por exemplo, você pode compartilhar um objeto entre diferentes aplicativos serializando-o para a área de transferência. Você pode serializar um objeto para um fluxo, um disco, a memória, pela rede, e assim por diante. A Comunicação Remota do .NET usa a serialização para passar objetos "por valor" de um computador ou domínio de aplicativo para outro.
- A serialização XML serializa somente as propriedades públicas e os campos e não preserva a fidelidade de tipo. Isso é útil quando você deseja fornecer ou consumir dados sem restringir o aplicativo que usa os dados. Como o XML é um padrão aberto, é uma opção atrativa para compartilhar dados pela Web. SOAP é, da mesma forma, um padrão aberto, uma opção atrativa.
Considerações sobre migração
Quando pensamos em serialização, precisamos ter em mente o que estamos tentando alcançar. Uma pergunta a ter em mente ao migrar para 64 bits é se você pretende compartilhar informações serializadas entre as diferentes plataformas. Em outras palavras, o aplicativo gerenciado de 64 bits lerá (ou desserializará) as informações armazenadas por um aplicativo gerenciado de 32 bits.
Sua resposta ajudará a impulsionar a complexidade de sua solução.
- Talvez você queira escrever suas próprias rotinas de serialização para considerar as plataformas.
- Talvez você queira restringir o compartilhamento de informações, enquanto ainda permite que cada plataforma leia e escreva seus próprios dados.
- Talvez você queira revisitar o que está serializando e fazer alterações para ajudar a evitar alguns dos problemas.
Então, depois de tudo isso, quais são as considerações em relação à serialização?
- IntPtr tem 4 ou 8 bytes de comprimento, dependendo da plataforma. Se você serializar as informações, gravará dados específicos da plataforma na saída. Isso significa que você pode e terá problemas se tentar compartilhar essas informações.
Se você considerar nossa discussão na seção anterior sobre marshaling e deslocamentos, poderá chegar a uma ou duas perguntas sobre como a serialização aborda as informações de empacotamento. Para serialização binária, o .NET usa internamente o acesso não alinhado correto ao fluxo de serialização usando leituras baseadas em bytes e manipulando corretamente os dados.
Como vimos, o uso da serialização não impede a migração para 64 bits. Se você usar a serialização XML, precisará converter de e para tipos gerenciados nativos durante o processo de serialização, isolando-o das diferenças entre as plataformas. O uso da serialização binária fornece uma solução mais rica, mas cria a situação em que as decisões precisam ser tomadas sobre como as diferentes plataformas compartilham informações serializadas.
Resumo
A migração para 64 bits está chegando e a Microsoft tem trabalhado para fazer a transição de aplicativos gerenciados de 32 bits para 64 bits o mais simples possível.
No entanto, não é realista supor que é possível apenas executar o código de 32 bits em um ambiente de 64 bits e executá-lo sem examinar o que você está migrando.
Conforme mencionado anteriormente, se você tiver um código gerenciado seguro de 100% de tipo, poderá realmente copiá-lo para a plataforma de 64 bits e executá-lo com êxito no CLR de 64 bits.
Mas, mais do que provável, o aplicativo gerenciado estará envolvido com qualquer um ou todos os seguintes:
- Invocando APIs de plataforma por meio de p/invoke
- Invocando objetos COM
- Fazendo uso de código não seguro
- Usando o marshaling como um mecanismo para compartilhar informações
- Usar a serialização como uma forma de manter o estado
Independentemente de quais dessas coisas seu aplicativo está fazendo, será importante fazer sua lição de casa e investigar o que seu código está fazendo e quais dependências você tem. Depois de fazer essa lição de casa, você terá que examinar suas escolhas para fazer qualquer um ou todos os seguintes procedimentos:
- Migre o código sem alterações.
- Faça alterações no código para lidar com ponteiros de 64 bits corretamente.
- Trabalhe com outros fornecedores, etc., para fornecer versões de 64 bits de seus produtos.
- Faça alterações na lógica para lidar com marshaling e/ou serialização.
Pode haver casos em que você tome a decisão de não migrar o código gerenciado para 64 bits, nesse caso, você tem a opção de marcar seus assemblies para que o carregador do Windows possa fazer a coisa certa na inicialização. Tenha em mente que as dependências downstream têm um impacto direto no aplicativo geral.
Fxcop
Você também deve estar ciente das ferramentas disponíveis para ajudá-lo na migração.
Hoje a Microsoft tem uma ferramenta chamada FxCop, que é uma ferramenta de análise de código que verifica assemblies de código gerenciado do .NET em busca de conformidade com as Diretrizes de Design do Microsoft .NET Framework. Ele usa reflexão, análise msil e análise de grafo de chamada para inspecionar assemblies para mais de 200 defeitos nas seguintes áreas: convenções de nomenclatura, design de biblioteca, localização, segurança e desempenho. O FxCop inclui as versões de gui e de linha de comando da ferramenta, bem como um SDK, para criar suas próprias regras. Para obter mais informações, consulte o site do FxCop . A Microsoft está no processo de desenvolver regras adicionais do FxCop que fornecerão informações para ajudá-lo em seus esforços de migração.
Há também funções de biblioteca gerenciadas para ajudá-lo no runtime a determinar em qual ambiente você está executando.
- System.IntPtr.Size – para determinar se você está executando no modo de 32 bits ou 64 bits
- System.Reflection.Module.GetPEKind – para consultar programaticamente um .exe ou .dll para ver se ele deve ser executado apenas em uma plataforma específica ou em WOW64
Não há um conjunto específico de procedimentos para resolver todos os desafios que você pode enfrentar. Este white paper destina-se a aumentar sua conscientização sobre esses desafios e apresentar possíveis alternativas.