Value types and exceptions in .NET profiling
Here comes the end of the series about .NET profiling APIs. This final episode describes how to get fields of a value type instance and how to deal with exceptions.
Getting fields of a value type instance
The case of a value type is very similar to a reference type except that the address you receive points directly to the beginning of the fields value; instead of the type MethodTable (or ObjectID if you prefer).
It means that you won’t be able to call ICorProfilerInfo::GetClassFromObject to get its ClassID and start the field enumeration like for a reference type. Note that despite its name, ICorProfilerInfo2::GetClassLayout is perfectly capable of providing fields offset for a value type.
Instead, you will have to use the metadata token (extracted from the method signature) corresponding to the parameter. If the type is defined in the same assembly as the method, it is just a matter of calling ICorProfilerInfo::GetClassFromToken with the same moduleID as the method:
If the type is defined in another assembly (i.e. TypeFromToken() will return mdTypeRef), the metadata keeps track of the relationships:
The ResolutionScope (i.e. assembly where the typeref is defined) is given by IMetaDataImport::GetTypeRefProps:
Unfortunately, I did not find any direct API call (either from IMetaDataImport or ICorProfilerInfo) to find the ModuleID where a typeref is defined in another assembly. The only link is the IMetaDataImport corresponding to the module implementing the typeref that is available via the not recommended IMetaDataImport::ResolveTypeRef:
This looks like a dead end: the metadata API knows about tokens (i.e. values generated by C# compiler) and the profiling API knows about IDs (i.e. pointers to internal data structures).
Remember that ICorProfilerInfo:: GetModuleMetaData returns the IMetaDataImport corresponding to a given ModuleID. So the idea is to be able to identify a ModuleID by its IMetaDataImport counterpart, enumerate the modules loaded by the profiler and get their “identifier” to compare with the one implementing the type we are interested in. This identifier could be the mdModule token return by IMetaDataImport::GetModuleFromScope:
Well… not really because I always got 0x1 in my test. This value could be the module in the assembly and I only tested single-module assemblies generated by Visual Studio. Hopefully, each module is labelled by a unique “mvid” (i.e. a GUID identifying each module) returned by IMetaDataImport::GetScopeProps:
Here is the code to enumerate profiled modules and check for the given refMvid:
For performance sake, it would be better to build (in your IProfilerCallback implementation of ModuleLoadFinished and ModuleUnloadFinished), a map between the loaded modules and their mvid. This map could then be used when a ModuleID is needed while only the metadata side is known.
What has been returned?
The final step of our journey is to figure out what is returned by a method. The leave callback executed each time a method returns receives a FunctionID and a COR_PRF_ELT_INFO as parameters:
The signature parsing for a FunctionID already shown tells whether it returns void or an instance of a type identified by an element type and a metadata token.
The COR_PRF_ELT_INFO parameter is the key to get the address of the returned instance thanks to ICorProfilerInfo3::GetFunctionLeave3Info:
To get meaningful information from this API, the COR_PRF_ENABLE_FUNCTION_RETVAL flag must be set when ICorProfilerInfo::SetEventMask is called during ICorProfilerCallback::Initialize. The returned COR_PRF_FUNCTION_ARGUMENT_RANGE contains the address of the returned instance in its startAddress field.
The same GetObjectValue helper function already used to get parameters’ value is still valid here.
And what about exceptions?
I discussed how to follow the normal flow of execution by entering and exiting a method. When, in a method, an exception is thrown and not caught, you won’t get notified by the Leave callback. Instead other methods of ICorProfilerCallback are called if you pass COR_PRF_MONITOR_EXCEPTIONS to ICorProfilerInfo::SetEventMask.
Let’s take the following C# example to understand when which callbacks are executed:
The blue arrows are showing the flow of execution from Throws to ThrowLevel3. When the InvalidOperationException is thrown, ExceptionSearchFunctionXXX callbacks are executed “backward” to find the first catch block that will match the exception (i.e. up to Throws). It is now time to run the finally blocks (if any) starting from where the exception was thrown (i.e. ThrowLevel3) up to the catch block in Throws.
The object corresponding to the exception is passed to ExceptionThrown and ExceptionCatcherEnter as ObjectID. Feel free to use the code that has been presented earlier to get the type of the exception. However, getting interesting fields such as _message, or _innerException requires to figure out the ClassID of the System.Exception base class.
As already mentioned, the ICorProfilerInfo2::GetClassIDInfo2 function returns the ClassID of the parent type. Here is the code to search a parent type in a type hierarchy:
The FunctionID corresponding to the method is passed as a parameter to ExceptionSearchFunctionEnter, ExceptionSearchFilterEnter, ExceptionSearchCatcherFound, ExceptionUnwindFunctionEnter, ExceptionUnwindFinallyEnter, and ExceptionCatcherEnter. (i.e. not to the xxxLeave callbacks)
Conclusion
This series of articles introduced the .NET native profiling API in the context of method enter/leave tracing. The relationships between its metadata counterpart has also been detailed. You should now be able to implement other overrides of ICorProfilerCallback to get details about allocations for example.
References
- Episode 1: Start a journey into the .NET Profiling APIs
- Episode 2: Dealing with Modules, Assemblies and Types with CLR profiling API
- Episode 3: Decyphering methods signature with .NET Profiling APIs
- Episode 4: Reading parameters value with the .NET Profiling APIs
- Episode 5: Accessing arrays and class fields with .NET profiling APIs