Prévia do material em texto
e-Book 4 MSC Rafael De Moura Moreira PARADIGMAS DE LINGUAGENS DE PROGRAMAÇÃO Sumário INTRODUÇÃO ������������������������������������������������� 3 COMPILAÇÃO ������������������������������������������������ 4 INTERPRETAÇÃO �����������������������������������������22 PROCESSOS HÍBRIDOS ������������������������������25 CONSIDERAÇÕES FINAIS ����������������������������34 REFERÊNCIAS BIBLIOGRÁFICAS & CONSULTADAS ��������������������������������������������37 3 INTRODUÇÃO Olá estudante! Estudaremos os processos através dos quais o computador compreende os programas que escrevemos nessas linguagens. Começaremos estudando o processo de compilação e seus processos relacionados, como o pré-pro- cessamento e o linking, e conceitos relacionados, como código objeto e bibliotecas. Em seguida estudaremos um pouco sobre o pro- cesso de interpretação, e por fim, estudaremos exemplos de linguagens híbridas, que utilizam em maior ou menor grau ambos os processos, trazen- do consigo vantagens de ambos e atenuando as desvantagens. Vamos lá? Bons estudos! 44 COMPILAÇÃO O primeiro processo que estudaremos é a com- pilação. Neste processo, o programa é traduzido uma única vez para a linguagem nativa da máqui- na, e ao final um programa executável é gerado. Utilizaremos a linguagem C como exemplo para ilustrar esse processo. Introdução Em outra oportunidade, estudamos que computa- dores possuem uma linguagem nativa, conhecida por linguagem de máquina. Essa linguagem é representada por números binários – sequências de dígitos 0 e 1 – representando diferentes instru- ções que devem ser executados por seus circuitos internos. Esses dígitos representam a passagem ou ausência de sinal elétrico, já que os circuitos digitais são formados por dispositivos que realizam o chaveamento de eletricidade. Para contornar as dificuldades óbvias da escrita de programas complexos utilizando apenas números, foram criadas as linguagens de montagem, chamadas de linguagens assembly. Elas dão um nome curto para cada uma das instruções, conhecido como mnemônico. Isso facilita bastante a programação, mas ainda estamos bastante próximos da máqui- na, lidando quase diretamente com seu hardware. 55 Quando utilizamos uma linguagem de nível mais alto, mais próxima da forma humana de raciocinar e de se expressar, os códigos precisam ser tradu- zidos para essa linguagem de máquina. Quando esse processo ocorre uma única vez, gerando ao seu final um programa em linguagem de máquina, ele é chamado de compilação. O processo de montagem do programa final possui várias etapas. Sem entrar em detalhes específicos de análise sintática e semântica, podemos visualizar algumas etapas mais gerais, como o pré-proces- samento, a compilação em si e o linking. Pré-processamento O pré-processamento é uma etapa de preparação antes do acionamento do compilador em si. Ele “prepara” o código – que, a princípio, provavelmente foi escrito de maneira a ser legível por outros seres humanos – para ser mais facilmente compreendido pelo compilador. Vamos tomar como exemplo a linguagem C. É muito comum que um programa escrito em C tenha logo em seu início uma ou mais linhas iniciadas com a expressão “#include”. Essa expressão indica que gostaríamos de importar códigos presentes em outros arquivos. O pré-processador fará o papel de buscar esses arquivos e copiar seu conteúdo para o programa que será compilado. 66 De fato, todas as linhas iniciadas pelo caractere “#” em C são diretivas de compilação. Isso significa que elas não são instruções do programa, e sim instruções para o pré-processador que irão afetar a compilação do programa. Por exemplo, temos diretivas de compilação condi- cional. Essas irão analisar certas condições (como a declaração ou não de certas definições) para determinar se um bloco de código será compilado ou ignorado. Isso pode ser útil quando estamos escrevendo um código que deverá ser compilado para vários sistemas diferentes, com muitas partes em comum, porém também algumas partes espe- cíficas para cada sistema. A existência de certas definições pode ser usada para determinar para qual sistema estamos compilando, e o pré-proces- sador descartará o código que não se enquadra. É comum também a criação de macros. Essas macros podem ser apenas valores ou podem ser uma espécie de “função de uma linha”. No primeiro caso, após a diretiva “#define”, determina-se um nome, e em seguida, opcionalmente, um valor. Esse nome representará um “valor constante”. No pré-processamento, todas as ocorrências desse nome serão substituídas pelo valor. No caso das macros estilo função, é possível especificar parâ- metros e um pequeno trecho de código. Porém, ao contrário de funções convencionais, essas macros 77 não virarão funções em linguagem de máquina. Ao invés disso, todos os trechos onde elas foram chamadas também serão substituídas durante o pré-processamento pelo código definido. Outro ponto que também é tratado pelo pré-pro- cessamento são os comentários do código. Na realidade, comentários não são ignorados pelo compilador. O que realmente ocorre é que o pré-pro- cessador identifica os comentários e os substitui por espaço em branco. Essas são algumas das várias tarefas realizadas pelo pré-processador para facilitar a vida do com- pilador, mas há outras inclusive mais complexas. Uma vez finalizado o pré-processamento, a etapa de compilação pode iniciar. Compilação A etapa de compilação é, possivelmente, a etapa mais complexa. Enquanto em assembly cada linha de código corresponde diretamente a uma instru- ção de máquina, o mesmo não pode ser dito sobre linguagens de alto nível. Uma linha de código em linguagem de alto nível geralmente corresponde a várias instruções de máquina. O compilador realiza diversas análises sobre o código. Para termos uma noção da complexidade, ele precisa não apenas identificar os comandos 88 sendo utilizados, mas também variáveis criadas pelo programador, funções etc. Também é comum que uma linha de código afete o trabalho de uma linha anterior a ela, o que faz com que normalmente um compilador analise o código mais de uma vez, já que na segunda ele já sabe o que vem a frente. Ao fazermos uma tradução de uma linguagem humana para outra, por exemplo do inglês para o português, a simplicidade de um texto em uma não implica, necessariamente, que o texto traduzido será simples. O tradutor pode usar sinônimos mais simples ou mais complexos, bem como organizar as frases de maneiras mais convencionais ou não, preservando o mesmo significado. O mesmo ocorre com o compilador. Há várias maneiras de se implementar em linguagem de máquina um programa escrito em linguagem de alto nível. Um programa bem escrito em alto nível pode ter mau desempenho se o compilador ado- tar estruturas e construções pouco eficientes em assembly. Por conta disso, vários compiladores realizam diversas otimizações. Por exemplo, uma função muito curta pode não se tornar uma sub-rotina em assembly, e sim ter seu conteúdo “copiado” para todos os pontos onde foi chamada, economizando o tempo e gasto de memória típicos de uma chamada de função – 99 uma técnica conhecida como inlining. Malhas de repetição podem ter algumas de suas instruções multiplicadas, e o número total de repetições da malha em si reduzido, uma técnica conhecida como loop unrolling, que também tem por objeti- vo economizar recursos gastos na verificação da condição de parada da malha. Por fim, além das otimizações “padrão”, compila- dores podem realizar algumas otimizações dis- poníveis especificamente em um ou outro tipo de hardware. Alguns compiladores mais conhecidos, como o gcc (GNU Compiler Collection), permitem que o programador opte por fazer ou não várias dessas otimizações individualmente, dependendo do resultado desejado ou de restrições do projeto. Ao final de todo o trabalho de compilação, tere- mos um conjunto de instruções emlinguagem de máquina (binário), ou em assembly, que logo em seguida será convertido para binário por um as- sembler. Chamamos esse binário de código objeto. Linking Para que o código objeto esteja pronto para ser utilizado e se torne útil, ele será ligado ou unido a outros códigos objeto (como bibliotecas) para finalmente se tornar um programa executável ou outra biblioteca. 1010 O termo “biblioteca” é um termo um pouco “guar- da-chuva” que utilizamos em alguns contextos diferentes. Um deles é quando temos códigos escritos na mesma linguagem (ou em linguagens compatíveis) que podem ser importados para utilizarmos na escrita de nosso código. Outro significado de biblioteca, mais interessante no contexto que estamos estudando, é de um código já compilado, bastante similar a um programa, mas com uma diferença: ela não possui um ponto de entrada, ou uma função principal. Esse código é uma coleção de recursos prontos, como funções, exatamente como as bibliotecas que incluímos em nossos códigos, mas com a diferença de que já estão compilados. Há duas maneiras que nosso programa pode ser ligado a essas bibliotecas: estaticamente ou dinamicamente. No caso da ligação dinâmica, a biblioteca será para sempre um arquivo apartado do nosso programa executável (como os arqui- vos com extensão .DLL no Windows), e na hora do programa ser executado o sistema irá buscar a biblioteca e disponibilizar seu conteúdo para o programa. Já no caso da ligação estática, o código objeto da biblioteca é incorporado ao do que acabamos de 1111 gerar pelo linker, resultando em outra biblioteca ou em um programa executável. Figura 1: O funcionamento do linker. Fonte: QEF. Input and output file types of the linking process. 2009. Disponível em https://en.wikipedia.org/wiki/Interpreter_ (computing)#/media/File:Linker.svg. Acesso em: 24 set. 2021. O programa em execução Em várias plataformas modernas, o programa em execução terá alguns elementos em comum. Sua memória total conterá uma área de código e uma área de dados, e esta será subdividida em duas regiões: o stack (pilha) e o heap (monte). A área de código é onde suas instruções de máquina serão armazenadas. Ao abrirmos um programa, suas instruções são copiadas para a memória e executadas. Porém, o programa também manipula 1212 dados frequentemente e para isso possui uma área designada para armazená-los. O stack é onde ocorre a alocação estática de memória: variáveis convencionais criadas den- tro de funções são armazenadas nessa região. Quando funções são chamadas, seus parâmetros são copiados para essa região também. O nome dessa estrutura indica seu comportamento: ao empilharmos objetos sobre uma mesa, o último objeto a ser colocado na pilha é o primeiro a ser removido, e o primeiro objeto será o último a ser removido. A pilha sempre irá crescer em um único sentido e diminuir no sentido oposto, tornando essa região bastante limitada. Quando temos várias chamadas recursivas sem controle, por exemplo, é comum nos depararmos com o erro de estouro de pilha: usamos mais memória dessa região do que poderíamos. A outra região, o heap, é mais flexível, porque pode ser utilizada para a alocação dinâmica de memória. Quando não sabemos previamente quanta memória necessitaremos e vamos pedir durante a execução do programa por mais memória para o sistema, essa memória extra pertence ao heap. Quando estamos perto de lotar o heap alocado para nosso programa, o sistema operacional pode aumentar seu tamanho, disponibilizando mais memória para o programa. Porém, ao contrário da pilha, o monte 1313 não é esvaziado sozinho. Memória alocada no heap em linguagens como C fica sob responsabilidade do programador. Se o programa não informar ao sistema que acabou de a usar, ela ficará marcada como ocupada, gerando um problema conhecido como vazamento de memória. Algumas linguagens (geralmente orientadas a objeto) oferecem um recurso chamado de coletor de lixo, que identifica automaticamente dados na heap que não serão mais utilizados e os liberam automaticamente. Tempo de compilação versus tempo de execução É importante compreender quais ações são re- alizadas durante o processo de compilação do programa – ou seja, uma única vez enquanto ele está sendo gerado – e quais ocorrem quando ele está sendo executado. Chamamos esses diferentes momentos da vida de um programa de “tempo de compilação” e “tempo de execução”. Erros e tratamento de exceção Considere, por exemplo, os erros de sintaxe. Um erro de sintaxe é aquele na qual cometemos um erro no uso da linguagem, como digitar errado o nome de um comando da linguagem ou utilizar incorretamente algum operador ou símbolo (como 1414 abrir um parêntese e não lembrar de fechá-lo). Esse tipo de erro é capturado pelo próprio compilador, pois, ao passar pelo programa, ele irá notar um comando não reconhecido ou a falta de algum símbolo que era esperado. O processo de compi- lação será interrompido e uma mensagem de erro será exibida. Agora vamos supor que o seu programa irá ler diversos valores em uma planilha fornecida pelo usuário e realizar algumas operações matemáticas sobre esses valores. Porém, por alguma falha ou da modelagem feita pelo programador ou de digitação da planilha pelo usuário há uma combinação de valores que fará aparecer o número zero em um denominador. Isso também é um erro, mas note que ele ocorreu com o programa já executando, e não na hora de compilar, portanto, meramente mandar dividir variáveis não provoca erros. Algumas linguagens fornecem meios para que o programador preveja antes da compilação erros que podem ocorrer na execução. Essa técnica é chamada de tratamento de exceções, e consiste em proteger uma região do código com algum comando indicando que é esperado que aquela região possa provocar erro, e em seguida outros blocos de códigos irão identificar qual o tipo de erro que ocorreu e executar outras ações corretivas, ao invés de simplesmente deixar o programa falhar, 1515 travar ou ser fechado pelo sistema operacional. Em algumas linguagens, existe também o conceito de exceções em tempo de compilação, permitindo que um possível erro de compilação leve a uma ação alternativa ao invés da falha na compilação. Polimorfismo Na programação orientada a objeto há o conceito de polimorfismo: um objeto de uma certa classe pode ser tratado como pertencente a outra classe ou a algum outro tipo de categoria desde que sejam respeitadas certas condições. Um exemplo bastante claro de polimorfismo ocorre na herança: quando uma classe é herdeira de outra classe, ela possui, por tabela, os métodos e atributos da classe mãe. É possível que os métodos sejam sobrescritos na classe filha, se comportando de maneira completamente distinta, mas eles existem com o mesmo nome. Sendo assim, código escrito para lidar com um objeto de uma certa classe ge- ralmente também consegue lidar com objetos de suas classes filhas, sem fazer distinção da classe. Outro exemplo é o que ocorre com as interfaces em Java. Uma interface não é uma classe, mas estabelece uma relação que lembra um pouco a herança. Uma interface define um conjunto de métodos. Ela não define a lógica interna dos mé- todos, ou seja, sua implementação. Mas define 1616 o que chamamos de assinatura de um método: seu nome, seu tipo de retorno e seus parâmetros. Quando uma classe implementa uma interface, ela assume o compromisso de possuir métodos que respeitem as assinaturas previstas. Portanto, um trecho de código feito para trabalhar com aquela interface aceitará objetos de qualquer classe que implemente aquela interface, também sem fazer distinção entre as classes. Imagine um sistema de um marketplace virtual, onde diferentes pessoas podem se cadastrar e fazer login: compradores, vendedores e administra- dores do sistema, cada um com diferentes níveis de segurança e passos de autenticação para logar. As trêsentidades podem ser modeladas com di- ferentes classes: Vendedor, Comprador e Admin. Todas essas classes podem ser herdeiras de uma superclasse Usuario, que prevê um método de login. Cada uma das subclasses possui sua própria versão do método de login tratando suas peculiaridades. O código para lidar com a autenticação pode ser uma função que irá receber um objeto Usuario e chama seu método de login. No momento da compilação, portanto, não está determinado qual função está sendo chamada ali. Durante a exe- cução, dependendo de quem tentar se logar, um 1717 objeto diferente será passado para aquela função, e, dependendo da classe, um método login diferente será executado. Chamamos esse tipo de caso de polimorfismo em tempo de execução, pois nem sequer o compilador sabia previamente qual ou quais métodos seriam chamados por aquela função. Em algumas linguagens, existe uma outra forma de polimorfismo chamado de polimorfismo em tempo de compilação. Vamos mais uma vez tomar o Java como exemplo. É possível que em uma mesma classe contenha diversos métodos com o mesmo nome, mas diferentes quantidades de parâmetros. Se em uma certa região do código o nome desse método aparecer, o compilador consegue deter- minar previamente qual das variações será exe- cutada, pois basta analisar os parâmetros sendo passados. Sendo assim, já no ato da compilação ocorre a ligação entre o método e a sua chamada. Considerações sobre segurança A diferença entre certas verificações ocorrerem em tempo de compilação ou de execução pode impactar dramaticamente o grau de segurança e confiabilidade do programa gerado. Vamos considerar um erro comum: o estouro de um vetor. Vetores são estruturas de dados 1818 contínuas na memória, geralmente armazenando diversos valores de um mesmo tipo. Eles podem ser acessados através de um índice, um número indicando qual das “casinhas” dentro da faixa contínua gostaríamos de acessar. Na linguagem C o compilador não verifica a possi- bilidade de estarmos acessando um índice inválido, ou seja, um índice que não pertence à faixa reser- vada para aquele vetor. Isso abre caminho para erros em tempo de execução: o programa sendo executado poderá invadir uma faixa indevida de memória, provocando diversos problemas, como o funcionamento inadequado do programa (por sobrescrever dados de maneira não planejada), um fechamento abrupto do programa pelo sistema operacional ou até mesmo abrir vulnerabilidades para usuários mal-intencionados e tecnicamente qualificados tentarem injetar código malicioso ou acessar informações protegidas. Algumas linguagens se comportam de maneira um pouco diferente. Por exemplo, na linguagem Rust, se o índice que será acessado depende de alguma interação que ocorrerá durante a execução (por exemplo, um valor digitado pelo usuário), ocor- rerá um erro de tempo de execução. Porém, se o programador explicitamente digitar no código um índice superior ao comprimento do vetor, o próprio 1919 compilador irá pegar o erro em potencial, evitando que o programa seja gerado com essa falha. De maneira geral, quanto mais erros puderem ser capturados já em tempo de compilação, melhor, pois erros de compilação impedem a criação do programa até que estejam corrigidos. Erros não pegos na compilação estarão “escondidos” no programa e podem passar despercebidos até que seja tarde demais e eles acabem provocando algum grande estrago. Compilação cruzada Um programa escrito em uma certa plataforma não precisa, necessariamente, ser projetado para executar na plataforma onde ele foi escrito. Vamos considerar um exemplo bastante claro: um aplica- tivo para smartphone, que será executado em um aparelho com sistema iOS ou Android. Apesar de não ser impossível, celulares hoje ainda são ferramentas pouco convenientes para a escri- ta de código: suas telas são bastante pequenas, enquanto desenvolvedores frequentemente usam até dois ou três monitores diferentes para ter um bom campo visual. Além disso, a digitação atra- vés de tela de toque não é tão fácil ou confortável quanto em um teclado físico que comporte todos os dedos simultaneamente. 2020 Desenvolvedores costumam programar utilizando PCs convencionais, como laptops ou desktops e sistemas operacionais como Windows, MacOS ou Linux – mesmo quando eles pretendem desenvol- ver aplicativos para smartphones ou para qualquer tipo de computador embarcado. Essa forma de trabalho é possível graças aos cross-compilers, ou compiladores cruzados. O compilador cruzado é desenvolvido para ser exe- cutado em uma plataforma, mas gerar programas que serão executados em outra plataforma. Por exemplo, você pode ter um kit de desenvol- vimento para Android em um computador com Windows. Você escreverá seus programas em um processador com arquitetura Intel x86 no Windows e o compilador será executado no Windows, mas ao final do processo você terá um programa que roda em celulares Android com processadores ARM. Trabalhar dessa maneira costuma ser um pouco mais complexo do que trabalhar diretamente na plataforma que irá executar os códigos, princi- palmente na etapa de testes e debugging. Mas é comum que os kits de desenvolvimento que incluem os cross-compilers também tragam outras ferramentas para auxiliar nessas tarefas. Uma dessas ferramentas costuma ser algum assistente para a comunicação com o dispositivo- 2121 -alvo. Através de um cabo ou de alguma forma de comunicação sem fio, o computador utilizado no desenvolvimento se comunica com o dispositivo sendo testado e solicita uma série de informações para monitorar o que está ocorrendo durante a execução do programa. Outra ferramenta bastante popular é um emulador: um programa que é executado no computador de desenvolvimento imitando o comportamento do dispositivo-alvo, podendo inclusive executar o programa gerado, mas permitindo que diversas informações sobre a execução sejam monitoradas e o próprio fluxo de execução seja controlado ou alterado para testar diferentes casos. 22 INTERPRETAÇÃO Quando um filme estrangeiro chega ao Brasil, é comum que seja oferecida a opção de assisti-lo dublado: uma equipe recebeu o filme na língua original bastante tempo antes da data de lançamen- to, traduziu todas as suas falas para o português e uma equipe de atores especializados gravou essa tradução. Ao final desse processo, o filme está permanentemente disponível em português. O processo de compilação de um programa, que estudamos no capítulo anterior, é análogo a essa tradução. Porém, quando estamos assistindo a um evento ao vivo em língua estrangeira, como uma entre- vista com um político, atleta ou artista, é comum que haja tradução simultânea: um intérprete deve escutar o que o entrevistado está falando, traduzir mentalmente e imediatamente falar a tradução em voz alta. Linguagens de programação interpretadas funcionam dessa maneira. Quando escrevemos um programa em uma lin- guagem interpretada, ele não será traduzido para gerar um programa executável. Ao invés disso, ele será salvo como um arquivo de texto, mesmo que contenha o nosso programa da maneira que escrevemos. Todos que desejarem utilizar nosso programa precisarão ter em seus computadores 23 um programa chamado de interpretador daquela linguagem. O interpretador irá abrir nosso código, ler cada linha e imediatamente realizar a instrução descrita naquela linha. De maneira geral, trabalhar com uma linguagem interpretada tende a ser mais simples: não pre- cisamos recompilar toda vez que fizermos uma modificação, tampouco compilar para diferentes máquinas ou sistemas. Todos os sistemas que possuírem um interpretador compatível poderão rodar o mesmo programa, sem alterações ou etapas adicionais por parte do programador. Em contrapartida, linguagens interpretadas tendem a ser menos eficientes, e seu consumo de recursos computacionais (como tempo de processamento e uso de memória) tende a ser pior.Devemos fazer algumas considerações importan- tes antes de prosseguir. A primeira delas é que, na prática, “não existe” linguagem interpretada ou linguagem compilada. A especificação de uma linguagem diz respeito aos seus comandos e comportamentos, mas estes podem ser coloca- dos em um compilador ou em um interpretador. Porém, algumas implementações específicas de certas linguagens são tão populares que seu uso se confunde com o da própria linguagem, fazendo com que a linguagem em si seja muito fortemen- 24 te associada ao compilador ou interpretador em questão. Em outros casos, é uma questão cultural mesmo: a linguagem C, por exemplo, é considerada mais “difícil” de programar do que linguagens como Python, justamente porque aquela fornece recur- sos para manipulação de hardware. Sendo assim, haveria pouca ou nenhuma vantagem em utilizar linguagem C em um ambiente interpretado, se comunicando com o interpretador ao invés de diretamente com o sistema físico. Já o Python, fre- quentemente utilizado para pequenas automações, pode trazer mais vantagens sendo interpretado, já que pequenas modificações não implicam em um longo processo de recompilação, e seus códigos são compatíveis com diferentes sistemas. Outra consideração importante é que atualmente são raras as linguagens puramente interpretadas, apesar de já ter sido comum. Conforme estuda- remos a seguir, as principais implementações de linguagens que coloquialmente chamamos de “interpretadas” combinam diversos processos diferentes, incluindo uma espécie de “compilação” que garantirá um desempenho melhor. Esse é o caso, por exemplo, do Python. Estudaremos essa abordagem híbrida a seguir. 2525 PROCESSOS HÍBRIDOS Para atenuar os problemas de desempenho tipi- camente associados às linguagens interpretadas, diversas implementações começaram a adotar alguns processos relacionados à compilação, enquanto se tenta preservar a linguagem ao me- nos parcialmente interpretada para não perder as vantagens, como a portabilidade do programa (o mesmo programa poder rodar em vários sistemas sem a necessidade de gerar executáveis diferentes). Citaremos algumas dessas técnicas mais comuns, utilizando como exemplo as linguagens Java e Python. Bytecode e máquinas virtuais No processo de compilação, diversas otimizações podem ser realizadas. Por exemplo, abordamos técnicas como loop unrolling e funções inline. Parte dessas otimizações pode ser aplicada porque o compilador pode, durante a compilação, analisar mais de uma vez o código, identificar as relações entre diferentes partes do código, a frequência com a qual se acredita que cada bloco de códigos deve ser executado, entre outras coisas. Um interpreta- dor clássico não possui toda essa versatilidade. Em contrapartida, ao final do processo de compi- lação, temos um código binário, que é totalmente 2626 dependente da arquitetura para a qual ele foi com- pilado, e não é compatível com outros sistemas computacionais, enquanto um programa escrito em linguagem interpretada pode ser executado sem modificações em qualquer computador que possua o interpretador adequado. Uma estratégia intermediária encontrada, que permite um certo grau de otimizações e o conse- quente ganho de performance, mas preservando a portabilidade do código é a compilação para bytecode, que será executado em uma máquina virtual. Uma máquina virtual é um computador fictício, im- plementado em software. Ela possui suas próprias instruções e recursos. Suas diferentes instruções podem ser representadas por um binário próprio, conhecido por bytecode. Esse bytecode é indepen- dente da máquina real, física, na qual a máquina virtual está sendo executada. Na prática, a máquina virtual é um interpretador de bytecode. Ela irá ler o código escrito em bytecode e executar as diferentes operações, resultando em uma espécie de “tradução ao vivo” de bytecode para binário nativo da máquina física. Uma van- tagem dessa abordagem é que o bytecode é uma linguagem muito mais próxima da linguagem real de máquina do que uma linguagem de alto nível, 2727 tornando a sua interpretação muito mais eficiente do que a interpretação direta de um programa em linguagem de alto nível. Para chegarmos ao bytecode, é necessária uma etapa de compilação: um compilador irá realizar em nosso código de alto nível os mesmos proces- sos que o compilador tradicional das linguagens compiladas fará, mas o resultado, ao invés de ser um código objeto escrito em binário nativo, será um bytecode, que posteriormente pode ser interpretado pela máquina virtual. Esses proces- sos incluem possíveis otimizações, tornando o bytecode gerado muito mais eficiente do que o código de alto nível original. Esse é outro ganho de performance providenciado pela ideia de bytecode e máquina virtual. A implementação de referência do Python, conhecida por CPython, segue esse processo. O interpretador de Python inclui um compilador de bytecode e uma máquina virtual própria. Na primeira execução de um programa, ele é compilado para bytecode, e nas execuções seguintes, essa etapa pode ser pulada, com o bytecode sendo chamado diretamente pela máquina virtual. Em programas Python com vários módulos, pode- mos notar o resultado da compilação ao analisar a pasta do nosso projeto: se você importou vários 2828 arquivos .py diferentes (a extensão padrão para códigos Python), perceberá que apareceram diver- sos arquivos .pyc, que é a extensão para códigos Python compilados para bytecode. É possível também instruir explicitamente o Python a gerar e armazenar o bytecode, de modo que o código seja enviado já pré-compilado para os usuários. Compilação just-in-time Outra etapa que você deve se recordar que ocorre na compilação é a otimização específica para o hardware-alvo. Essa etapa, ao contrário das outras técnicas que mencionamos, não depende apenas de lógica, mas da presença de recursos específicos no hardware onde o código será executado. Por exemplo, se o programa utiliza diversas ope- rações sobre grandes conjuntos de números de ponto flutuante, o compilador pode utilizar módulos específicos da CPU otimizados para fazer essas operações em grande velocidade caso ela os pos- sua. Se esses cálculos podem ocorrer de maneira independente entre si, o compilador pode utilizar recursos de paralelismo da CPU, caso ela permita. Na abordagem de máquinas virtuais estudada, a princípio não é possível realizar esse nível de otimização. Afinal, o programa está sendo compi- lado para uma máquina “imaginária” e não possui informações sobre a máquina física que está exe- 2929 cutando a máquina virtual, e por isso não pode se beneficiar de seus recursos. Para contornar essa limitação surgiu o conceito de compilação just-in-time, frequentemente abre- viada como JIT. Como o nome sugere, essa é uma compilação feita “em cima da hora”, quando o pro- grama está prestes a ser executado – ao contrário do conceito tradicional de compilação, conhecido por compilação ahead-of-time, ou seja, realizada em uma etapa anterior à execução do programa. Voltemos ao conceito anterior: máquinas virtuais interpretando bytecode. Algumas otimizações lógicas foram realizadas, e agora a máquina virtual deve interpretar todo o bytecode. Algumas máquinas virtuais trazem um compilador JIT junto consigo. Na primeira execução do bytecode, ela identifica trechos do código que poderiam ser compilados de bytecode para binário nativo e o faz imediata- mente. Nessa etapa, é possível, inclusive, realizar otimizações de hardware – afinal, no momento da execução, a máquina física é conhecida. Essas partes pré-compiladas nessa primeira execução podem ser armazenadas, de modo que em execuções futuras a etapa de compilação pode ser pulada. Por isso, é comum que em linguagens com JIT a primeira execução seja significativamente mais lenta, e as execuções posteriores sejam mais rápidas. 3030 Um exemplo clássico é o Java.Para que seu com- putador execute programas escritos em Java, você deve ter alguma implementação da Java Virtual Machine (JVM) instalada em seu computador – independentemente de qual seja sua CPU ou seu sistema operacional. O código Java é compilado para o bytecode da JVM, e em sua primeira exe- cução o JIT irá compilar partes do código para binário nativo, resultando em um grande ganho de performance. Módulos completos escritos em Java podem ser distribuídos em sua forma pré-compilada para by- tecode, independentemente de qual a plataforma que irá executá-los e em qual plataforma eles foram originalmente compilados. Tanto a compilação de trechos específicos para binário nativo quanto a interpretação do restante do bytecode só ocorrerão no momento da execução, já na máquina-alvo. Aplicações Android em geral são executadas por uma máquina virtual conhecida como ART desde a versão 5.0, que substituiu uma máquina anterior conhecida como Dalvik. Por isso diferentes aparelhos podem baixar aplicações da mesma loja, sem que os desenvolvedores precisem gerar múltiplas versões diferentes: o aplicativo baixado SAIBA MAIS 3131 pela loja é um bytecode que será processado pelo ART, podendo inclusive ser compilado para linguagem nativa da CPU do celular. A linguagem padrão para desenvolvimento Android era Java, e atualmente, apesar dele ainda ser aceito, o Google tem incentivado a migração para a linguagem Kotlin. Consulte a documentação do ART para saber mais sobre seus recursos: https://source.android.com/devices/tech/ dalvik?hl=pt-br. Acesso em 13 ago 2021. Intercompatibilidade entre linguagens A popularização da adoção de máquinas virtuais com seus próprios bytecodes acabou por trazer outra conveniência: linguagens diferentes inter- compatíveis entre si. Vamos tomar como exemplo novamente o Java. Sabemos que o resultado da primeira compilação de um programa em Java é um bytecode que pode ser interpretado pela JVM. O programa final é esse bytecode que será interpretado pela JVM e/ou compilado pelo JIT. O que aconteceria se um programa escrito em outra linguagem fosse compilado para bytecode da JVM? A JVM não “sabe” de onde surgiu o código que ela recebe. Ela apenas recebe um bytecode e executa. Várias linguagens atualmente, como Kotlin e Clojure, possuem compiladores para gerar bytecode para 3232 JVM. Sendo assim, é perfeitamente possível um programa em Kotlin utilizar uma biblioteca escrita em Java e vice-versa, afinal, uma vez compilados, todos eles são apenas programas em bytecode. De maneira experimental, existe até mesmo Py- thon compilando para JVM, uma implementação conhecida como Jython. Algo parecido ocorre na família de linguagens .NET, da Microsoft. Linguagens distintas, como C#, VB.NET e F# utilizam a mesma Common Lan- guage Infrastructure (Infraestrutura de Linguagem Comum) – CLI –, na qual o programa é compilado para uma linguagem intermediária (Common Inter- mediate Language), e essa será executada sobre um ambiente virtual responsável por fazer a ponte com a máquina física. Assim como no caso da Java Virtual Machine, outros desenvolvedores fornecem implementa- ções de linguagens variadas gerando código em Common Intermediate Language, como o projeto IronPython, visando a compatibilidade dentre o Python e as linguagens .NET. 3333 Não é necessário que diferentes linguagens sejam “compatíveis” entre si ou sejam executadas em uma mesma máquina virtual para que programas escritos em diferentes programas se comuniquem e troquem recursos. Uma API (Application Programming Interface – interfa- ce de programação de dispositivos) pode ser utilizada para intermediar conversas entre programas que não se relacionariam de maneira alguma. Ela consiste de um conjunto de regrinhas, como formato das mensagens que serão trocadas (ordem e tipo dos dados, por exemplo). Dessa maneira, um programa pode solicitar que outro programa distinto realize parte de seu trabalho sem ter acesso ao seu funcionamento interno. Um caso comum são as APIs dos próprios sistemas operacionais: para trabalhar com diversos recursos, como funcionalidades gráficas, programação concor- rente, comunicação de rede, entre outras, os sistemas operacionais possuem seus próprios programas e biblio- tecas e fornecem uma documentação de como nossos programas podem acessá-los e solicitar certas tarefas. Outro caso comum são as APIs HTTP: programas sen- do executados em servidores na internet que recebem mensagens de outros programas rodando nas chamadas máquinas-cliente solicitando acesso a dados. É assim que o aplicativo de rede social do seu smartphone so- licita aos servidores da rede o conteúdo que irá exibir na tela do seu aparelho. FIQUE ATENTO 34 CONSIDERAÇÕES FINAIS Nesta unidade, focamos nas diferentes formas como um programa escrito em uma linguagem de programação de alto nível – isto é, uma linguagem mais conveniente para nós, seres humanos – pode ser traduzido para instruções em linguagem de máquina, a linguagem nativa do computador. Começamos pela ideia mais básica, a compilação, também chamada em alguns locais de compilação ahead-of-time. Nesse processo, o programa é tra- duzido uma única vez, após passar por uma série de análises, otimizações e ligações com outros códigos. Ao final do processo, temos um programa executável, ou ao menos uma biblioteca binária que pode ser utilizada por outros programas. Ela tem a vantagem de poder gerar programas bastante eficientes e otimizados, e evitar que o usuário pre- cise instalar ferramentas da linguagem ou compilar sua própria versão. Como desvantagem, devemos fazer uma compilação específica para cada tipo de sistema que irá usar nosso programa, e recompilar sempre que fizermos uma modificação. Também estudamos como um programa executável é organizado na memória durante o seu funciona- mento, as diferentes maneiras que ele pode interagir com uma biblioteca – um conjunto de códigos já prontos – e a importância de compreender quais 35 processos ocorrem durante a compilação e quais ocorrem durante a execução, como a verificação de certos tipos de erro ou a identificação de quais funções serão executadas em uma certa região do código. Em seguida, estudamos a interpretação, uma ideia semelhante a uma “tradução simultânea”: um programa, chamado de interpretador, abre o código-fonte escrito em linguagem de alto nível e executa as instruções conforme as lê. Essa aborda- gem é bastante ineficiente e pouco otimizada, mas garante o máximo de portabilidade para o código, ou seja, o mesmo código pode ser executado em vários sistemas diferentes, desde que cada um possua um interpretador. Concluímos verificando que uma ideia muito for- te é uma abordagem híbrida: os programas são compilados, de modo a garantir eficiência e otimi- zações, mas ao invés de serem traduzidos para a linguagem da máquina física, eles são traduzidos para um binário conhecido como bytecode, que é especificado por uma máquina virtual. Ela trabalha como uma espécie de interpretador, traduzindo o bytecode para linguagem nativa. Assim, os bytecode compilados são portáveis por serem independentes do sistema real, mas ao mesmo tempo compactos e otimizados por terem passado pelo processo de compilação. Por fim, estudamos que algumas 36 linguagens levam a ideia a um patamar adicional, acrescentando um compilador just-in-time ao interpretador que decide durante a execução se certas partes do bytecode devem ser compiladas para a máquina nativa para ganhar desempenho ou se serão apenas interpretadas mesmo. Referências Bibliográficas & Consultadas BENNETT, J. An introduction to Python bytecode. Opensource.com, 2018. Disponível em: https:// opensource.com/article/18/4/introduction-python- bytecode. Acesso em: 24 de setembro de 2021. CORREA, A. G. D. Programação I. São Paulo: Pearson, 2015. [Biblioteca Virtual]. DEITEL, H. M.; DEITEL, P. J; Java: como programar. 8. ed. São Paulo:Pearson, 2010. [Biblioteca Virtual]. DEITEL, P; DEITEL, H; Java: como programar. 10. ed. São Paulo: Pearson Education do Brasil, 2016. [Biblioteca Virtual]. DOWNEY, A. B. Pense em Python: Pense como um cientista da computação. 2. ed. São Paulo: Editora Novatec, 2016. FELIX, R. (Org.). Programação orientada a objetos. São Paulo: Pearson, 2016. [Biblioteca Virtual]. IRONPYTHON. .NET Integration. IronPython. net. Disponível em: https://ironpython.net/ documentation/dotnet/. Acesso em: 24 de setembro de 2021. JUNEAU, J.; BAKER, J.; NG, V.; SOTO, L.; WIERZBICKI, F. The Definitive Guide to Jython. Readthedocs.io, 2010. Disponível em: https:// jython.readthedocs.io/en/latest/. Acesso em: 24 de setembro de 2021. KLABNIK, S; NICHOLS, C. The Rust Programming Language. No Starch Press. Disponível em https://doc.rust-lang.org/book/. Acesso em: 24 de setembro de 2021. MICROSOFT. Common Language Runtime overview. Microsoft Docs, 2020. Disponível em https://docs.microsoft.com/en-us/dotnet/ standard/clr. Acesso em: 24 de setembro de 2021. LEAL, G. C. L. Linguagem, programação e banco de dados: guia prático de aprendizagem. Curitiba: Intersaberes, 2015. [Biblioteca Virtual]. SANTOS, M. G. dos; SARAIVA, M. de O.; GONÇALVES, P. de F. Linguagem de programação. Porto Alegre: SAGAH, 2018. [Minha Biblioteca]. SEBESTA, R. W. Conceitos de linguagens de programação. 11. ed. Porto Alegre: Bookman, 2018. [Minha Biblioteca]. SILVA, E. A. da. Introdução às linguagens de programação para CLP. São Paulo: Blucher, 2016. [Biblioteca Virtual]. SILVA, F. M. da; LEITE, M. C. D.; OLIVEIRA, D. B. de. Paradigmas de programação. Porto Alegre: SAGAH, 2019. [Minha Biblioteca]. TUCKER, A.; NOONAN, R. Linguagens de programação: princípios e paradigmas. 2. ed. Porto Alegre: AMGH, 2014. [Minha Biblioteca]. Introdução Compilação Interpretação Processos híbridos Considerações finais Referências Bibliográficas & Consultadas