A BEAM
é uma VM que está sendo muito falada ultimamente com o advento de novas tecnologias como gleam e riot a utilizando como target de compilação ou mesmo recriando algumas de suas funcionalidades. A parte legal é que essa VM é beeeem antiga, sendo criada pela Ericsson em 1986 para a linguagem Erlang, essas tecnologias foram as principais protagonistas para aplicações em ambiente telecom lidando com incontáveis requests por segundo de forma confiável e performática.
Algumas décadas depois, essa VM foi considerada para a web pela primeira vez por José Valim, que criou a linguagem Elixir em meados de 2011 para utilizar todo o poder da BEAM
em uma linguagem moderna e com foco na experiência de desenvolvimento. Essa linguagem foi também a responsável pela popularização da máquina virtual e provavelmente você não estaria lendo este artigo se não fosse por ela, não é mesmo? 👀
Mas afinal...Por que essa maquina virtual foi criada? Quais suas vantagens, poderes e o mais importante: quais seus segredos? Nesse artigo vamos nos aventurar brevemente nos detalhes dessa VM, vem que vem!
Table of contents
- O que é a BEAM
- Stack machine vs Register machine
- O que significa ser simultâneo por padrão
- Um caso interessante para fazer nodes interagirem pela rede
- Conclusão
O que é a BEAM
A BEAM
é uma das partes que compõem o sistema OTP e atua semelhante a como a JVM funciona para o java, seus objetivos principais são performance, concorrência, tolerância a erros e o uso de threads leves para executar computações de forma concorrente o máximo possível.
Interagir com essa VM pode ser feito de 2 formas principais:
- 1. Compilar uma linguagem X para Erlang e utilizar o próprio compilador do Erlang para gerar o bytecode
*.beam
(Como é o caso da linguagem gleam que possui Erlang como um de seus targets) - 2. Compilar uma linguagem X para o bytecode
*.beam
de forma direta (Como é o caso do elixir)
Apesar de ser muito mencionada como a ultima layer de execução, a BEAM
é apenas um dos componentes que compõem todo processo de execução de uma aplicação Erlang/Elixir/Gleam, no diagrama abaixo podemos ver exatamente em que ponto a VM se encaixa:
Como é possível observar, a BEAM roda dentro do runtime Erlang (ERTS) e consequentemente todo o código da nossa aplicação rodam como um node*
independente na VM.
node* pode ser entendido de maneira breve como uma thread do sistema operacional. Cada node pode ter milhares de processos rodando ao mesmo tempo e esses processos são threads leves que imitam o comportamento de uma nativa do sistema operacional possuindo as seguintes capacidades: comunicar entre si por meio de signals, executar computações e iniciar outros processos.
Stack machine vs Register machine
Bom, eu poderia simplesmente te dizer que a BEAM é uma maquina de registros com capacidades de uma maquina de stack, mas isso não seria muito útil né? Então vamos voltar do começo e entender o que é cada coisa:
O que é uma stack machine
Podemos falar que uma stack machine é a forma mais comum de se criar um interpretador, justamente por ter um modelo mental simples de compreender e pelo fato do código final gerado ser super simples de compreender por humanos, pense nesse modelo como uma fila FIFO onde temos 2 ações principais:
- 1.
push
: Insere um valor na fila - 2.
pop
: Tira um valor da fila
Mas...só isso? SIM!! Com esse conceito podemos traduzir a expressão exemplo 2 + 2
como se fosse interpretada por uma stack machine 👇:
push 2
push 2
add
É eu sei, tem uma palavra add
ali né? Mas ela está completamente alinhada com o conceito de stack não se preocupe, nesse caso o add
representa uma chamada de uma função e tudo o que ela faz é remover os argumentos da pilha (utilizando pop
) e depois adicionar o resultado da operação de volta a fila (utilizando push
), abaixo podemos ver em mais detalhes esse processo:
push 2 #=> Adiciona 2 a stack
push 2 #=> Adiciona 2 a stack
add #=> detalhe da execução abaixo
pop #=> Remove argumento da stack
pop #=> Remove argumento da stack
push 4 #=> Adiciona o resultado 4 a stack
O que é uma register machine
Diferente da stack machine, uma register machine usa de registros para armazenar tanto valores de argumentos quanto seus resultados e nenhum desses valores é removido justamente por ter a capacidade de utilizar múltiplos registros para diferentes coisas (a BEAM utiliza um registro X0
para guardar resultados de computações).
Por ter a capacidade de trabalhar com mais registradores, uma register machine possui mais operadores da manipulação como move
, swap
e também formas de acessar o valor de um registrador de forma específica como podemos ver em mais detalhes abaixo:
{function, add, 2, 2}.
{label,1}.
{line,[{location,"add.erl",4}]}.
{func_info,{atom,add},{atom,add},2}.
{label,2}.
{allocate,1,2}. #=> Prepara um lugar na memoria para eventualmente armazenar os valores
{move,{x,1},{y,0}}. #=> Copia o argumento da função (salvo automaticamente no registrador x) para um registrador temporario y
{call,1,{f,4}}.
{swap,{y,0},{x,0}}. #=> Inverte os valores de cada registrador
{call,1,{f,4}}.
{gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}. #=> Soma os numeros nos registradores x e y, sobrescrevendo então o registrador x com o resultado
{deallocate,1}.
return.
PS: Esse artigo não se propõe em te ensinar exatamente como funcionam registradores e muito menos assembly (até porque nem eu sei tanto sobre 😅), mas sim prover uma visão geral de como esse modelo se comporta.
E o que é a BEAM afinal?
Agora acredito que podemos entender a frase inicial certo? A BEAM é uma register machine que possui stacks em registradores específicos (no caso o Y
), pois dessa forma se torna mais simples transitar valores de argumentos por exemplo.
O que significa ser simultâneo por padrão
Se você leu o suficiente sobre elixir ou alguma outra linguagem do ecossistema já deve ter ouvido falar que a BEAM é simultânea por padrão, mas afinal o que isso quer dizer? Em suma quer dizer que as linguagens da BEAM fornecem formas nativas e inteligentes de lidar com computações concorrentes.
Como vimos anteriormente, toda nossa aplicação roda em um dos nodes da VM e esse node pode ter milhares de processos (como também foi falado sobre termos processos leves) executando computações simultaneamente. Esses processos são nossa principal forma de garantir simultaneidade durante a nossa aplicação, nativamente temos funções como spawn
para iniciar novos processos e utilizando essa função primária temos abstrações inteiras como GenServer
e Task
que possibilitam o uso de processos em contextos específicos.
E não é só de iniciar novos processos que uma aplicação simultânea vive não é mesmo? As linguagens da BEAM também fornecem construtores nativos para lidar com troca de mensagens entre processos por meio de mailboxes. Essas mailboxes são blocos que funcionam como um switch case
, cada branch pode ser correspondida utilizando padrões como mostrado abaixo 👇:
Em elixir
iex(1)> defmodule Listener do
...(1)> def call do
...(1)> receive do
...(1)> {:hello, msg} -> IO.puts("Received inside the mail: #{msg}")
...(1)> end
...(1)> end
...(1)> end
{:module, Listener,
<<70, 79, 82, 49, 0, 0, 6, 116, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 240,
0, 0, 0, 25, 15, 69, 108, 105, 120, 105, 114, 46, 76, 105, 115, 116, 101,
110, 101, 114, 8, 95, 95, 105, 110, 102, 111, ...>>, {:call, 0}}
iex(2)> pid = spawn(&Listener.call/0)
#PID<0.115.0>
iex(3)> send(pid, {:hello, "Hello World"})
Received inside the mail: Hello World
{:hello, "Hello World"}
iex(4)>
Em erlang
1> c("mailbox.erl").
{ok,mailbox}
2> mailbox:module_info().
[{module,mailbox},
{exports,[{call,0},{module_info,0},{module_info,1}]},
{attributes,[{vsn,[330096396114390727100476047769825248960]}]},
{compile,[{version,"8.4.3"},
{options,[]},
{source,"/private/tmp/NKI90h/mailbox.erl"}]},
{md5,<<248,86,64,221,149,120,150,9,30,225,159,226,217,
253,6,192>>}]
3> Pid = spawn(fun mailbox:call/0).
<0.93.0>
4> Pid ! {hello, "Hello World"}.
Received inside the mail: "Hello World"
{hello,"Hello World"}
5>
Com isso temos todas as ferramentas necessárias para entender como as linguagens da BEAM fornecem simultaneidade por padrão apenas por definir suas abstrações de forma concorrente desde o ponto inicial, legal né?!
Dica bonus: Tenho uma série de artigos falando sobre essas abstrações na linguagem Elixir: https://dev.to/cherryramatis/handling-state-between-multiple-instances-with-elixir-4jm1
Um caso interessante para fazer nodes interagirem pela rede
Vamos relembrar algumas coisas legais por aqui, primeiro sabemos que ao iniciarmos uma nova aplicação Erlang/Elixir a mesma executa em um node da BEAM, e em segundo sabemos que temos construtores nativos da linguagem para manipular processos que executam dentro desse node e podem se comunicar por troca de mensagem certo?
Agora e se eu te dissesse que os nodes também podem se comunicar? Isso mesmo! A BEAM é realmente a tecnologia perfeita para software distribuído, em um famoso video do Honeypot o José Valim explica essa interação que eu vou descrever aqui embaixo, o video pode ser visto em: https://www.youtube.com/watch?v=lxYFOM3UJzo a partir do minuto 4:41
Para fazer jus ao José Valim vamos realizar o exemplo em elixir da mesma forma que ele fez no video, mas é possível fazer com Erlang da mesma forma.
Para iniciar sistema elixir em um node da BEAM vamos iniciar o REPL interativo com um nome de host e vamos definir um modulo exemplo que printa "Hello world" na tela:
🍒 iex --name cherry
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.17.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(cherry@Cherrys-Laptop.local)1> defmodule Hello do
...(cherry@Cherrys-Laptop.local)1> def world do
...(cherry@Cherrys-Laptop.local)1> IO.puts("Hello World")
...(cherry@Cherrys-Laptop.local)1> end
...(cherry@Cherrys-Laptop.local)1> end
{:module, Hello,
<<70, 79, 82, 49, 0, 0, 5, 72, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 184,
0, 0, 0, 19, 12, 69, 108, 105, 120, 105, 114, 46, 72, 101, 108, 108, 111, 8,
95, 95, 105, 110, 102, 111, 95, 95, 10, ...>>, {:world, 0}}
iex(cherry@Cherrys-Laptop.local)2> Hello.world
Hello World
:ok
iex(cherry@Cherrys-Laptop.local)3>
Em outro terminal, vamos iniciar um novo REPL com outro nome e vamos tentar executar o modulo que foi definido na instancia acima:
🍒 iex --name kalane
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.17.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(kalane@Cherrys-Laptop.local)1> Hello.world
** (UndefinedFunctionError) function Hello.world/0 is undefined (module Hello is not available)
Hello.world()
iex:1: (file)
iex(kalane@Cherrys-Laptop.local)1>
Bom...nada é tão simples né? Na verdade precisamos utilizar o modulo de Node
para tornar essa comunicação possível, vamos executar agora a seguinte linha no mesmo REPL que estávamos:
iex(kalane@Cherrys-Laptop.local)1> Node.spawn(:"cherry@Cherrys-Laptop.local", fn -> Hello.world() end)
Hello World
#PID<13525.123.0>
iex(kalane@Cherrys-Laptop.local)2>
Sua cabeça explodiu? Eu espero que sim porque isso estamos comunicando duas instancias separadas do elixir dentro de uma mesma rede de forma completamente nativa da linguagem (pra que Kubernetes /j).
Com essa tecnologia podemos construir sistemas inteiros de pub sub e notificação sem precisar depender de SaaS dedicados, apenas utilizando as funcionalidades que a VM já entrega.
Conclusão
Nesse artigo tentei ao máximo repassar meus últimos estudos com a BEAM e principalmente repassar meu entusiasmo e momentos de descoberta porque certamente foi uma série constante de "mind blown" a medida que fui lendo e pesquisando.
Meus estudos envolveram principalmente tentar recriar um arquivo .beam
a partir de uma linguagem fictícia que só define funções retornando valores estáticos, o projeto tem uma versão em rust inicial e uma versão WIP em elixir e pode ser encontrado no link: https://github.com/cherryramatisdev/beam_studies
Referências
- The BEAM Book
- Elixir compilation stages
- Tsoding: I made a new programming language
- Elixir: The documentary
- OpenErlang: Implementing languages on the BEAM
May the force be with you 🍒