VM

ВСТУП

Тут хочеться розказати про віртуальні машини які намагаються бути сумісними з семантикою та байткодом віртуальної машини BEAM яка є основою для платформи Erlang/OTP, де Erlang — це специфікація на мову програмування в цій платформі яка компілює в BEAM байткод з високорівневих репрезентацій Erlang AST (Elixir) або Erlang Core (Joxa, Hamler), а OTP це набір бібліотек разом з кореневими бібліотеками kernel (бібліотека середовища виконання) та stdlib (базова бібліотека з алгоритмами та структурами даних).

Протягом десятиліть аматори та малі компанії намагаються побудувати сумісну з BEAM віртуальну машину, а деякі навіть зі своїми мовами. Розкажемо про основні спроби імплементації, які в необхідній мірі реалізують можливості Erlang.

LING (Erlang On Xen)

Перша віртуальна машина LING від компанії Cloudozer авторства Максима Харченко, яка викорстовувалась в SDN свічі LINCX компанії Infoblox. Основним критерієм оптимізації при побудові цієї машини був час її завантаження під гіпервізором Xen (сама машина працювала без операційної системи, але пізніше був зроблений порт у вигляді звичайного POSIX додатку. Ця віртуальна машина завантажується за 50мс. Був також порт на STM мікроконтролери.

AtomVM (Microcontrollers)

У той час як LING був зосереджений та розроблювався для еластичних cloud-платформ з використанням гіпервізора Xen, головною мотивацією AtomVM є розповсюдження платформи Erlang на ринок мікроконтролерів ESP, STM, FreeRTOS.

LAM (Little Abstract Machine)

Леандро Остера один з небагатьох авторів мов програмування для платформи Erlang який робить все правильно. По-перше на відміну від інших авторів варіантів Standard ML, він вибрав у якості компілятора промисловий OCaml і почав створювати для нього бекенд в Erlang, тас само як Hamler є портом PureScript з Erlang бекендом компіляції в Erlang Core. Так з'явилася мова Caramel і леандро пішов ще далі, він захотів миву з цільовою платформою WebAssembly, що природнім чином привело його в Rust, а з огляду на високу культуру OCaml екосистеми це дало в поєднанні дуже якісний продукт — віртуальну машину LAM як цільову мову для Caramel. Ми як N2O PRO компанія не фіксуємося на конкретних мовах, для нас основним чином LAM цікава як ідіоматична реалізація семантики Erlang на мові Rust, настільки (ідіоматична), що не виникає бажання писати свою версію. Ця версія дійсно пронизана глибоким розуміння BEAM. Наприклад, Леандро Остера як і Максим Харченко прийшов до висновку що для власної віртуальної машини більше підходить свій, більш оптимізовний байткод. Процеc компіляції у цьому випадку замінюється процесом трансляції вже скомпільованого коду erlc компілятором з потужним паттерн-мачінгом в свій більш оптимізовний код для розмірів кешів першого рівня на кристалі мікропроцесора. Розмір кодової бази віртуальної машини — 1700 LOC, а розмір усієї інфраструктури разом з транслятором та лінкером (які можна переписати на Erlang і тримати наприклад як плагіни MAD) складає вього 4000 LOC. Однак із-за crate залежностей розмір native WebAssembly файлів для POSIX середовища займають 35МВ. Це проблема яку потрібно вирішити, для нас не так важлива екосистема WebAssembly як розмір інстанса в пам'яті, адже ціни на пам'ять не падають надто швидко, особливо на ту память для якої ми орієнтуємося — L1 кеш процесора.

КООРДИНАТОР

pub struct Coordinator { program: Program, scheduler_manager: Box<dyn SchedulerManager>, scheduler_count: u32, } pub trait SchedulerManager { fn setup(&mut self, cpus: u32, prog: &Program) -> Result<(), Error>; fn run(&self, coordinator: &Coordinator) -> Result<(), Error>; } pub trait Runtime { fn execute(&mut self, call: &MFA, args: &[Literal]) -> Literal; fn sleep(&self, _duration: u64) {} fn halt(&self) {} fn r#yield(&self); }

ПЛАНУВАЛЬНИК

pub struct Scheduler { id: u32, reduction_count: u64, process_registry: ProcessRegistry, process_queue: ProcessQueue, program: Program, } pub struct ProcessQueue { ready: VecDeque<Pid>, dead: VecDeque<Pid>, } pub struct ProcessRegistry { process_count: u64, processes: HashMap<Pid, Rc<Process>>, }

ПРОЦЕС

pub struct Process { status: RefCell<Status>, pid: Pid, emulator: Emulator, pub mailbox: Mailbox, } pub struct Emulator { registers: RefCell<Registers>, instr_ptr: RefCell<InstructionPointer>, }

ІНТЕРПРЕТАТОР

pub struct Program { pub main: MFA, pub modules: HashMap<String, Module>, } pub struct MFA { pub module: String, pub function: String, pub arity: u32, } pub struct Module { pub name: String, pub lambdas: HashMap<(String, Arity), Label>, pub functions: HashMap<(String, Arity), Label>, pub labels: Vec<FunctionLabel>, } pub struct FunctionLabel { pub id: Label, pub instructions: Vec<Instruction>, } pub struct Registers { global: Vec<Value>, local: VecDeque<Vec<Value>>, current_local: Vec<Value>, }

СКРИНЬКА

pub struct Mailbox { messages: RefCell<VecDeque<Message>>, current: RefCell<u32>, }

БАЙТКОД

Halt, Move(Value, Register), Swap(Register, Register), Clear(Register), Allocate(Words,Keep), Deallocate(Words), ShiftLocals(Amount), RestoreLocals, Label(Label), Jump(Label), Return, Test(Label, Test), ConditionalJump(reg,er,tab), Badmatch, Call(FnCall, FnKind), TailCall(FnCall, FnKind), MakeLambda(Fst,Mod,Ar,Env), ConsList(H,T,target), SplitList(list,head,tail), SplitListTail(l,t), SplitListHead(l,h), MakeTuple(target,elems), GetTupleElement(tupl,elems,target), GetMapElements(lab,map,elems), Spawn(Spawn), Sleep(Label), Kill, Monitor, Send(msg,proc), PeekMessage(onemp,msg), RemoveMessage, PidSelf(Register),