물에 사는 벌레
바이트 코드 패턴을 이용한 VM 본문
디자인 패턴 카테고리를 만들긴 했는데 디자인 패턴은 설명하기가 좀 난감하네요.
본래 설명을 잘하지 못하는데 디자인 패턴은 스스로 구현하고, 변형, 응용하여 공부하는 것이라고 생각되어 딱히 할 말이 생각나지 않네요.
디자인 패턴은 그냥 메모장 비슷하게 작성할 것 같습니다.
가상 머신과 바이트로 이루어진 가상의 명령어를 만들어 적용시켜 보았습니다.
먼저 명령어를 정의합니다.
4부터 시작하는 이유는 좀 더 크게 만드려고 했으나 귀찮아서 지워버렸기 때문입니다...
#pragma once
enum class VMInstruction
{
VMadd = 0x4,
VMminus = 0x5,
VMmultiply = 0x6,
VMdivide = 0x7,
VMprint = 0x8,
VMprint_enter = 0x9,
VMscan_int = 0x10,
VMscan_string = 0x11,
VMappendString = 0x12,
VMintToString = 0x13,
VMstringToInt = 0x14,
};
enum class VMType : unsigned char
{
VMint = 0x1,
VMstring = 0x2,
};
명령은 사칙연산과 출력, 입력으로 아주 간단하게 구성했습니다.
다음으로 가상 머신 클래스를 정의합니다.
#pragma once
#include "Stack.h"
#include "VMInstruction.h"
#include <iostream>
using namespace std;
#define STR_MAX 255
class VM
{
public:
typedef unsigned char byte;
private:
Stack<byte> stack;
private:
inline bool IsType(VMType type);
public:
void Push(byte *instructions, int size);
void Push(byte instruction);
void Push(VMInstruction instruction);
void PushInt(int _int);
void PushString(const char* str);
private:
int PopInt();
char* PopString();
void Process();
};
#define TypeNotMatchException \
{ \
cout << "VM: type not match exception" << endl; \
cout << "file: " << __FILE__ << endl; \
cout << "line: " << __LINE__ << endl; \
cout << "function: " << __func__ << endl; \
throw exception("VM: type not match"); \
}//
Push 함수로 명령 혹은 데이터를 스택에 저장합니다.
PushInt를 통하여 int 데이터 타입을 4개의 바이트로 분할하여 저장합니다.
PushString을 통하여 문자열 타입을 각 문자로 분할하여 저장합니다.
PopInt를 사용하여 스택에서 4개의 바이트를 꺼내 int 데이터 타입으로 변환 후 반환합니다.
PopString을 통하여 스택에서 문자열 길이만큼의 바이트를 꺼내와 문자열로 변환 후 반환합니다.
Process를 사용하여 명령을 처리합니다.
#include "VM.h"
inline bool VM::IsType(VMType type)
{
return (VMType)stack.Pop() == type;
}
void VM::Push(byte *instructions, int size)
{
for (int i = 0; i < size; i++)
stack.Push(instructions[i]);
}
void VM::Push(byte instruction)
{
stack.Push(instruction);
}
void VM::Push(VMInstruction instruction)
{
stack.Push((byte)instruction);
Process();
}
void VM::PushInt(int _int)
{
byte byte4 = _int;
byte byte3 = _int >> 8;
byte byte2 = _int >> 16;
byte byte1 = _int >> 24;
stack.Push(byte4);
stack.Push(byte3);
stack.Push(byte2);
stack.Push(byte1);
stack.Push((byte)VMType::VMint);
}
void VM::PushString(const char * str)
{
int string_length = strlen(str);
for (int i = string_length; i > 0; i--)
{
stack.Push(str[i - 1]);
}
stack.Push(string_length);
stack.Push((byte)VMType::VMstring);
}
int VM::PopInt()
{
int _int = 0x0;
_int |= stack.Pop();
_int <<= 8;
_int |= stack.Pop();
_int <<= 8;
_int |= stack.Pop();
_int <<= 8;
_int |= stack.Pop();
return _int;
}
char * VM::PopString()
{
int string_length = stack.Pop();
char *str = new char[string_length + 1];
for (int i = 0; i < string_length; i++)str[i] = stack.Pop();
str[string_length] = '\0';
return str;
}
void VM::Process()
{
VMInstruction instruction = (VMInstruction)stack.Pop();
switch (instruction)
{
case VMInstruction::VMprint: {
VMType vm_type = (VMType)stack.Pop();
switch (vm_type)
{
case VMType::VMint:
cout << PopInt();
break;
case VMType::VMstring:
char *str = PopString();
cout << str;
delete[] str;
break;
}
}break;
case VMInstruction::VMprint_enter: {
cout << endl;
}break;
case VMInstruction::VMscan_int: {
int i;
cin >> i;
while (cin.fail())
{
cin.clear();
cin.ignore();
cin >> i;
}
PushInt(i);
}break;
case VMInstruction::VMscan_string: {
cout << "<< ";
cin.clear(); cin.ignore();
char str[STR_MAX]; cin.getline(str, STR_MAX);
PushString(str);
}break;
case VMInstruction::VMadd: {
if (!IsType(VMType::VMint))TypeNotMatchException;
int last_int = PopInt();
if (!IsType(VMType::VMint))TypeNotMatchException;
int first_int = PopInt();
int result = first_int + last_int;
PushInt(result);
}break;
case VMInstruction::VMminus: {
if (!IsType(VMType::VMint))TypeNotMatchException;
int last_int = PopInt();
if (!IsType(VMType::VMint))TypeNotMatchException;
int first_int = PopInt();
int result = first_int - last_int;
PushInt(result);
}break;
case VMInstruction::VMmultiply: {
if (!IsType(VMType::VMint))TypeNotMatchException;
int last_int = PopInt();
if (!IsType(VMType::VMint))TypeNotMatchException;
int first_int = PopInt();
int result = first_int * last_int;
PushInt(result);
}break;
case VMInstruction::VMdivide: {
if (!IsType(VMType::VMint))TypeNotMatchException;
int last_int = PopInt();
if (!IsType(VMType::VMint))TypeNotMatchException;
int first_int = PopInt();
int result = first_int / last_int;
PushInt(result);
}break;
case VMInstruction::VMappendString: {
if (!IsType(VMType::VMstring))TypeNotMatchException;
char* str1 = PopString();
if (!IsType(VMType::VMstring))TypeNotMatchException;
char* str2 = PopString();
int str1len = strlen(str1);
int str2len = strlen(str2);
int length = str1len + str2len;
char* str3 = new char[length + 1];
int cur = 0;
for (int i = 0; i < str1len; i++)str3[cur++] = str1[i];
for (int i = 0; i < str2len; i++)str3[cur++] = str2[i];
str3[length] = '\0';
delete[] str1; delete[] str2;
PushString(str3);
}break;
case VMInstruction::VMintToString: {
if (!IsType(VMType::VMint))TypeNotMatchException;
char str[STR_MAX];
_itoa_s(PopInt(), str, 10);
PushString(str);
}break;
case VMInstruction::VMstringToInt: {
if (!IsType(VMType::VMstring))TypeNotMatchException;
int i = atoi(PopString());
PushInt(i);
}break;
}
}
구현은 위와 같습니다.
문자열 하나 추가했다고 더러워진 것 보세요.
#include "VM.h"
int main()
{
typedef VM::byte byte;
VM vm;
// ((1+2) * 3 + (2+3) * 4) * x
vm.PushInt(1);
vm.PushInt(2);
vm.Push(VMInstruction::VMadd);
vm.PushInt(3);
vm.Push(VMInstruction::VMmultiply);
// (1 + 2) * 3 = 9
vm.PushInt(2);
vm.PushInt(3);
vm.Push(VMInstruction::VMadd);
vm.PushInt(4);
vm.Push(VMInstruction::VMmultiply);
// (2 + 3) * 4 = 20
vm.Push(VMInstruction::VMadd);
// 9 + 20 = 29
vm.PushString("[((1+2) * 3 + (2+3) * 4) * x]: x = ");
vm.Push(VMInstruction::VMprint);
vm.Push(VMInstruction::VMscan_int);
vm.Push(VMInstruction::VMmultiply);
// 29x
vm.PushString("result: ");
vm.Push(VMInstruction::VMprint);
vm.Push(VMInstruction::VMprint);
vm.Push(VMInstruction::VMprint_enter);
}
((1 + 2) * 3) + (2 + 3) * 4) * x 를 계산하는 과정입니다.
x는 사용자가 입력한 자연수입니다.
마지막 4줄에서는 계산 결과를 출력하고 있습니다.
한 번 작성해보면 스택의 입출력을 머리로 익히기 좋습니다.
x를 마지막까지 가져다가 식에 대입하여 다시 한번 출력하고 싶었는데 그러려면 추가적인 버퍼가 필요할 것 같습니다.
서브 버퍼의 자료구조를 스택, 큐, 리스트 중 무엇으로 해야할 지 애매하여 그냥 만들지 않았습니다.