The Mci
class is similar to the AutoTrace
class previously mentioned. Listing One contains the Mci
class. Mci
just notifies its sink that some method has entered or left (by the program counter). It also provides a lot of information about this method to the sink. This information consists of the filename and line where this specific instance of Mci
is located and also a method info struct, which contains the class name, method name, and type of each argument. The sink is registered by calling the static Register
method. The GetSink()
method is an interesting hack; it allows using a static variable without declaring it in a .cpp file. Although the C++ Standard allows declaring and initializing a static variable in the class definition, VC++ 6.0 (which I use) doesn't. The GetSink()
method returns a reference to an internal static object and thus circumvents the problem. This means that all the instances of Mci
in each and every method in your code notifies this single sink. On the surface, it looks like the same code is executed for every method, but in practice, the registered sink may employ a filtering and classification system based on the method information passed to it and dispatch the events accordingly to different handlers. For example, the initial sink may dispatch events according to groups of filenames (dispatch all events from files a.cpp, b.cpp, and c.cpp to Handler_1
and all other events to Handler_2
). The important point here is that Mci
the event's sourceis completely unaware of the entire procedure. It doesn't even know the true type of the original sink. All Mci
knows is that someone registered an IMciEvents
pointer to which it sends all the events.
Listing One
(a)
#ifndef MCI_H #define MCI_H #include <string> #include "MethodAnalyzer.h" struct IMciEvents; class Mci { public: Mci(const std::string & filename, int lineNumber, std::string line); ~Mci(); static IMciEvents * & GetSink(); static void Register(IMciEvents * pSink); private: std::string GetLine(const std::string & filename, int lineNumber); private: std::string m_filename; int m_lineNumber; MethodInfo m_methodInfo; }; #endif // !defined(__MCI_H__)
(b)
#include "Mci.h" #include "IMciEvents.h" #include "MethodAnalyzer.h" #include <fstream> using std::string; using std::ifstream; Mci::Mci(const string & filename, int lineNumber, string line) : m_filename(filename), m_lineNumber(lineNumber) { if (!GetSink()) return; if (line.empty()) line = GetLine(filename, lineNumber-2); m_methodInfo = MethodAnalyzer::Analyze(line); // verify corectness of class name using typeinfo GetSink()->OnEnter(m_filename, m_lineNumber, m_methodInfo); } Mci::~Mci() { if (!GetSink()) return; GetSink()->OnLeave(m_filename, m_lineNumber, m_methodInfo); } IMciEvents * & Mci::GetSink() { static IMciEvents * pSink = 0; return pSink; } void Mci::Register(IMciEvents * pSink) { GetSink() = pSink; } string Mci::GetLine(const string & filename, int lineNumber) { ifstream f; f.open(filename.c_str()); const int BUFF_SIZE = 1024; char buff[BUFF_SIZE]; for (int i = 0; i < lineNumber; i++) f.getline(buff, BUFF_SIZE); return string(buff); }
IMciEvents.
This interface (abstract class) should be implemented by some object and registered with the Mci
class by calling the static Mci::Register()
method; see Listing Two. There is nothing much to say about this interface, except that it provides an empty implementation for the events in case some sink doesn't care about one of the events. If the events were declared pure virtual, the implementing sink is compelled to implement all the events, even if it is only interested in the OnLeave
event. This is not a big deal for an interface with two methods, but I call it consideration and putting the client first. You may also notice that the return type is void
since Mci
doesn't care what the sink does with the information it sends. The <string>
header is included, although a forward declaration would have been good enough. Unfortunately, it is forbidden by the Standard to add declarations or definitions to namespace std
(to let vendors add their own extensions without collisions with user's code).
Listing Two
#ifndef MCI_EVENTS_H #define MCI_EVENTS_H #include <string> struct MethodInfo; struct IMciEvents { virtual void OnEnter(const std::string & filename, int line, const MethodInfo & mi) {} virtual void OnLeave(const std::string & filename, int line, const MethodInfo & mi) {} }; #endif
MethodAnalyzer.
This class is responsible for analyzing the current method and populating a MethodInfo
struct. Example 3 contains a censored definition of the class. MethodAnalyzer
exposes a single static method Analyze()
. This method accepts as input a string that contains the text line from the source where the method was declared. The important thing about the Analyze
method is that it is called dynamically every time a method is entered by the code, even if the same method is called lots of times. It could be wasteful if the analysis results were always the same for each method. In this case, some sort of caching per method would be helpful. However, it is likely that the analysis may also include the values of input/output arguments, and the return value of the method in the future. Clearly, this is a classic time/space trade-off.
Example 3: Class MethodAnalyzer.
class MethodAnalyzer { public: static MethodInfo Analyze(std::string line); private: ... };
Automating MCI
Putting an Mci
object at the beginning of each method in your code is a tedious task, and if you want to use it on a large existing project, it becomes daunting. To remedy this and cater to the natural programmer's laziness, I present some automation options. The objective is to have a project where all source files #include <Mci.h>
and all methods contain as their first statement the line:
Mci m(__FILE__, __LINE__, "Method(ArgType1 arg1, ArgType2 arg2...");
To achieve this objective automatically, I came up with the following algorithm:
- Identify all the project source files.
#include <Mci.h>
in every source file.- Scan each source file.
- Identify every method (or function).
- Extract the string that the
Mci
constructor requires as a third parameter. - Place a proper
Mci
line at the beginning of the method.
This automation procedure can be done offline in any language. I use Python, which is great in general and superb for such text-processing tasks. The script I wrote is naive and you are encouraged to modify it, or write a completely new automation script. Listing Three contains the InjectMci.py script. I will not delve into all the gory details. The basic idea is to detect lines that contain a method definition (using regular expressions), generate an Mci
line, and inject it in the proper place. I put in a moderate amount of flexibility, such as working with several brace styles and whitespace filtering.
Listing Three
#!/usr/local/bin/python import os, sys, glob, re index = 0 text = '' def InjectMci(text, selective): if selective and text.find('INJECT_MCI') == -1: return text index = 0 text = InjectMciHeader(text) text = InjectMciObjects(text, selective) return text def InjectMciHeader(text): if text.find('#include "Mci.h"') != -1: return text lines = text.split('\n') index = 0 # find index of last line that contains #include (0 if no #include is found) for i in range(len(lines)-1, 0, -1): if lines[i].find('#include') != -1: index = i+1 break text = '\n'.join(lines[:index]) text += '\n#include "Mci.h"\n' text += '\n'.join(lines[index:]) return text def GetMciLine(line): mci_mask = '\tMci m(__FILE__, __LINE__, "%s");' line = line.replace('{', ' ').strip() return mci_mask % line def InjectMciObject(base, index, lines, new_lines, selective): line = lines[index] if selective: if lines[index+base+1].find('INJECT_MCI') != -1: if base == 1: new_lines.append(lines[index+1]) new_lines.append(GetMciLine(line)) index += base+2; # skip the INJECT_MCI line else: index += 1 else: if base == 1: new_lines.append(lines[index+1]) new_lines.append(GetMciLine(line)) index += base+1; return index def InjectMciObjects(text, selective): method_re = r'[ \t]*.+[ \t]+.+::.+\(.*\)[ \t]*' open_par_re = r'[ \t]*{[ \t]*' p1 = re.compile('%s$' % method_re) p2 = re.compile('%s$' % open_par_re) p3 = re.compile('%s%s$' % (method_re, open_par_re)) lines = text.split('\n') new_lines = [] index = 0 while index < len(lines)-2: line = lines[index] new_lines.append(line) if p1.match(line) and p2.match(lines[index+1]): index = InjectMciObject(1, index, lines, new_lines, selective) elif p3.match(line): index = InjectMciObject(0, index, lines, new_lines, selective) else: index += 1 return '\n'.join(new_lines) if __name__ == "__main__": selective = len(sys.argv) > 1 and sys.argv[1] == 'selective' cpp_fi les = glob.glob('*.cpp') for f in cpp_files: print '-'*20 text = open(f).read() text = InjectMci(text, selective) print text open(f, 'w').write(text)