sexta-feira, janeiro 23, 2009

Simulando Um Computador - 4/4

Nesta última parte vamos dar uma examinada no código fonte do simulador, que pode ser baixado daqui.

Estrutura Básica

O SimComp é um programa C que segue a estrutura típica de uma aplicação "Windows API", nos moldes popularizados pelo Petzold.

O programa inicia pela rotina WinMain, que comanda a criação e apresentação da janela e depois entra no loop de tratamento de mensagens:
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInst,
LPSTR lpCmdLine, int nCmdShow)
{
HACCEL hAccel;
MSG msg;

// Iniciacoes
hInst = hInstance;
InitComp ();
nModoAtual = MODO_CMD;
hevBreak = CreateEvent (NULL, FALSE, FALSE, NULL);
hevInput = CreateEvent (NULL, FALSE, FALSE, NULL);

// Cria a janela principal
if (! CriaJanela())
return 1;

// Apresenta a janela
ShowWindow (hMainWnd, nCmdShow == SW_SHOWMAXIMIZED ? SW_SHOW :
nCmdShow);
UpdateWindow (hMainWnd);

// Trata as mensagens
hAccel = LoadAccelerators (hInst, MAKEINTRESOURCE (IDR_ACCEL));
while (GetMessage (&msg, NULL, 0, 0))
{
// Usa accelerator para converter Enter em Exec
if (!TranslateAccelerator (hMainWnd, hAccel, &msg))
TranslateMessage (&msg);
DispatchMessage (&msg);
}

// Limpeza final
CloseHandle (hevBreak);
CloseHandle (hevInput);
return 0;
}
A criação da janela não tem nada de especial, uma classe de janela é registrada e a janela é criada (vide rotina CriaJanela).

A rotina de janela cuida da interface com o operador:
LRESULT CALLBACK MainWinProc (HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
char szCmd [40]; // comando
PAINTSTRUCT ps;
HDC hdc;


switch (message)
{
case WM_CREATE:
// salva o handle, cria o fonte de letra e acerta o tamanho
hMainWnd = hwnd;
CriaFonte ();
MoveWindow (hwnd, 0, 0, 90*nCxCar, 32*nCyCar, FALSE);

// cria controles auxiliares e os inicia
hEdit = CreateWindow ("edit", "",
WS_VISIBLE | WS_CHILD | WS_BORDER | ES_UPPERCASE,
2*nCxCar, 28*nCyCar, 72*nCxCar, nCyCar+2,
hwnd, (HMENU) IDC_INPUT, hInst, 0);
hExec = CreateWindow ("button", "Exec",
WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
76*nCxCar, 28*nCyCar-3, 7*nCxCar, nCyCar+6,
hwnd, (HMENU) IDC_EXEC, hInst, 0);
hBreak = CreateWindow ("button", "Break",
WS_CHILD | BS_PUSHBUTTON,
76*nCxCar, nCyCar-3, 7*nCxCar, nCyCar+6,
hwnd, (HMENU) IDC_BREAK, hInst, 0);
SendMessage (hEdit, EM_SETLIMITTEXT, sizeof(szCmd)-1, 0);
SendMessage (hEdit, WM_SETFONT, (WPARAM) hFont, 0);
SendMessage (hExec, WM_SETFONT, (WPARAM) hFont, 0);
SendMessage (hBreak, WM_SETFONT, (WPARAM) hFont, 0);
return 0;

case WM_SETFOCUS:
// Deixar foco sempre no editbox
if (hEdit != NULL)
SetFocus (hEdit);
return 0;

case WM_PAINT:
if (! IsIconic (hwnd))
{
// Refaz a tela
hdc = BeginPaint (hwnd, &ps);
AtlReg (hdc);
AtlMem (hdc);
AtlDisplay (hdc);
Copyright (hdc);
EndPaint (hwnd, &ps);
}
return 0;

case WM_COMMAND: // Botoes
switch (LOWORD(wParam))
{
case IDC_EXEC: // Trata texto digitado
SendMessage (hEdit, WM_GETTEXT, sizeof (szCmd), (LPARAM) szCmd);
TrataDigitacao (szCmd);
SendMessage (hEdit, WM_SETTEXT, 0, (LPARAM) "");
SetFocus (hEdit);
return 0;

case IDC_BREAK: // Interrompe o programa
SetEvent (hevBreak);
SetFocus (hEdit);
return 0;
}
break;

case WM_CLOSE:
// Destroi controles e a janela
if (hEdit != NULL)
DestroyWindow (hEdit);
if (hExec != NULL)
DestroyWindow (hExec);
if (hBreak != NULL)
DestroyWindow (hBreak);
DestroyWindow (hwnd);
DeleteObject (hFont);
return 0;

case WM_DESTROY:
// Encerra o programa
PostQuitMessage (0);
return 0;
}

// Demais mensagens vao para a rotina padrao
return DefWindowProc (hwnd, message, wParam, lParam);
}
Os detalhes mais importantes desta rotina, como indicado nos comentários, são o tratamento das seguintes mensagens:
  • WM_CREATE: acerta o tamanho da janela, cria e configura os controles auxiliares (editbox de digitação os botões Exec e Break) na janela principal.
  • WM_SETFOCURS: obriga o foco a estar sempre no editbox.
  • WM_PAINT: redesenha a tela, mais detalhes adiante.
  • WM_COMMAND: trata os botões.
  • WM_CLOSE: faz a limpeza dos controles e do fonte criado.
  • WM_DESTROY: encerra o programa.
A Representação do Computador

Como o COMP é muito simples, a sua representação também é:
static int ac;            // acumulador
static int cy; // "sobrecarga" (carry)
static int cp; // contador de programa
static int mem [100]; // memoria

#define NLIN_DISP 11
#define NCOL_DISP 80

static char szDisplay [NLIN_DISP] [NCOL_DISP+1]; // display do computador
static int nLinDisp; // linha do cursor do display
static int nColDisp; // coluna do cursor do display

// Modo atual do computador
static enum
{
MODO_CMD, // aguardando um comando
MODO_ASM, // executando comando Asm
MODO_EDIT, // executando comando Edit
MODO_RUN, // executando comando Run
MODO_INPUT // aguradando INPUT no comando Run
} nModoAtual;
Por simplificação usei int para o acumulador e memória, na verdade eles estão limitados a valores entre 0 e 9999. Da mesma forma, carry pode ser somente 0 ou 1 e cp pode variar de 0 a 99 mas também são int. O display do COMP foi organizado em 11 linhas de 80 colunas.

Por último, e mais importante, temos o "modo de operação" do COMP, que indica o que ele está fazendo. Isto é usado para definir o tratamento a ser dado ao texto digitado no textbox quando o botão Exec é pressionado.

O Monitor

Inicialmente o COMP está em MODO_CMD, no qual o texto digitado é tratado como um comando do monitor. A rotina TrataCmd examina a primeira letra do comando e executa a ação correspondente.
static void TrataCmd (char *szCmd)
{
unsigned tid;
HANDLE hThread;
char szAux [10];
int i;

// pula espacos iniciais
for (i = 0; *szCmd == ' '; i++)
;
if (szCmd[i] == 0) // ignora linha vazia
return;

// coloca a linha digitada no display
Display (szCmd, TRUE, TRUE);

// primeira letra deve ser comando
switch (szCmd[i++])
{
case 'A': // Asm
nPosAtl = PegaValor (szCmd, &i, 100);
wsprintf (szAux, "%02d: ", nPosAtl);
Display (szAux, FALSE, TRUE);
nModoAtual = MODO_ASM;
break;

case 'C': // Clear
InitComp ();
RefazTela ();
break;

case 'E': // Edit
nPosAtl = PegaValor (szCmd, &i, 100);
wsprintf (szAux, "%02d: ", nPosAtl);
Display (szAux, FALSE, TRUE);
nModoAtual = MODO_EDIT;
break;

case 'L': // Load
Load ();
Display (">", TRUE, TRUE);
break;

case 'R': // Run
cp = PegaValor (szCmd, &i, 100);
nVeloc = PegaValor (szCmd, &i, 100) * 100;
nModoAtual = MODO_RUN;
ShowWindow (hBreak, SW_SHOW);
SetFocus (hBreak);
ShowWindow (hEdit, SW_HIDE);
ShowWindow (hExec, SW_HIDE);
ResetEvent (hevBreak);
hThread = (HANDLE) _beginthreadex (NULL, 0, Exec, NULL, 0, &tid);
CloseHandle (hThread);
break;

case 'S': // Save
Save ();
Display (">", TRUE, TRUE);
break;

default:
Display ("Comando Invalido!", TRUE, TRUE);
break;
}
}
No caso do comando Asm a rotina Assembla é quem faz o parse da instrução assembly e coloca o código de máquina correspondente na memória.

Atualizando a Janela

Conforme pode ser visto no tratamento de WM_PAINT, as seguintes rotinas são responsáveis por atualizar a janela do programa: AtlReg, AtlMem, AtlDisplay e Copyright. Estas rotinas usam as funções do GDI do Windows.

A rotina Print encapsula a escrita de textos na janela:
static void Print (HDC hdc, int lin, int col, COLORREF cor, char *szTexto)
{
HFONT hOldFont;

hOldFont = (HFONT) SelectObject (hdc, hFont);
SetTextColor (hdc, cor);
SetBkColor (hdc, RGB(255,255,255));
SetBkMode (hdc, OPAQUE);
TextOut (hdc, col*nCxCar, lin*nCyCar, szTexto, strlen (szTexto));
SelectObject (hdc, hOldFont);
}
No caso da rotina Copyright, o texto é escrito duas vezes, uma em preto e outra em branco, com a defasagem de 1 pixel para produzir um efeito de baixo relevo:
static void Copyright (HDC hdc)
{
static char szTexto[] = "(C) 2004, Daniel Quadros";
HFONT hOldFont;

hOldFont = (HFONT) SelectObject (hdc, hFont);
SetBkMode (hdc, TRANSPARENT);
SetTextColor (hdc, RGB (255, 255, 255));
TextOut (hdc, 59*nCxCar+1, 26*nCyCar+6, szTexto, strlen (szTexto));
SetTextColor (hdc, RGB (0, 0, 0));
TextOut (hdc, 59*nCxCar, 26*nCyCar+5, szTexto, strlen (szTexto));
SelectObject (hdc, hOldFont);
}
Executando um programa

A execução de um programa no COMP é feita em uma thread separada, para não interferir no funcionamento da UI. A thread é criada quando o comando Run é interpretado:
        case 'R':        // Run
cp = PegaValor (szCmd, &i, 100);
nVeloc = PegaValor (szCmd, &i, 100) * 100;
nModoAtual = MODO_RUN;
ShowWindow (hBreak, SW_SHOW);
SetFocus (hBreak);
ShowWindow (hEdit, SW_HIDE);
ShowWindow (hExec, SW_HIDE);
ResetEvent (hevBreak);
hThread = (HANDLE) _beginthreadex (NULL, 0, Exec, NULL, 0, &tid);
CloseHandle (hThread);
break;
A execução em si é feita pela rotina Exec. De forma resumida, ela pega na memória a instrução, separa as várias partes e usa switches para decodificar as várias combinações. O trecho abaixo está simplificado, veja no fonte a rotina completa.
static unsigned __stdcall Exec (void *pParam)
{
int instr, op, ind, addr, val;
BOOL fCont, fErro;
HANDLE tbEvt [2];

fCont = TRUE;
fErro = FALSE;
while (fCont && !fErro)
{
// Pega a instrucao e avanca contador ("fetch")
instr = mem [cp++];
if (cp > 99)
cp = 0;

// Decodifica a instrução ("decode")
ind = instr / 1000;
op = (instr / 100) % 10;
addr = instr % 100;

// Decodifica o endereçamento
switch (ind)
{
case 0: // direto
val = mem [addr];
break;
case 1: // indireto
addr = mem [addr] % 100;
val = mem [addr];
break;
case 2: // imediato
default:
val = addr;
break;
}
if ((ind == 2) && (cTipoEnder [op] != '2'))
{
fErro = TRUE;
break;
}

// Executa a instrucao
switch (op)
{
case 0: // LDA
ac = val;
break;
case 1: // STA
mem [addr] = ac;
nPosMem = addr;
break;

// etc
}

if (fCont)
{
// testa break e controla a velociadade
fCont = WaitForSingleObject (hevBreak, nVeloc) == WAIT_TIMEOUT;
}
}

nModoAtual = MODO_CMD;
return fErro;
}
Dois eventos, cujos handles são mantidos em hevBreak e hevInput são usados para sincronizar a execução com a UI.

Quem quiser ver um exemplo bem sofisticado de simulação de computador pode examinar os fontes do POSE (Palm OS Emulator), que nomento podem ser baixados daqui.

Um comentário:

Anônimo disse...

O link do simulador esta fora do ar.