Version 3 in Development
As some of you may already know, I am working on a third version of HexaEdit CE that will improve the program's stability, add some new features, and have better source code documentation.
A few of the issues that v3 will fix are:
- The overall sluggish response time in both the main menu and and in the editor
- Empty file handling (a bane of the previous versions)
- Lack of a testing framework for edge cases
The features that I am hoping add in v3 will be:
- The superuser lock (mentioned in the last post but extended to the "Ports" area)
- Password for the superuser
- Cut-Copy-Paste functions for the editor
- The option to write either nibbles or entire ASCII characters
- A rebooted Headless Start system that will allow TI-BASIC/ASM/C programs to open editors or to edit memory without opening an editor
- The "Ports" editor
- An editor for TI-vars, such a reals, strings, etc.
- Possibly an option to switch between the v1 and the v2 main menu styles
This version will be a complete rewrite in C++ and ASM.
Software Design Questions
HexaEdit is now split into three parts: the core (handles the actual memory operations, reading, writing, etc.), the GUI (main menu, editor, etc.), and the Headless Start. In the core is three abstract base classes: ABC_ReadOnlyMemory, ABC_ReadWriteMemory, and ABC_VariableMemory. Each ***Memory class describes a section of memory and its attributes. ABC_ReadOnlyMemory would describe ROM and ABC_ReadWriteMemory would describe RAM, for example.
ABC_ReadOnlyMemory implements all reading-related functions. ABC_ReadWriteMemory inherits from ABC_ReadOnlyMemory and adds writing functionality to it. ABC_VariableMemory inherits from ABC_ReadWriteMemory, is only used for files, and adds byte insertion/deletion functionality.
Each of these abstract base classes (ABC's) do not have any sanity-checking and assume that all of the arguments passed to them are valid. These classes can be considered the core's backend. Needless to say, these classes cannot be accessed directly, but are instead each encapsulated by a derived class: ReadOnlyMemory, ReadWriteMemory, and VariableMemory. These classes mirror all of the member functions in the base class and sanitize the arguments before passing them to the ABC for processing.
This setup makes sense to me, but I wanted feedback from the other, more experienced programmers on Cemetech who have designed similar systems. Will this design yield a robust core? Will it be maintainable and, if need be, extensible?
Another question that I had related to debugging. Consider this
function from ABC_ReadOnlyMemory which finds all occurrences of a given byte sequence in a particular memory area:
Code:
uint8_t ABC_ReadOnlyMemory::FindAll(
uint8_t **matches,
const uint8_t max_matches,
const uint8_t seq[],
const uint8_t len,
const uint24_t offset,
const uint24_t range
)
{
uint8_t num_matches = 0;
uint24_t range_from_start;
#if DEBUG
CCDBG_CHKPT_START("ABC_ReadOnlyMemory::FindAll");
CCDBG_DUMP_PTR(matches);
#endif
// Case 1: (<offset> + <range>) <= memory size
// The <range> can be wildly irrational, but the <offset> will be less than
// <m_Size>, thus this comparison must be written so:
if (range <= (m_Size - offset))
{
#if DEBUG
CCDBG_PRINT_MSG("Case 1");
CCDBG_DUMP_PTR(m_Addr + offset);
CCDBG_DUMP_PTR(m_Addr + range);
#endif
num_matches = asm_BFind_All(
m_Addr + offset,
m_Addr + range,
seq,
len,
matches,
max_matches
);
}
else
{
// Case 2: (<offset> + <range>) > m_Size
#if DEBUG
CCDBG_PRINT_MSG("Case 2");
CCDBG_DUMP_PTR(m_Addr + offset);
CCDBG_DUMP_PTR(m_Addr + m_Size);
#endif
num_matches = asm_BFind_All(
m_Addr + offset,
m_Addr + m_Size,
seq,
len,
matches,
max_matches
);
range_from_start = range - (m_Size - offset);
// Case 2a: <range_from_start> < <offset>
// Loop around to the start of the memory and end before the <offset>.
if (range_from_start < offset)
{
#if DEBUG
CCDBG_PRINT_MSG("Case 2a");
CCDBG_DUMP_PTR(m_Addr);
CCDBG_DUMP_PTR(m_Addr + range_from_start);
#endif
num_matches += asm_BFind_All(
m_Addr,
m_Addr + range_from_start,
seq,
len,
matches,
max_matches
);
}
else
{
// Case 2b: <range_from_start> > <offset>
// Loop around to the start of memory and end at the <offset>.
#if DEBUG
CCDBG_PRINT_MSG("Case 2b");
CCDBG_DUMP_PTR(m_Addr);
CCDBG_DUMP_PTR(m_Addr + offset - 1);
#endif
num_matches += asm_BFind_All(
m_Addr,
m_Addr + offset - 1,
seq,
len,
matches,
max_matches
);
}
}
#if DEBUG
CCDBG_CHKPT_END;
#endif
return num_matches;
}
When built with "make debug" and run inside of a function that tests it for edge cases, it produces the following CEmu console output:
Code:
|
| main()
|-----------------------------------------------------------------
|
| | ABC_ReadOnlyMemory::FindAll()
| |-----------------------------------------------------------------
| | matches = 0xd1a530
| | Case 1
| | m_Addr + offset = 0x1b0015
| | m_Addr + range = 0x1b7b1c
| |-----------------------------------------------------------------
|
| num_matches = 3
| data[data_num]->m_NumMatches = 3
|
| | ABC_ReadOnlyMemory::FindAll()
| |-----------------------------------------------------------------
| | matches = 0xd1a530
| | Case 1
| | m_Addr + offset = 0x1b0015
| | m_Addr + range = 0x1b7b1c
| |-----------------------------------------------------------------
|
| num_matches = 172
| data[data_num]->m_NumMatches = 172
|
| | ABC_ReadOnlyMemory::FindAll()
| |-----------------------------------------------------------------
| | matches = 0xd1a530
| | Case 2
| | m_Addr + offset = 0x1b7545
| | m_Addr + m_Size = 0x1b7b1c
| | Case 2b
| | m_Addr = 0x1b0015
| | m_Addr + offset - 1 = 0x1b7544
| |-----------------------------------------------------------------
|
| num_matches = 3
| data[data_num]->m_NumMatches = 3
|
| | ABC_ReadOnlyMemory::FindAll()
| |-----------------------------------------------------------------
| | matches = 0xd1a530
| | Case 2
| | m_Addr + offset = 0x1b7545
| | m_Addr + m_Size = 0x1b7b1c
| | Case 2a
| | m_Addr = 0x1b0015
| | m_Addr + range_from_start = 0x1b020e
| |-----------------------------------------------------------------
|
| num_matches = 2
| data[data_num]->m_NumMatches = 2
|
| | ABC_ReadOnlyMemory::FindAll()
| |-----------------------------------------------------------------
| | matches = 0xd1a530
| | Case 2
| | m_Addr + offset = 0x1b7545
| | m_Addr + m_Size = 0x1b7b1c
| | Case 2b
| | m_Addr = 0x1b0015
| | m_Addr + offset - 1 = 0x1b7544
| |-----------------------------------------------------------------
|
| num_matches = 3
| data[data_num]->m_NumMatches = 3
| test_succeeded = 1
|-----------------------------------------------------------------
|
Every programmer has their own preferred method of tracing functions, so this may seem like overkill to some, but I like the verbosity and aesthetic.
Assuming that even a rigorously debugged function can still have bugs that go unnoticed for any number of reasons, I would like to leave the "#if DEBUG ... #endif" blocks in the function so I don't have to rewrite them if another bug crops up in the function. On the other hand, having these blocks of non-algorithmic code makes it harder to follow the function's flow. How could I improve the legibility of the functions while preserving the verbose debugging output?