Method call interception means executing some code before and after the body of a function or a method is executed. During development, there are many situations where method call interception can come in handy. For instance, you may want to:
- Trace every method call to follow the flow of your program in a certain scenario in real time.
- Log every method invocation for later study or audit call patterns in an application.
- Profile how much time is spent in which method.
- Ensure that there are no resource leaks on method exit.
Method call interception is in the realm of Aspect-Oriented Programming (AOP), which deals with issues that can't be encapsulated in an object-oriented class or set of classes because these issues cut across the entire software (cross-cutting concerns). Programming these aspects of the program is difficult because they are scattered all over the code. AOP deals with this problem by identifying that you can't properly manage aspects using conventional object-oriented techniques, and then goes on to define language extensions that help in programming aspects (see http://www.aosd.org/ for more information).
In this article, I discuss several ways to perform method call interception, introduce the Method Call Interceptor (MCI) mechanism that enables source-level interception, discuss MCI automation and the overhead incurred by MCI, and finally present the Poor Man's Profiler (PMP), a simple profiler that demonstrates the use of MCI in a real-world scenario.
Intercept Method Calls
There are various ways to intercept method calls, depending on your execution environment, source code access, and expertise. COM+, for example, provides a hooking mechanism that can be used for this purpose. DLL calls can be easily intercepted because the address of all functions is kept in an Import Address Table (IAT), which can easily be modified to call your interception code instead. Another technique is to inject interception code into object files before linking. However, in this article I concentrate on source-level interception. This means modifying the source of a program you typically develop to do the interception.
The naive approach (which works fine in many cases) is just to write some code at the beginning and end of each method body; see Example 1. The problem with this approach is that it is tedious, labor intensive, nothing can enforce it, and across a multideveloper project, inconsistent formats are likely to appear. You are also likely to forget the finish
statement, which leads to functions that appear to never exit (at least in the trace output). Yet another problem with this approach is that you often need different types of interception at different times. For example, when trying to locate a difficult resource leak, you may want to track resources on entering/leaving a method; while hunting a stubborn logical bug, you may want a call tree tracing; and while tuning performance, you may want to know how long each method takes and how often it is called. Putting all this code in every method or changing the interception code every now and then is inhuman, if not inhumane.
Example 1: Naive approach.
void Foo(int x, int y) { cout << endl << "Foo(int x, int y) - start"; ... cout << endl << "Foo(int x, int y) - finish"; }
A somewhat better solution is defining an automatic object that does the "start" thing in its constructor and the "finish" thing in its destructor; see Example 2. The automatic object makes sure you won't forget that closing finish
line and keeps all the code in one place so you don't have to edit the code in every method separately. Still, you must pass the name of each method specifically, and if you want to add logging code or profiling, you must put AutoLog and AutoProfiler automatic objects in every method. Again, you have different interception needs in different methods and in different phases of the development process. Enter the Method Call Interceptor mechanism.
Example 2: Class AutoTrace.
class AutoTrace { public: AutoTrace(const string & s) : m_line(s) { cout << endl << m_line << " - start"; } ~AutoTrace() { cout << endl << m_line << " - finish"; } private: string m_line; }; void Foo(int x, int y) { AutoTrace("Foo(int x, int y)"); ... }
Method Call Interceptor: The Mechanism
Method Call Interceptor (MCI) is a mechanism that addresses the aforementioned problems. It consists of class Mci
, an abstract class (IMciEvents
), and a utility class (MethodAnalyzer
). The basic idea is this: An instance of the Mci
class is placed automatically at the beginning of each method. The constructor of Mci
, which is called upon entering each method, collects some information regarding the current method using the MethodAnalyzer
and notifies a preregistered events sink (an object that implements the IMciEvents
interface).
The motivation behind this observer-style design is separation of concerns. The code that performs the interception (Mci
) is totally independent and is actually unaware of the code that performs the actual tracing, logging, profiling, or what have you. This partitioning allows sophisticated implementations such as filtering and performing different actions for different methods without changing the generic method interception code or the method's code.