Створення віртуальної машини, натхненної JVM — Рефакторинг (Частина 5)

Вступ

Оскільки наша реалізація віртуальної машини, натхнена JVM, розрослася, включаючи потоки, управління пам'яттю та примітиви синхронізації, ми досягли точки, коли рефакторинг став необхідним. Ця стаття пояснює, чому модульний підхід важливий для віртуальних машин, і як ми реорганізуємо наш код.

pic

Чому рефакторинг?

Оскільки наша реалізація віртуальної машини розрослася, включаючи потоки, управління пам'яттю та примітиви синхронізації, виникло кілька проблем:

  1. Наші файли стали занадто великими та переплутаними, що ускладнює розуміння того, як взаємодіють різні компоненти.
    2.
    # Вступ

Оскільки наша реалізація віртуальної машини, натхнена JVM, розрослася, включаючи потоки, управління пам'яттю та примітиви синхронізації, ми досягли точки, коли рефакторинг став необхідним. Ця стаття пояснює, чому модульний підхід важливий для віртуальних машин, і як ми реорганізуємо наш код.

pic

Чому рефакторинг?

Оскільки наша реалізація віртуальної машини розрослася, включаючи потоки, управління пам'яттю та примітиви синхронізації, виникло кілька проблем:

  1. Наші файли стали занадто великими та переплутаними, що ускладнює розуміння того, як взаємодіють різні компоненти.
  2. Зміни стали більш ризикованими, оскільки модифікації в одній частині можуть призвести до непередбачуваних наслідків в інших місцях.

Вивчення JVM

Архітектура JVM поділена на кілька основних підсистем:

  • Завантажувач класів (Class loader) — відповідає за завантаження, лінкування та ініціалізацію класів
  • Робочі області пам'яті (memory area): керує областю методів, купою, стеком, реєстрами PC і стеками рідних методів
  • Двигун виконання (Execution engine): інтерпретує та виконує байт-код, JIT компілятор та збирач сміття

Хоча наша реалізація є спрощеною, ми можемо навчитися з цього поділу відповідальностей.

Цілі реалізації

Організація модулів

Ми реорганізуємо код у логічні модулі, кожен з яких має чітку відповідальність:

  • core/ — Ініціалізація ядра віртуальної машини та управління життєвим циклом
  • instruction/ — Парсинг та виконання інструкцій
  • memory/ — Управління пам'яттю для купи та локальних змінних
  • synchronization/ — Примітиви синхронізації потоків
  • thread/ — Управління потоками та виконання
  • utils/ — Спільні утиліти, як логування

Цей поділ відповідальностей робить код більш підтримуваним та зрозумілим.

Ми створимо таку структуру:

tiny_vm_05_refactoring/  
├── CMakeLists.txt  
└── src/  
 ├── main.c  
 ├── types.h  
 ├── core/  
 │ ├── vm.c  
 │ └── vm.h  
 ├── instruction/  
 │ ├── instruction.c  
 │ └── instruction.h  
 ├── memory/  
 │ ├── memory.c  
 │ └── memory.h  
 ├── synchronization/  
 │ ├── synchronization.c  
 │ └── synchronization.h  
 ├── thread/  
 │ ├── thread.c  
 │ └── thread.h  
 └── utils/  
 ├── logger.c  
 └── logger.h

Система побудови

Ми також відмовляємося від використання Makefile і використовуватимемо CMakeLists.txt, щоб зробити нашу збірку більш декларативною.

Ми переходимо від Make до CMake з кількох причин:

  • Більш декларативна конфігурація збірки
  • Краща інтеграція з IDE (особливо з CLion)
  • Покращене управління залежностями
  • Кросплатформена сумісність
  • Легша конфігурація варіантів збірки (debug/release)

Рефакторинг

Ми створимо нову папку (проект) tiny_vm_05_refactoring, де ми рефакторимо наш код, а потім будемо використовувати це в наступних ітераціях.

Ми можемо використовувати https://www.jetbrains.com/clion як редактор (це полегшує роботу та налагодження).

CMakeLists

Давайте спростимо спосіб побудови нашого коду спочатку.

# CMakeLists.txt  
cmake_minimum_required(VERSION 3.30)  
project(tiny_vm_05_refactoring C)  

set(CMAKE_C_STANDARD 11)  

add_executable(  
 tiny_vm_05_refactoring  
 src/main.c  
 src/utils/logger.c  
 src/utils/logger.h  
 src/core/vm.c  
 src/core/vm.h  
 src/thread/thread.c  
 src/thread/thread.h  
 src/synchronization/synchronization.c  
 src/synchronization/synchronization.h  
 src/memory/memory.c  
 src/memory/memory.h  
 src/instruction/instruction.c  
 src/instruction/instruction.h  
 src/types.h  
)

Ми плануємо створити кілька виконуваних файлів у майбутніх статтях.

Типи

Ми будемо зберігати типи, які використовуються в багатьох модулях, в types.h, щоб уникнути циклічних залежностей.

// src/types.h  
#ifndef TINY_VM_TYPES_H  
#define TINY_VM_TYPES_H  

#include   

typedef int32_t t_int;  

// Зберігання змінних  
typedef struct Variable {  
 char* name;  
 t_int value;  
} Variable;  

// Виконувана рамка (stack frame)  
typedef struct LocalScope {  
 Variable* variables;  
 int var_count;  
 int var_capacity;  
} LocalScope;  

// Контекст потоку  
typedef struct ThreadContext {  
 LocalScope* local_scope;  
 const char** program;  
 int pc;  
 pthread_t thread;  
 int thread_id;  
 int is_running;  
 struct VM* vm;  
 char* function_name;  
} ThreadContext;  

// Синхронізація  
typedef struct SynchronizationLock {  
 char* name;  
 pthread_mutex_t mutex;  
 int locked; // Для відлагодження  
} SynchronizationLock;  

// Стан віртуальної машини  
typedef struct VM {  
 // Управління потоками  
 ThreadContext* threads;
int thread_count;  
 int thread_capacity;  
 pthread_mutex_t thread_mgmt_lock;  
 int next_thread_id;  

 // Купа пам'яті для спільних змінних  
 Variable* heap;  
 int heap_size;  
 int heap_capacity;  
 pthread_mutex_t heap_mgmt_lock;  

 // Управління мютексами  
 SynchronizationLock* locks;  
 int lock_count;  
 int lock_capacity;  
 pthread_mutex_t lock_mgmt_lock;  
} VM;  

#endif

Головний файл

Ось спрощений файл main.c, який служить точкою входу.

// src/main.c  
#include "utils/logger.h"  
#include "core/vm.h"  

#include   

int main(void) {  
 const char* program[] = {  
 "set a 10",  
 "print a",  
 NULL  
 };  

 print("Запуск TinyVM...");  

 VM* vm = create_vm();  
 start_vm(vm, program);  
 destroy_vm(vm);  

 print("TinyVM завершено.");  
 return 0;  
}

Логування (print з флашем, та ін.)

Ми визначимо багаторазовий “логер”, який надасть нам функцію print, що додає мітку часу, перехід на новий рядок і флашить, щоб ми могли побачити результат негайно.

// src/utils/logger.h  
#ifndef TINY_VM_LOGGER_H  
#define TINY_VM_LOGGER_H  

// Утиліти для логування  
void print(const char *format, ...);  

#endif
// src/utils/logger.c  
#include "logger.h"  

#include   
#include   
#include   
#include   
#include   

void print(const char *format, ...) {  
 struct timespec ts;  
 clock_gettime(CLOCK_REALTIME, &ts);  

 time_t t = ts.tv_sec;  
 struct tm tm;  
 localtime_r(&t, &tm);  

 printf("[%04d-%02d-%02d %02d:%02d:%02d.%06ld] ",  
 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,  
 tm.tm_hour, tm.tm_min, tm.tm_sec, ts.tv_nsec / 1000);  

 va_list args;  
 va_start(args, format);  
 vprintf(format, args);  
 va_end(args);  
 printf("\n");  
 fflush(stdout);  
}

Ця функція print використовується лише для цілей налагодження.

Віртуальна машина

Ми покладемо код віртуальної машини до папки core.
Ми будемо оновлювати ці частини в майбутніх статтях.

// src/core/vm.h  
#ifndef TINY_VM_CORE_VM_H  
#define TINY_VM_CORE_VM_H  

#include "../types.h"  

// Основні функції VM  
VM* create_vm(void);  
void start_vm(VM* vm, const char** program);  
void destroy_vm(VM* vm);  

#endif
// src/core/vm.c  
#include "vm.h"  
#include "../thread/thread.h"  
#include "../memory/memory.h"  

#include   

VM* create_vm() {  
 VM* vm = malloc(sizeof(VM));  

 // Управління потоками  
 vm->thread_capacity = 10;  
 vm->thread_count = 0;  
 vm->threads = malloc(sizeof(ThreadContext) * vm->thread_capacity);  
 vm->next_thread_id = 0;  
 pthread_mutex_init(&vm->thread_mgmt_lock, NULL);  

 // Ініціалізація купи  
 vm->heap_capacity = 10;  
 vm->heap_size = 0;  
 vm->heap = malloc(sizeof(Variable) * vm->heap_capacity);  
 pthread_mutex_init(&vm->heap_mgmt_lock, NULL);  

 // Ініціалізація управління синхронізацією/мютексами  
 vm->lock_capacity = 10;  
 vm->lock_count = 0;  
 vm->locks = malloc(sizeof(SynchronizationLock) * vm->lock_capacity);  
 pthread_mutex_init(&vm->lock_mgmt_lock, NULL);  

 return vm;  
}  

void start_vm(VM* vm, const char** program) {  
 // Створюємо головний потік, що починається з лінії 0  
 create_thread(vm, program, 0);  

 // Чекаємо, поки всі потоки завершать свою роботу  
 for (int i = 0; i < vm->thread_count; i++) {  
 pthread_join(vm->threads[i].thread, NULL);  
 }  
}  

void destroy_vm(VM* vm) {  
 for (int i = 0; i < vm->thread_count; i++) {  
 //if (vm->threads[i].local_scope) {  
 destroy_local_scope(vm->threads[i].local_scope);  
 //}  
 }  

 // Очищення купи  
 for (int i = 0; i < vm->heap_size; i++) {  
 free(vm->heap[i].name);  
 }  
 free(vm->heap);  
 pthread_mutex_destroy(&vm->heap_mgmt_lock);  

 // Очищення мютексів  
 for (int i = 0; i < vm->lock_count; i++) {  
 pthread_mutex_destroy(&vm->locks[i].mutex);  
 free(vm->locks[i].name);  
 }  
 pthread_mutex_destroy(&vm->lock_mgmt_lock);  
 free(vm->locks);  

 free(vm->threads);  
 pthread_mutex_destroy(&vm->thread_mgmt_lock);  
 free(vm);  
}

Парсинг і виконання інструкцій

Наступним логічним етапом для віртуальної машини є парсинг і виконання коду (інструкцій).
Ми будемо оновлювати ці частини в майбутніх статтях.

// src/instruction/instruction.h  
#ifndef TINY_VM_INSTRUCTION_H  
#define TINY_VM_INSTRUCTION_H  

#include "../types.h"  

typedef enum {  
 PRINT, // print   
 SET, // set    
 ADD, // add     
 SLEEP, // sleep   
 THREAD, // thread   
 EXIT, // exit  
 SETSHARED, // setshared    
 LOCK, // lock   
 UNLOCK // unlock   
} InstructionType;  

typedef struct {  
 InstructionType type;  
 char args[3][32];  
} Instruction;  

Instruction parse_instruction(const char* line);  

void execute_instruction(ThreadContext* thread, Instruction* instr);  

#endif
// src/instruction/instruction.c  
#include "instruction.h"  

#include "../thread/thread.h"  
#include "../memory/memory.h"  
#include "../synchronization/synchronization.h"  
#include "../utils/logger.h"  

#include   
#include   
#include   
#include   

Instruction parse_instruction(const char* line) {  
 Instruction instr;  
 memset(&instr, 0, sizeof(Instruction));  

 char cmd[32];  
 sscanf(line, "%s", cmd);  

 if (strcmp(cmd, "print") == 0) {  
 instr.type = PRINT;  
 sscanf(line, "%s %s", cmd, instr.args[0]);  
 }  
 else if (strcmp(cmd, "set") == 0) {  
 instr.type = SET;  
 sscanf(line, "%s %s %s", cmd, instr.args[0], instr.args[1]);  
 }  
 else if (strcmp(cmd, "add") == 0) {  
 instr.type = ADD;  
 sscanf(line, "%s %s %s %s", cmd, instr.args[0], instr.args[1], instr.args[2]);  
 }  
 else if (strcmp(cmd, "sleep") == 0) {  
 instr.type = SLEEP;  
 sscanf(line, "%s %s", cmd, instr.args[0]);  
 }  
 else if (strcmp(cmd, "thread") == 0) {  
 instr.type = THREAD;  
 sscanf(line, "%s %s", cmd, instr.args[0]);  
 }  
 else if (strcmp(cmd, "exit") == 0) {  
 instr.type = EXIT;  
 }  
 else if (strcmp(cmd, "setshared") == 0) {  
 instr.type = SETSHARED;  
 sscanf(line, "%s %s %s", cmd, instr.args[0], instr.args[1]);  
 }  
 else if (strcmp(cmd, "lock") == 0) {  
 instr.type = LOCK;  
 sscanf(line, "%s %s", cmd, instr.args[0]);  
 }  
 else if (strcmp(cmd, "unlock") == 0) {  
 instr.type = UNLOCK;  
 sscanf(line, "%s %s", cmd, instr.args[0]);  
 }  

 return instr;  
}  

void execute_instruction(ThreadContext* thread, Instruction* instr) {  
 switch (instr->type) {  
 case PRINT: {  
 const t_int value = get_value(thread, instr->args[0]);  
 print("[Thread %d] Змінна %s = %d", thread->thread_id, instr->args[0], value);  
 break;  
 }  
 case SET: {  
 Variable* var = get_variable(thread, instr->args[0]);  
 if (var) {  
 var->value = atoi(instr->args[1]);  
 }  
 break;  
 }  
 case ADD: {  
 t_int val1 = get_value(thread, instr->args[1]);  
 t_int val2 = get_value(thread, instr->args[2]);  
 Variable* target = get_variable(thread, instr->args[0]);  
 if (target) {  
 target->value = val1 + val2;  
 }  
 break;  
 }  
 case SLEEP: {  
 usleep(atoi(instr->args[0]) * 1000);  
 break;  
 }  
 case THREAD: {  
 const int start_line = atoi(instr->args[0]);  
 create_thread(thread->vm, thread->program, start_line);  
 break;  
 }  
 case EXIT: {  
 thread->is_running = 0;  
 break;  
 }  
 case SETSHARED: {  
 Variable* var = get_shared_variable(thread, instr->args[0]);  
 if (var) {  
 var->value = atoi(instr->args[1]);  
 print("[Thread %d] Set-shared %s = %d", thread->thread_id, var->name, var->value);  
 }  
 break;  
 }  
 case LOCK: {  
 SynchronizationLock* mutex = get_sync_lock(thread->vm, instr->args[0]);  
 if (mutex) {  
 print("[Thread %d] Чекає на замок '%s' за адресою %p",  
 thread->thread_id,  
 mutex->name,  
 (void*)&mutex->mutex  
 );  
 pthread_mutex_lock(&mutex->mutex);  
 mutex->locked = 1;  
 print("[Thread %d] Отримано замок '%s'", thread->thread_id, mutex->name);  
 }  
 break;  
 }  
 case UNLOCK: {  
 SynchronizationLock* mutex = get_sync_lock(thread->vm, instr->args[0]);  
 if (mutex && mutex->locked) {
pthread_mutex_unlock(&mutex->mutex);  
 mutex->locked = 0;  
 print("[Thread %d] Звільнено замок '%s' за адресою %p",  
 thread->thread_id,  
 mutex->name,  
 (void*)&mutex->mutex  
 );  
 }  
 break;  
 }  
 }  
}

Пам'ять

Моделі локальної області видимості та пам'яті купи розташовані у папці memory.

// src/memory/memory.h  
#ifndef TINY_VM_MEMORY_H  
#define TINY_VM_MEMORY_H  

#include "../types.h"  

LocalScope* create_local_scope(void);  
void destroy_local_scope(LocalScope* local_scope);  

t_int get_value(ThreadContext* thread, const char* name);  

Variable* get_variable(ThreadContext* thread, const char* name);  
Variable* get_shared_variable(ThreadContext* thread, const char* name);  

#endif
// src/memory/memory.c  
#include "memory.h"  
#include "../thread/thread.h"  
#include "../utils/logger.h"  

#include   
#include   

LocalScope* create_local_scope() {  
 LocalScope* local_scope = malloc(sizeof(LocalScope));  
 local_scope->var_capacity = 10;  
 local_scope->var_count = 0;  
 local_scope->variables = malloc(sizeof(Variable) * local_scope->var_capacity);  
 return local_scope;  
}  

void destroy_local_scope(LocalScope* local_scope) {  
 for (int i = 0; i < local_scope->var_count; i++) {  
 free(local_scope->variables[i].name);  
 }  
 free(local_scope->variables);  
 free(local_scope);  
}  

Variable* get_variable(ThreadContext* thread, const char* name) {  
 LocalScope* local_scope = thread->local_scope;  
 for (int i = 0; i < local_scope->var_count; i++) {  
 if (strcmp(local_scope->variables[i].name, name) == 0) {  
 return &local_scope->variables[i];  
 }  
 }  

 if (local_scope->var_count < local_scope->var_capacity) {  
 Variable* var = &local_scope->variables[local_scope->var_count++];  
 var->name = strdup(name);  
 var->value = 0;  
 return var;  
 }  
 return NULL;  
}  

// Отримати спільну змінну з купи  
Variable* get_shared_variable(ThreadContext* thread, const char* name) {  

 // Шукаємо існуючу змінну  
 for (int i = 0; i < thread->vm->heap_size; i++) {  
 if (strcmp(thread->vm->heap[i].name, name) == 0) {  
 print("[Thread %d] Знайдена спільна змінна %s", thread->thread_id, name);  
 return &thread->vm->heap[i];  
 }  
 }  

 // Створюємо нову змінну, якщо не знайдена  
 pthread_mutex_lock(&thread->vm->heap_mgmt_lock);  
 if (thread->vm->heap_size < thread->vm->heap_capacity) {  
 Variable* var = &thread->vm->heap[thread->vm->heap_size++];  
 var->name = strdup(name);  
 var->value = 0; // Ініціалізуємо як int за замовчуванням  
 pthread_mutex_unlock(&thread->vm->heap_mgmt_lock);  
 print("[Thread %d] Створена спільна змінна %s", thread->thread_id, name);  
 return var;  
 }  

 return NULL;  
}  

t_int get_value(ThreadContext* thread, const char* name) {  
 for (int i = 0; i < thread->local_scope->var_count; i++) {  
 if (strcmp(thread->local_scope->variables[i].name, name) == 0) {  
 return thread->local_scope->variables[i].value;  
 }  
 }  
 Variable* shared = get_shared_variable(thread, name);  
 if (shared) {  
 return shared->value;  
 }  
 return 0;  
}

Управління потоками

Тепер ми виділяємо управління потоками в окремі файли, ми будемо оновлювати їх у майбутніх статтях.

// src/thread/thread.h  
#ifndef TINY_VM_THREAD_H  
#define TINY_VM_THREAD_H  

#include "../types.h"  

ThreadContext* create_thread(VM* vm, const char** program, int start_line);  

void* execute_thread_instructions(void* arg);  

#endif
// src/thread/thread.c  
#include "thread.h"  
#include "../core/vm.h"  
#include "../instruction/instruction.h"  
#include "../utils/logger.h"  
#include "../memory/memory.h"  

ThreadContext* create_thread(VM* vm, const char** program, int start_line) {  
 pthread_mutex_lock(&vm->thread_mgmt_lock);  

 if (vm->thread_count >= vm->thread_capacity) {  
 pthread_mutex_unlock(&vm->thread_mgmt_lock);  
 return NULL;  
 }  

 ThreadContext* thread = &vm->threads[vm->thread_count++];
текст перекладу

thread->localscope = createlocalscope();
thread->program = program;
thread->pc = start
line;
thread->isrunning = 1;
thread->thread
id = vm->nextthreadid++; // Призначення та збільшення ідентифікатора потоку
thread->vm = vm;

pthreadcreate(&thread->thread, NULL, executethread_instructions, thread);

pthreadmutexunlock(&vm->threadmgmtlock);
return thread;
}

void* executethreadinstructions(void* arg) {
ThreadContext* thread = (ThreadContext*) arg;
print("[Потік %d] Інструкції потоку почали виконуватись", thread->thread_id);

while (thread->is_running) {
const char* line = thread->program[thread->pc];
if (line == NULL) break;

Instruction instr = parseinstruction(line);
execute
instruction(thread, &instr);

thread->pc++;
}

print("[Потік %d] Інструкції потоку завершені", thread->thread_id);
return NULL;
}
```

Синхронізація потоків (Thread synchronization)

Останнім логічним сегментом є синхронізація.
текст перекладу
```
// src/synchronization/synchronization.h

ifndef TINYVMSYNCHRONIZATION_H

define TINYVMSYNCHRONIZATION_H

include "../types.h"

SynchronizationLock* getsynclock(VM* vm, const char* name);

endif


// src/synchronization/synchronization.c

include "synchronization.h"

include

include

// Отримання або створення м'ютексу
SynchronizationLock* getsynclock(VM* vm, const char* name) {

// Пошук існуючого м'ютексу
for (int i = 0; i < vm->lock_count; i++) {
if (strcmp(vm->locks[i].name, name) == 0) {
return &vm->locks[i];
}
}

// Створення нового м'ютексу, якщо не знайдений
if (vm->lockcount < vm->lockcapacity) {
pthreadmutexlock(&vm->lockmgmtlock);

SynchronizationLock* mutex = &vm->locks[vm->lockcount++];
mutex->name = strdup(name);
pthread
mutex_init(&mutex->mutex, NULL);
mutex->locked = 0;

pthreadmutexunlock(&vm->lockmgmtlock);
return mutex;
}
return NULL;
}
```

Комплексний вихідний код для цієї статті доступний у директорії tiny-vm05refactoring репозиторію TinyVM.

Наступні кроки

Ми готові додати функції до нашого коду, щоб ми могли повторно використовувати та виконувати код як синхронно (в тому ж потоці, де викликається функція), так і асинхронно (в новому потоці).

  1. Вступ
  2. Частина 1 — Основи
  3. Частина 2 — Багатопотоковість
  4. Частина 3 — Купа
  5. Частина 4 — Синхронізація
  6. Частина 5 — Рефакторинг (ви тут)
  7. Частина 6 — Функції (скоро)
  8. Частина 7 — Компіляція (скоро)
  9. Частина 8 — Виконання байт-коду (скоро)
  10. Частина 9 — Стек викликів функцій (не розпочато)
  11. Частина 10 — Збірка сміття (не розпочато)

Перекладено з: Building a Virtual Machine, JVM-inspired — Refactoring (Part 5)

Leave a Reply

Your email address will not be published. Required fields are marked *