Olá, meu nome é Lucas Wasilewski e assim como coloquei na descrição do projeto no meu github desde que eu comecei a programar com o NodeJS (lá no início de 2021) eu sempre quis escrever algo que parecesse com a ferramenta, isso só aumentou depois que eu assisti o documentário sobre o projeto e fiquei de boca aberta em como o mundo open source pode ter várias reviravoltas e se mostrar muito acolhedor quando quer. E depois de uma semana de muita bateção de cabeça resolvi escrever esse artigo para que assim os futuros loucos programadores que quiserem esse desafio não cometam os mesmos erros que eu.
Runtimes de Javascript
Esse termo pode facilmente enganar qualquer pessoa que não entenda muito do assunto e então é preciso de uma boa definição:
Uma runtime de Javascript é uma ferramenta que permite executar a linguagem fora do navegador
Hoje em dia existem 3 runtimes populares: NodeJS, Deno (Node Killer) e o Bun (Deno Killer), mas elas basicamente fazem a mesma coisa: permitem você usar o javascript longe do navegador e usam de outras bibliotecas para criar funcionalidades novas, e isso é muito bom, uma vez que você pode usar qualquer uma delas para subir um servidor, criar bibliotecas e até mesmo aplicações mobile ou de terminal.
Ambos Node e Deno foram criados pela mesma pessoa: Ryan Dahl, e ele lá em 2009 criou a ferramenta para possibilitar que os desenvolvedores pudessem criar aplicações "async IO", ou seja, que não bloqueassem a thread principal mas ainda continuassem respondendo aos pedidos, com isso em mente ele criou a Libuv uma biblioteca que faz justamente isso. Até ai o projeto era apenas um grande amontoado de C e se ele quisesse mas pessoas usando a ferramenta ele precisava de alguma linguagem mais fácil de entender e usar, coincidentemente, nessa mesma época o Google lança a V8, que de um modo geral é um compilador ultra rápido de javascript, isso fez ele unir os dois e assim criar o Node.
Algum bom tempo depois (9 anos mais especificamente), Ryan saí do projeto e vai trabalhar em outras coisas que considerava mais interessante, isso fez ele perceber alguns vários erros que podiam ser corrigidos no Node, mas a comunidade já estava muito grande e ter que dar um grande passo para trás era impossível, então, determinado a fazer um trabalho melhor ele cria o Deno, outra runtime IO que promete ser muito superior ao Node, até a data de hoje (2024) o Deno está na versão 2.0 e está bem estável para projetos e comunidade.
Toda essa história fez mais pessoas entrarem na comunidade das runtimes e isso também nos levou a criação do Bun, e muito melhor, da minha e da sua runtime! Agora vamos ao que interessa.
Compilando a V8
Como dito anteriormente a V8 é o motor do Node, por isso vamos ter que realmente baixá-la e compilar manualmente para assim ter acesso as suas bibliotecas e headers. Por ser um projeto do Google eles tem os próprios métodos de baixar e compilar, então pra isso vamos ter que seguir o manual deles: link, apenas copiar e colar vai te levar ao comando finais.
Porém, aqui eu cometi um erro que demorou 3 dias para me tocar que estava fazendo tudo de errado. Após gerar os arquivos de configuração de build com:
tools/dev/v8gen.py x64.release
Você precisar tomar muito cuidado com o arquivo args.gn
dentro da pasta out.gn/x64.release/
porque nele tem a configuração de build que o ninja (ferramenta de compilação) vai utilizar para gerar os arquivos da biblioteca, alguns tutoriais antigos usam o parâmetro v8_monolithic = true
, mas nas versões recentes isso não é mais utilizado. De acordo com esse comentário do StackOverflow, nós precisamos agora usar o parâmetro is_component_build = true
para gerar os arquivos certos e modificar as flags ao compilar o arquivo, algo muito bobo que se você não prestar atenção pode gastar um tempo precioso.
Após colocar o resto das flags corretamente, apenas precisamos rodar o comando para compilar o projeto
ninja -C out.gn/x64.release
Enquanto isso vá comer alguma coisa, porque a V8 é um projeto bem extenso e com inúmeros testes, dependendo da sua máquina esse processo pode facilmente levar 1 hora ou mais, então deixe rodando e continue lendo.
Cadê o console.log?
Após compilar você pode dar uma olhada no v8/samples/hello-world.cc
e já começar a ter uma ideia de como compilar javascript, mas especificamente essas linhas:
v8::Local<v8::String> source =
v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");
// Compile the source code.
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
Vá em frente e dê uma brincada com a string que contêm o "Hello World", crie funções, loops, condicionais e se veja maluco ao perceber que se você incluir o clássico console.log()
vai receber um undefined
, isso primeiramente me deixou perplexo, eu sempre achei que o objeto console fazia parte da própria V8, mas na verdade quem o inclui é o próprio Node e os navegadores o incluem como parte do DOM (post de 2012 falando que o console.log provavelmente não é suportado pelos navegadores), ou seja, vamos ter que criá-lo nós mesmos.
Olá, Mundo!
Para conseguir criar nossas próprias funções primeiro precisamos entender que a V8 trabalha com vários escopos, um deles é o de contexto, onde é através dele que a runtime sabe onde e como executar o script de forma individual, dentro dele pode haver um objeto global que é compartilhado entre todos os outros, e é dentro dele que vamos inserir nossas funções customizadas.
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(GetIsolate());
global->Set(GetIsolate(), "print", v8::FunctionTemplate::New(GetIsolate(), this->Print));
v8::Local<v8::Context> context = v8::Context::New(GetIsolate(), nullptr, global);
Com essas linhas conseguimos criar um objeto que se chama global
, inserimos um template de função "print" que ao ser executado chama a função Print
.
static void Print(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::HandleScope handle_scope(info.GetIsolate());
for(int i = 0; i < info.Length(); i++) {
if(i != 0) printf(" ");
v8::String::Utf8Value str(info.GetIsolate(), info[i]);
const char *c = ToCString(str);
printf("%s", c);
}
printf("\n");
fflush(stdout);
}
A função Print recebe esse parâmetro maluco que contém informações sobre a chamada da função dentro do javascript e é através dele que iteramos sobre todos os itens dentro dela, os transformamos em uma string C e printamos na tela, bem direta, bem simples e que cumpre com seu papel, isso já é o suficiente para colocar em um arquivo, lê-lo e jogar para a V8 (deixo essa em suas mãos).
// test.js
print("Olá, mundo!")
Libuv
Bom, espero que até aqui você esteja conseguindo acompanhar e até tenha parado de ler para fazer algumas implementações únicas para o seu Node caseiro, porém a V8 só vai nós levar até certo ponto, para conseguirmos chegar mais perto de uma runtime profissional precisamos fazer com que o javascript consiga realizar mais operações, para tal vamos utilizar da Libuv que foi criada justamente para isso.
Você pode achar o tutorial para instalar e compilar aqui. O importante a se notar aqui é que ela nos dá a liberdade de fazer operações assíncronas, ou seja, sem bloquear a thread principal, permitindo assim que o programa continue executando enquando faz um trabalha mais pesado (como abrir um arquivo ou esperar por requisições em um socket de servidor).
Ela mesma já vem embutido com a funcionalidade de criar um servidor http, então só precisamos sincronizá-la com as chamadas da V8. Não se engane, isso não é uma tarefa fácil, até porque a interface das duas bibliotecas se diferem bastante então é difícil conectar ambas, mas sempre há um jeito e o código fonte do node é aberto então certifique-se de roubar algumas ideias de lá
Conclusões
Chegamos ao final de mais um artigo e com eles vamos a alguns detalhes que eu percebi durante a implementação. A primeira com certeza é a complexidade, claro, não é um projeto simples, mas depois que você entender como interagir com a interface da V8 as coisas andam bem rápido.
Esse projeto me fez entender o Node muito melhor também. A questão que a runtime é apenas um conglomerado de bibliotecas se comunicando faz ser muito fácil entender como as coisas mais complexas (como o "event-loop") funcionam.
Caso você queira olhar o que eu fiz de certo, ou provavelmente de muito errado, por favor dê uma olhada no projeto pelo github: done
Talk is cheap, show me the code - Linus Torvalds
## Refêrencias
https://github.com/libuv/libuv
https://v8.dev/docs
https://stackoverflow.com/questions/71213580/cant-get-v8-monolith-to-genorate
https://github.com/ErickWendel/myownnode
https://github.com/WasixXD/done