BEAM VM The good, the bad and the ugly
Tags: elixir, erlang, braziliandevs, beginners
Published: 2024-05-20 22:14:04

A `BEAM` é uma VM que está sendo muito falada ultimamente com o advento de novas tecnologias como [gleam](https://gleam.run) e [riot](https://riot.ml/) 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](https://www.erlang.org/about) em **1986** para a linguagem [Erlang](https://www.erlang.org/), 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](https://github.com/josevalim), que criou a linguagem [Elixir](https://elixir-lang.org/) 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](#o-que-é-a-beam)
- [Stack machine vs Register machine](#stack-machine-vs-register-machine)
- [O que significa ser simultâneo por padrão](#o-que-significa-ser-simultâneo-por-padrão)
- [Um caso interessante para fazer nodes interagirem pela rede](#um-caso-interessante-para-fazer-nodes-interagirem-pela-rede)
- [Conclusão](#conclusão)

## O que é a BEAM

A `BEAM` é uma das partes que compõem o sistema [OTP](https://en.wikipedia.org/wiki/Open_Telecom_Platform) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ko71tgwqnxzx2zo14jls.png)

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 👇:

```asm
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:

```asm
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:

```asm
{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**

```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**

```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](https://www.youtube.com/@Honeypotio) 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:

```elixir
🍒 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:

```elixir
🍒 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:

```elixir
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](https://blog.stenmans.org/theBeamBook)
- [Elixir compilation stages](https://elixirforum.com/t/getting-each-stage-of-elixirs-compilation-all-the-way-to-the-beam-bytecode/1873)
- [Tsoding: I made a new programming language](https://www.youtube.com/watch?v=6k_sR6yCvps)
- [Elixir: The documentary](https://www.youtube.com/watch?v=lxYFOM3UJzo)
- [OpenErlang: Implementing languages on the BEAM](https://www.youtube.com/watch?v=0MTiqI9v-ck)

May the force be with you 🍒