- What Is COM-interop?
- COM-interop from CM
- Behind the Scenes in .NET
- Behind the Scenes in CM
- Complex Parameters/Return Values
- Registration of Your COM-assemblies
What Is COM-interop?
COM is a windows-standard for making calls to external applications or libraries. You can use it for example if you have a .NET-assembly that you want to call from your CM-code. Many major applications like Excel also have COM-interfaces that you can use to make calls to/automate them.
It works like this:
- In order for an assembly to be callable by COM, interfaces are added to it declaring which classes/methods are COM-visible. Each class must be assigned a GUID (globally unique identifier)
- The assembly is COM-registered, which means info about it is added to the registry. You can see this info in regedit under the HKEY_CLASSES_ROOT-folder, which is a combination of CURRENT_USER\Software\Classes and LOCAL_MACHINE\Software\Classes. The GUID of each class is written to the CLSID-folder.
- A COM-call to the assembly can then be made by specifying either the program id or GUID.
The False Myth about COM-objects
It’s a widespread myth that COM-registration can only be done one LOCAL_MACHINE-level (requiring admin privileges).
Even Microsofts own tool regasm.exe only supports registering on LOCAL_MACHINE-level. But registering on CURRENT_USER level works just fine.
You can read more about this for example in this article/project: http://www.codeproject.com/Articles/3505/RegSvrEx-An-Enchanced-COM-Server-Registration-Util
COM-interop from CM
If you’re familiar with COM-interop from C++, you know it can be a total pain. You need to make static wrapper-assemblies that make the actual calls on the COM-object, so you need to more or less duplicate all function headers, keep them updated when you change the actual assembly, make sure everything is registered the right way etc.
You don’t need that anymore. This is how you instantiate a COM-object and make a method call using the cm.import.interop package:
use cm.import.interop; { NetObj host(); NetObj myObj = host.createByProgId("My.COMObject"); str val = myObj.callStr("MyFunction"); }
That’s it. If you want a more realistic example, this is an actual working cm-code for opening up an excel file and write something in it. Go to cm/abstract/interop/excelObj.cm, below the file, there is a function called excelReadAndWriteTest(str url)
, and there is already an executable code at the bottom. Do note you'll need to have the excel file in the url
you pass as the argument.
Behind the Scenes in .NET
cm.import.interop contains a .NET-assembly that uses reflection to generate calls on COM-objects.
In .NET the code for instantiating a COM-object from progid
(the name associated with the COM-type in the registry) is very straight forward:
Type comObjType = Type.GetTypeFromProgID(progId); object comObj = Activator.CreateInstance(comObjType);
So now you have your object, and you can make dynamic calls to its members like this:
object returnVal = comObjType.InvokeMember( name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, comObj);
Of course, this is just the basic principle, in order to make it usable you need a framework with nice interfaces, error handling, and whatnot. And this is where abstract.interop comes in because that’s what it is.
So now you know what’s going on at the .NET-side, let’s look at it from the CM point of view.
Behind the Scenes in CM
In order for CM to call the .NET-assembly containing the reflection-code, c++-assembly is used as a middle man.
So the call chain looks like this:
cm.abstract.interop (CM) -> dotNetInvoker.dll (C++) -> dotNetInvokerServer.exe (.NET) -> COM
When the CM-code is talking to dotNetInvoker, there are actually a number of calls made for each COM-call. This is in order to make the interface minimal. So instead of having all possible combinations of parameters/return value types in the interface, there are separate calls for:
- Adding each parameter to the call.
- Making the actual call.
- Getting the return value.
So in the cm-class NetObj
, the code for making a call to a method that takes two int-parameters and returns a string looks like this:
extend public str callStr(str name, int index1, int index2) { if (!verifyCall(COMAddParamInt(objPtr, index1))) return null; if (!verifyCall(COMAddParamInt(objPtr, index2))) return null; if (!verifyCall(COMCall(objPtr, name))) return null; str result = COMReturnString(objPtr); return result; }
As you see, the parameters are added one by one with their types specified, and the return value is also retrieved with its type specified. All primitive types (int, long, short, byte, char, str (yes I count string as a primitive ?), bool, float etc..) are specified explicitly in the interface.
Important note: the cm-class NetObj
is far from complete, even if the com/c++ interface itself is. I have just added methods with parameter/return value-combinations that I used in my Excel-example. But it’s very easy to add overloads for the parameter/return type-combo you want, just add them to netObj.cm as needed.
Complex Parameters/Return Values
You can use the type Object
for both parameters and return value. When a method call on a NetObj
returns a class (or a struct), you get another NetObj
wrapping it, that you can continue to dig into (by calling its methods/public properties) until you’re down on primitive-level. You can see in the Excel-example how you can get the data out of a 2d- array by first asking for upper/lower bounds and then getting the values as strings one by one (Modify the example in cm/abstract/interop/excelObj.cm with the bold code):
private void excelReadAndWriteTest(str url="C:\\Temp\\exceltest\\excelTest.xlsx") { ExcelObj excelApp(); try { excelApp.start(); if (ExcelWorkbook wb = excelApp.openWorkbook(url)) { for (i in 1..wb.sheetCount) { if (ExcelSheet ws = wb.getSheet(i)) { if (NetObj usedRange = ws.getObj("UsedRange")) { if (NetObj data = usedRange.getObj("Value")) { int leftmost = data.callInt("GetLowerBound", 1); int rightmost = data.callInt("GetUpperBound", 1); int top = data.callInt("GetLowerBound", 0); int bottom = data.callInt("GetUpperBound", 0); for (y in top..bottom) { for (x in leftmost..rightmost) { pnn(data.callStr("GetValue", y, x), "\t"); } pnn("\n"); } } } } } wb.save(); } } finally { excelApp.close(); } }
By the way, you can always specify a string as return type, the value will be converted to a string if it is of another type, even if it’s null or a class (if it’s a class, it’s ToString()
method will be called, default is that it returns the class name).
Registration of Your COM-assemblies
The only piece left in the puzzle is now the registration of your COM-assembly (if you’re not calling a 3rd party one that’s already installed on the users machine).
This can also easily be done by the cm.abstract.interop library, just call the function registerCOMAssembly()
with the name of your dll.
registerCOMAssembly("myComAssembly.dll");
The assembly is registered in the CURRENT_USER part if the registry, so you don’t need any admin-privileges to do it. You can register it whenever you like, as long as it’s before you attempt to COM-call it of course.
Example Code/Summary
Here’s a code snippet that sums up everything you need to do in order to make a COM-call from CM:
private void testCOM() { use cm.import.interop; once { registerCOMAssembly(Url("myDirectory/myComAssembly.dll")); } NetObj host(); try { NetObj myObj = host.createByProgId("My.COMObject"); str val = myObj.callStr("MyFunction"); } finally { host.dispose(); } }
Comments
0 comments
Please sign in to leave a comment.