Sophis Value/Risque: how to override the global functions in C# when using previous versions (<6.3)?
Nov
30
Written by:
11/30/2014 9:25 PM
This week, I just wondered why the Sophis R&D didn't permit to override the CSRGlobalFunctions class in C# before the 6.3 version. If you take a look at the DotNet toolkit, you will see that a CSMGlobalFunctions is defined, but no static method Register exist as for the other classes. This point was several times scrolled up to Sophis in the toolkit forums, and I know some clients who ask Sophis to provide a toolkit permitting their own implementation. Unfortunately, they don't have the code and the interface provided in C# does not implement all virtual of CSRGlobalFunctions, for instance StartPortfolioAddition which is a callback on which we can implement the agregation of results for user columns that we want to store in a cache.
For remember, the overriding of the CSRGlobalFunctions class permits to be called when the folios are calculated (StartPortfolioCalculation, EndPortfolioCalculation).
The goal of this post is to describe how to implement a C++ implementation of the CSRGlobalFunctions that permits to implement callback in C#.
The first thing to know and the hardest to implement is how to call managed code from unmanaged/native C++ code. There are a lot of articles on the web discussing how to do it (at least there: http://www.codeproject.com/Articles/9903/Calling-Managed-Code-from-Unmanaged-Code-and-vice and there: http://msdn.microsoft.com/en-us/library/ms973872.aspx). Instead of using COM, specific classes or whatever as described in the mentioned links, the method retained there consists to implement pure C methods in the CLR dll since we just have to override few methods.
If you take a look at the previous articles on which I've already implemented a mix of managed/unmanaged code (webservice for example), this corresponds to a simplifcation of the object factory. On the C++ managed side, I just need to implement a manager that will register all callbacks. Such a solution permits to workaround the singleton problem of CSRGlobalFunctions. By the way, even in C++, if you have several modules that are not linked each together and if you have to implement, in several modules, callbacks of available methods in the CSRGlobalFunctions, the right way is to declare your own singleton that manages registration, a global interface/abstract class, and implementation of your interface in your different modules.
Concretely, to override the CSRGlobalFunctions and to permit to be called in C#, the different steps are the following ones:
- declare a C# interface (in a specific dll)
- declare the overriding of the CSRGlobalFunctions in a native C++ toolkit
- declare a C++ managed dll that permits to manage the registration of C# interfaces and that can be called from the C++ native code
- finally implement our own C# interface in a C# toolkit.
In my case, I just want to override StartPortfolioCalculation, EndPortfolioCalculation and StartPortfolioAddition. The C native functions will be then declared as follow:
#pragma once
#ifdef ITQCALLER_EXPORTS
#define ITQCALLERAPI __declspec(dllexport)
#else
#define ITQCALLERAPI __declspec(dllimport)
#endif
#ifdef __cplusplus
extern
"C"
{
#endif
ITQCALLERAPI
void
StartPortfolioCalculation(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id);
ITQCALLERAPI
void
StartPortfolioAddition(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id);
ITQCALLERAPI
void
EndPortfolioCalculation(
const
sophis::portfolio::CSRExtraction* extraction,
long
folio_id);
typedef
void
(*STARTPORTFOLIOCALCULATION_CALL)(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id);
typedef
void
(*STARTPORTFOLIOADDITION_CALL)(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id);
typedef
void
(*ENDPORTFOLIOCALCULATION_CALL)(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id);
#ifdef __cplusplus
}
#endif
The declaration of the callback permits to load dynamically the dll, to initialize a pointer to functions by using the GetProcAddress method and to use it from the caller side without to link it statically. The body of these C native functions will just call the C++ managed code as follow:
#include "stdafx.h"
#include "ITQColumnGlobalFunctionsWrapper.h"
#include "ITQColumnGlobalFunctionsWrapperCallback.h"
#ifdef __cplusplus
extern
"C"
{
#endif
ITQCALLERAPI
void
StartPortfolioCalculation(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id)
{
ITQuants::ITQColumnGlobalFunctions::ITQColumnGlobalFunctionsWrapper::Instance()->StartPortfolioCalculation(gcnew sophis::portfolio::CSMExtraction(extraction),folio_id);
}
ITQCALLERAPI
void
StartPortfolioAddition(
const
sophis::portfolio::CSRExtraction * extraction,
long
folio_id)
{
ITQuants::ITQColumnGlobalFunctions::ITQColumnGlobalFunctionsWrapper::Instance()->StartPortfolioAddition(gcnew sophis::portfolio::CSMExtraction(extraction),folio_id);
}
ITQCALLERAPI
void
EndPortfolioCalculation(
const
sophis::portfolio::CSRExtraction* extraction,
long
folio_id)
{
ITQuants::ITQColumnGlobalFunctions::ITQColumnGlobalFunctionsWrapper::Instance()->EndPortfolioCalculation(gcnew sophis::portfolio::CSMExtraction(extraction),folio_id);
}
#ifdef __cplusplus
}
#endif
The ITQColumnGlobalFunctionsWrapper is the manager that permits the registration of the C# callbacks. In order to declare native mehtods, we have to follow the recommendations of Microsoft: an entrypoint.cpp file is declared in the project with a DllMain callback that will be called by the OS when the dll is loaded/unloaded. Note that this file has to be compiled without the /clr option. If it is not disabled, the managed LoaderLock exception will be raised when loading and can generate a crash at the beginning. I know some dll specific to clients but provided by Misys that do not respect this rule...
Since I have common code to the C++ code and C# code (the declaration of the interface for example), I've added a specific dll for declaring the interfaces. Using such a paradigm permits to be sure that the loading of the C++ toolkit will not interfere with the loading of the C# toolkit.
In the native C++ code, the main code to remember is the following one:
void
CITQColumnGlobalFunctions::init()
{
static
HINSTANCE
hInstCaller = NULL;
if
(hInstCaller==NULL)
{
#ifdef _NDEBUG
const
char
* dllName =
"ITQColumnGlobalFunctionsCaller64d.dll"
;
#else
const
char
* dllName =
"ITQColumnGlobalFunctionsCaller64.dll"
;
#endif
hInstCaller = LoadLibrary(dllName);
if
(hInstCaller!=NULL)
{
m_startPortfolioCalculation = (STARTPORTFOLIOCALCULATION_CALL)GetProcAddress(hInstCaller,
"StartPortfolioCalculation"
);
m_startPortfolioAddition = (STARTPORTFOLIOADDITION_CALL)GetProcAddress(hInstCaller,
"StartPortfolioAddition"
);
m_endPortfolioCalculation = (ENDPORTFOLIOCALCULATION_CALL)GetProcAddress(hInstCaller,
"EndPortfolioCalculation"
);
}
}
}
And on C# side:
namespace
ITQuants.ITQInterfaces
{
public
interface
IITQColumnGlobalFunctionsInterface
{
void
StartPortfolioCalculation(CSMExtraction extraction,
int
folio_id);
void
StartPortfolioAddition(CSMExtraction extraction,
int
folio_id);
void
EndPortfolioCalculation(CSMExtraction extraction,
int
folio_id);
}
}
using
System;
using
System.Collections.Generic;
using
System.Text;
using
sophis.misc;
using
sophis.portfolio;
namespace
ITQuants.ITQColumnsCache
{
public
class
ITQGlobalFunctions :ITQInterfaces.IITQColumnGlobalFunctionsInterface
{
public
void
StartPortfolioCalculation(CSMExtraction extraction,
int
folio_id)
{
}
public
void
EndPortfolioCalculation(CSMExtraction extraction,
int
folio_id)
{
}
public
void
StartPortfolioAddition(CSMExtraction extraction,
int
folio_id)
{
// some C# code
}
}
}
And finally the whole code:ITQGlobalFunctions.zip