Я занимаюсь рефакторингом эмулятора NMOS6502 на несколько классов. Мне было интересно, существует ли «объектно-ориентированный» способ определения таблицы переходов функций. По сути, я определил отдельные классы команд для классификации групп связанных операций с процессором, таких как «CStackInstHandler» или «CArithmeticInstHandler», которые будут иметь ссылку на объект процессора. Каждый класс инструкций является производным от абстрактного класса инструкций. Каждый производный класс инструкций имеет набор функций, которые будут использовать открытый интерфейс объекта ЦП для изменения состояния ЦП, например:
uint8_t opcode = _memory->readMem(_cpu->getProgramCounter());
AInstructionHandler* _handler = _cpu->getInstHandler(opcode);
_handler->setCpu(&cpu);
_handler->setMemory(&memory);
_handler->execute(opcode);
Проблема заключается в том, что во время выполнения должен быть определен обработчик команд, а также соответствующая функция-член, определенная для этого обработчика, с использованием кода операции.
Итак, мы имеем — код операции читается из памяти, процессор использует таблицу для сопоставления кода операции с типом обработчика инструкции, а затем тот же код операции используется обработчиком инструкции для выбора правильной функции. Каждая инструкция переопределяет функцию «выполнить», например:
void CBranchInstHandler::execute() {
switch(_opcode) {
case 0x90:
this->BCC();
break;
case 0xb0:
this->BCS();
break;
case 0xf0:
this->BEQ();
break;
case 0x30:
this->BMI();
break;
case 0xd0:
this->BNE();
break;
case 0x10:
this->BPL();
break;
default:
break;
}
}
void CBranchInstHandler::BCC() {
uint16_t address = this->getAddress();
if(!_cpu->isCarry()) {
uint16_t pc = _cpu->getPC();
pc += address;
_cpu->setPC(pc);
}
}
/*more instruction specific functions...*/
Я заканчиваю с двумя поисками, один из которых является избыточным. Один для выбора обработчика, а другой для выбора функции обработчика. Я чувствую, что это неправильный способ решения этой задачи, но я не уверен в альтернативе, которая не просто превращалась бы в группы функций, не являющихся членами.
Мне интересно, есть ли у кого-нибудь понимание этой проблемы. В основном это сводится к желанию реорганизовать класс в более мелкие фрагменты (класс cpu с функциями-членами инструкций, рефакторизованными под класс cpu и классы инструкций), но все компоненты настолько взаимосвязаны, что мне приходится повторяться. Избыточность вводится.
Необъектно-ориентированным решением было бы просто иметь эти инструкции не являющимися членами-функциями, которые принимают ссылку на процессор. Затем будет определена таблица переходов функций, инструкции будут найдены и проиндексированы с помощью кода операции и выполнены.
Это не кажется практичным с объектами. Я мог бы сделать все инструкции статичными или что-то в этом роде, но, похоже, это не соответствует действительности.
Любое понимание или информация о даже косвенно связанных проблемах будет очень полезным.
Благодарю.
Я собираюсь продвинуть свой комментарий к ответу: объектно-ориентированное решение, как вы говорите, состоит в том, чтобы дать дочерним классам полную ответственность за решение, на какие коды операций они отвечают.
Я бы предположил, что самый простой способ сделать это не попытаться построить двухэтапный switch
но просто направить каждый код операции каждому ребенку и позволить ребенку либо внести свой вклад, либо нет. Это минимальное жизнеспособное решение.
Если вам нужна оптимизация, то проще всего было бы переформулировать:
void CBranchInstHandler::execute() {
switch(_opcode) {
case 0x90:
this->BCC();
break;
... etc ...
}
}
Для того, чтобы:
FuncPtr CBranchInstHandler::execute() {
switch(_opcode) {
case 0x90:
return BCC;
... etc ...
}
return NULL;
}
Так что каждый execute
возвращает, действительно ли он обработал этот код операции.
Внутри родительского класса вы можете просто сохранить таблицу от кода операции до указателя на функцию. Массив будет делать. Таблица изначально будет содержать NULL
с повсюду.
При выполнении кода операции найдите обработчик в таблице. Если есть обработчик, позвоните ему и продолжайте. Если нет то позвони execute
на каждого ребенка по очереди, пока кто-то не вернет обработчик, затем положит их в таблицу и затем вызовет. Таким образом, вы создадите его точно в срок, во время выполнения. Первый запуск каждого кода операции займет немного больше времени, но впоследствии вы получите то, что составляет таблицу переходов.
Преимущество этого заключается в том, что он позволяет тесно связывать информацию о том, что обрабатывает дочерний процесс, с фактической обработкой этого синтаксически, уменьшая накладные расходы кода и вероятность ошибки.
Если я понимаю, что вы делаете, это создаете класс для каждого типа инструкций (ветвь, арифметика, загрузка, сохранение и т. Д.), А затем внутри тех, в которых вы пишете функции-члены для отдельных инструкций — c.f. у вас есть «CBranchInstrHandler», который обрабатывает «ветви на переносе», «ветви на нуле» и т. д.?
Полностью объектно-ориентированный подход заключается в расширении вашего подкласса до отдельных инструкций.
class CBranchInstrHandler { virtual void execute() = 0; };
class CBranchOnCarryClear : public CBranchInstrHandler {
void execute() override {
...;
}
};
class CBranchOnCarrySet : public CBranchInstrHandler {
void execute() override {
...;
}
};
Теперь вы можете просмотреть свои инструкции за один раз, но вам понадобится однозначное сопоставление всех этих.
switch (opCode) {
case 0x90: return .. CBranchOnCarryClear .. ;
case 0xB0: return .. CBranchOnCarrySet .. ;
}
Элипсис есть, потому что я не уверен, как вы получаете указатель на ваш CBranchInstrHandler; Я предполагаю, что они статичны, а вы нет new
Принимая их каждую инструкцию.
Если они не имеют данных, вы можете вернуть их как функциональные объекты по значению:
struct Base { virtual void execute() { /* noop */ } };
struct Derived { void execute(override) { ... } };
Base getHandler(opcode_t opcode) {
if (opcode == 1) { return Derived(); }
}
но я подозреваю, что вы, вероятно, хотите взять параметры и сохранить состояние, и в этом случае возврат по значению может привести к срезанию.
Конечно, если вы используете C ++ 11, вы можете использовать лямбда-выражения:
switch (opCode) {
case 0x90: return [this] () {
... implement BCC execute here ...
};
case 0xB0: return [this] () {
... implement BCS execute here ...
}
}
Вы можете использовать указатель на функцию / метод члена класса:
void (CBranchHandlerBase::*)();
Использование для хранения указателей на методы, которые должны быть вызваны для данного _opcode
,
map<uint8_t, void (CBranchHandlerBase::*)()> handlers;
handlers[0x90] = &BCC;
handlers[0xb0] = &BCS;
...
Приведенный выше код должен быть предоставлен в разделе / методе инициализации внутри вашего базового класса для обработчиков. Конечно, BCC, BCS и т. Д. Должны быть объявлены как чисто виртуальные методы, чтобы подход работал.
Тогда вместо вашего переключателя:
void CBranchHandlerBase::execute() {
(this->*handlers[_opcode])();
}
Обратите внимание, что execute определен в базовом классе (и он не обязательно должен быть виртуальным!, Поскольку каждый обработчик будет иметь одинаковую функциональность метода execute).
Редактировать: Карта на самом деле может быть заменена на вектор или массив размером: 2^(8*sizeof(uint8_t))
по соображениям эффективности