BEAM VM The good, the bad and the ugly

Cherry Ramatis - May 20 - - Dev Community

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

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:

ERTS Stack For Erlang

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


Enter fullscreen mode Exit fullscreen mode

É 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


Enter fullscreen mode Exit fullscreen mode

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.


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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

May the force be with you 🍒

. . . . . . . . . . . . . . . . . . . . . .