COM Integration/Writing your own COM component using VC++
Requires Analytica 4.6 Enterprise or better', and Microsoft Visual Studio
Introduction
This article leads you through the steps to create your own COM component in VC++ that can be called from your Analytica model. This provides a mechanism to call your own C++ code from Analytica. You may want to do this when you have an algorithm that you want to make really fast, have pre-existing and complex C++ code that you want to call, or want to create a wrapper to integrate with some other existing component, service or program.
The instructions here use Visual Studio 2013 Update 5. The steps may vary slightly in different Visual Studio releases.
In this introduction, I create a component named Rainfall.Counter
. Your own component name will differ, so make the substitution with your own component name where appropriate. Once completed, the component is used from Analytica by first instantiating the object in an Analytica variable using.
COMCreateObject("Rainfall.Counter")
And then calling its methods from other within the Definition of other variables. The methods of this class accept a one-dimensional array as input and return a one-dimensional array as a result.
Create the Project
Start by creating a Visual Studio project. Launch VS and select File / New / Project… / Templates / Visual C++ / ATL / ATL Project.
Enter a name for your project -- my project is named Rainfall
. Press OK. On the next screen, set the application type to Executable (EXE) and check Allow merging of proxy/stub code.
The proxy/stub is for convenience, eliminating an extra DLL that would otherwise have to be distributed with your final solution. The other Application types can also be used, but if you aren't sure why you'd want them, stick with EXE.
Once you press Finish, your initial project is created.
Configuring to be 64-bit
You may optionally configure your component to be 64-bit. A few factors come into play in making this decision. If you are using C++ for pure algorithmic speed, then I recommend building a 64-bit component, since 64-bit applications tend to run about 50% faster than 32-bit ones. In addition, your component will be able to access large amounts of memory, should that be needed. However, your 64-bit component will not be usable by someone with a 32-bit Windows operating system. If your component is a wrapper for another application, or if you are wrapping an existing third-party library, then you may need to match the bitness of your available database drivers, application drivers and libraries.
To configure to be 64-bit, select Build / Configuration Managerߪ. In the pulldown for Platform, select <Newߪ>
. Set New platform=x64, copy settings from=Win32 and press OK. It should now look like this.
Creating the Object Class
Next, create the class that will get instantiated when you call COMCreateObject from Analytica. It is usually most convenient at this point to go to the Class View pane in Visual Studio.
Select Project / Add Class… / ATL Simple Object and press Add.
(Note: In Visual Studio 2017 version 15.3.2, you have to select Add New Item... / Visual C++ / ATL / ATL Simple Object to get to the dialog)
Enter a short name for your class -- here I've named it Counter
. Then fill in the ProgID. The ProgID will be the text you'll use when calling COMCreateObject from Analytica.
Note: If at some point in the future you change your mind about what the ProgID should be, after you've created the class and implemented something so that you don't want to start over with the Wizard, you can change it by editing IDR_«classname»
in the "REGISTRY"
folder in your project resources. For example, in this case it is IDR_COUNTER
.
On the next screen, select the threading model. In most cases you will probably want to use Apartment
, especially if you don't understand the differences between these. In my case, I'll be using the Free threading model, which is the most challenging to program for. I am using the free threading model so that I can call a method multiple times concurrently on separate data sets.
For use by Analytica, you must have Interface=Dual or Interface=Custom with automation compatible checked. Press Finish to finalize the creating of the object class.
Setting the printable name
Next, set the printed representation that you'll see in the Analytica interface when it displays an instance of your object. This step is optional, but if you don't do it, your object will simply be shown as «COM object»
.
From the class view, right click on the interface class, ICounter
in my case, and select Add / Add Method…. Create a method that returns an [out, retval] BSTR*
result as shown here.
The method name can be anything, I have used ToText
here, you may prefer ToString
. Make sure you press the Add button before pressing Next>. Set the id
to 0. Analytica looks for a method with id=0 to see if the object can print itself. You may optionally select hidden when people are using tools to browse the methods of your class. Press Finish.
Now, edit the *.cpp
file for your class, Counter.cpp
in my case. You'll find the Add Method Wizard created an empty stub for your method, which you should now fill in to return the printed name of your object.
Register and test
Now that you have a class, you can run a test to verify that you can register (install) your component and instantiate it from Analytica.
Compile your project by selecting Build / Build solution. It should compile with no errors.
Next, to register (install) the component, open a CMD window and CD to your project directory, and then CD into the x64\Debug
directory where your project was just built. Type YourProgram.exe /RegServerPerUser
.
This inserts entries into your system registry telling Windows (and thus Analytica) how to instantiate your component. To uninstall, run it with /UnRegServerPerUser
. The /RegServerPerUser
installs the component for your user account only, and does not require system administration privileges. To install the component for all users on your computer, use /RegServer
, but this only works if you run CMD and Administrator.
Finally, to test that you can create an instance, start Analytica and created a variable to hold the instance and define it as a call to COMCreateObject
as seen here, replacing "Rainfall.Counter"
with the ProgID of your own class.
Evaluate. If you see an instance appear as in the preceding screenshot, you have a working class and are ready to start implementing methods.
Before you can compile anew, you'll need to have Analytica release the object. To do this, insert a space into the definition with COMCreateObject to cause the computed result to invalidate.
Creating a method with scalar parameters and result
Here I demonstrate how to create a method that accepts scalar parameters and returns a scalar result. Suppose we want a method that accepts and integer, «base», and a double, «x», and returns the base «base» logarithm of «x». To do this, right click on the interface name in the Class View panel (ICounter in my case) and select Add / Add Method…. Sometimes the Class View pane in Visual Studio lists the interface multiple times (possible a bug?), in which use the one that has a sub-folder Base Type.
In the Add Method Wizard, fill in the data types and names, as follows for the LogBase example.
As of Analytica 4.6, pure [out]
parameters are not supported in Analytica, so you should use only [in]
parameters, with the very last parameter being a [out,retval]
parameter if your method has a return value. The [out,retval]
parameter needs to be a pointer parameter like DOUBLE*
.
The second screen of the wizard selects an id for this method automatically and you don't need to make any changes on that screen. When the Wizard completes, open the *.cpp
for your class, where you'll find the Wizard has added a method stub for you to fill in.
From Analytica, test this by creating a new variable that calls the method.
counter->LogBase(2,Pi)
which evaluates to 1.651. Note that counter
is the variable defined previously which holds an instance of your COM component. You should insert a space in its definition to invalidate the result so that it lets go of the object, allowing you to recompile as you continue with your component development.
Creating a method with Array parameter and result
I now add an method that accepts a 1-D array as input and computes a 1-D array as a result. This method is called PeaksAndValleys
and filters out all points in the input array that are not a peak or valley.
This parameter will be of type SAFEARRAY<VARIANT>
, and the result will be of type SAFEARRAY<DOUBLE>
. Although I really want a SAFEARRAY<DOUBLE>
for the input parameter, Analytica will pass arrays as a SAFEARRAY<VARIANT>
, you should always use a VARIANT element type on input parameters.
Right-click on the interface name (ICounter
) and select Add / Add Method…. We are immediately faced with a problem -- SAFEARRAY does not appear as a data type option in the Visual Studio Wizard. Apparently they just forgot to include the array options, so just enter placeholder types for now.
Proceed through the second screen without making any changes. Next, open the *.idl
file from the Solution Explorer pane. The Wizard has added this line to the IDL:
[id(2)] HRESULT PeaksAndValleys([in] VARIANT timeSeries, [out, retval] DOUBLE* result);
Modify it by adding the SAFEARRAY type declaration so that it reads
[id(2)] HRESULT PeaksAndValleys([in] SAFEARRAY(VARIANT) timeSeries, [out, retval] SAFEARRAY(DOUBLE)* result);
Next, edit the *.h file for your class (Counter.h in my case). Here the Wizard has added the line
STDMETHOD(PeaksAndValleys)(DOUBLE /*in*/ timeSeries, DOUBLE* /*out,retval*/ result);
which you need to modify to be
STDMETHOD(PeaksAndValleys)(SAFEARRAY* /*in*/ timeSeries, SAFEARRAY** /*out,retval*/ result) override;
And in the *.cpp
file (Counter.cpp
in my case), change the parameter declarations in the same way. To allow the method to be tested, you can also add one line of code copy the input into the result.
STDMETHODIMP CCounter::PeaksAndValleys(SAFEARRAY* /*in*/ timeSeries, SAFEARRAY** /*out,retval*/ result) { SafeArrayCopy(timeSeries,result); return S_OK; }
You can test this from Analytica using
counter->PeaksAndValleys(COMArray(Time^2,Time))
In this expression, COMArray is used to pass a COM array in. The result is a list.
Implementing the function
From here, the implementation of the logic is just a matter of writing C++ code.
// Removes all points except those where the slope changes sign (the peaks and valleys). void FilterNonPeaksAndValleys( vector<double>& x ) { int prevSlope=0, nextSlope=0; // sign of preceding slope size_t iDst=0, n=x.size(); for (size_t iSrc=0 ; iSrc+1<n ; ++iSrc, prevSlope=nextSlope) { // All but last point nextSlope = sign(x[iSrc+1] - x[iSrc]); bool bSignChange = iSrc==0 || (prevSlope!=0) && (prevSlope!=nextSlope); // first point or sign change if (bSignChange) x[iDst++] = x[iSrc]; } if (n>=2) x[iDst++] = x[n-1]; // keep last point x.resize(iDst); } STDMETHODIMP CCounter::PeaksAndValleys(SAFEARRAY* /*in*/ timeSeries, SAFEARRAY** /*out,retval*/ result) { vector<double> data; if (!SafeArray1DToVector(timeSeries,data)) return E_INVALIDARG; FilterNonPeaksAndValleys(data); *result = VectorToSafeArray(data); return S_OK; }
Several helper functions are used in the above code. You may find the functions for converting from a SAFEARRAY to a C++ std::vector
to be convenient.
class AlwaysFinalize { std::function<void()> fn_; public: AlwaysFinalize(std::function<void()> fn) : fn_(fn) {} ~AlwaysFinalize() { fn_(); } }; inline bool SafeArray1DToVector( SAFEARRAY* /*in*/ psa, vector<double>& /*out*/ vec) { if (SafeArrayGetDim(psa) != 1) return false; VARIANT* pdata=nullptr; if (SafeArrayAccessData(psa,(void**)&pdata) != S_OK) return false; AlwaysFinalize rememberToUnaccess( [&](){ SafeArrayUnaccessData(psa); } ); unsigned n = psa->rgsabound[0].cElements; vec.resize( n ); for (unsigned i=0 ; i <n ; ++i) { _variant_t xi; if (VariantChangeType(/*dst*/ &xi,/*src*/ &pdata[i],0,VT_R8) != S_OK) return false; vec[i] = V_R8(&xi); } return true; } inline SAFEARRAY* VectorToSafeArray( const std::vector<double>& vec ) { unsigned n = (unsigned)vec.size(); SAFEARRAYBOUND bounds; bounds.cElements = n; bounds.lLbound = 0; SAFEARRAY* psa = SafeArrayCreate(VT_R8,1,&bounds); double* pdata = nullptr; if (SafeArrayAccessData(psa,(void**)&pdata) == S_OK) { AlwaysFinalize rememberToUnaccess( [&](){ SafeArrayUnaccessData(psa); } ); for (unsigned i=0 ; i<n ; ++i) { pdata[i] = vec[i]; } } return psa; } template <typename T> int sign(T val) { return (T(0) < val) - (val < T(0)); }
Summary
This article has shown how to set up a Visual Studio project to build your own COM component that can be called from an Analytica model.
Enable comment auto-refresher