E4_PALP

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 40

PARADIGMAS DE

LINGUAGENS DE
PROGRAMAÇÃO
MSC Rafael De Moura Moreira

e-Book 4
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
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!

3
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.

4
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.

5
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

6
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

7
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 –

8
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 em linguagem 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.

9
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

10
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

11
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

12
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

13
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,

14
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

15
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ês entidades 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

16
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

17
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

18
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.

19
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-

20
-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.

21
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

22
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-

23
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.

24
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

25
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,

26
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

27
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-

28
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.

29
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.

SAIBA MAIS
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

30
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

31
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.

32
FIQUE ATENTO

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.

33
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

34
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

35
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.

36
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].

Você também pode gostar