Preface
I've recently encountered such a problem: how to call a property or method from C project in C# class? Google gives me an inaccurate info of what to do with this kind of problem. I spent some time on it and when I finally know what to do I decided to share this knowledge with the others - maybe I could save someone some time.
I want to announce - I am not a C/C++ programmer rather I work with C# so if you know how something could do better just email me or post a suggestion in comments. What I want to present here is a straightforward solution for anyone who is not familiar with C/C++/COM programming.
Introduction
Before we begin we should answer basic question:
Why do we need this? We can use CLR and achieve the same results with less effort, can't we? This is true but only when you need to create that kind of communication from scratch. This article is mostly for people who have program (written in C) already created and for those who can't change the compiler from C to CLR because of the problems that it may provide.
This article covers a basic setup for creation of communication between .NET objects (through COM) to plain C (without any help from CLR and/or additional frameworks). This can give you a possibility to call .NET (created in C# or VB) methods, properties etc. directly from plain C project.
Our goal will be to acquire some data from C# Class Library back to C Win32 Console Application. Sounds simple but it requires a few steps to accomplish. Let's get started!
1. Setup proper compiler options
1.1 First we need to create a solution within Visual Studio (I use VS2008 Pro). Let's choose: File -> New -> Project, then Other Languages -> Visual C++ -> Win32 Console Application. I named it "plainC". After clicking 'OK' the project wizard should pop up. After clicking 'Next' I recommend to uncheck Precompiled header option unless you are familiar with it. To finish click 'Finish'.
1.2 As you can see IDE produces *.cpp class and indeed it's a C++ project. Let's change this to C. We need to rename our main file from "plainC.cpp" to "plainC.c".
1.3 Next we need to change compile language option: right click on project -> Properties then Configuration Properties -> C/C++ -> Advanced -> Compile As and choose: Compile as C Code (/TC).
The rest of options may (or should for this solution) remain as they are. Especially in general configuration properties tab:
1.4 OK now we can create C# class (for convenience let's do this in current solution). Create new project in Visual C# -> Windows and choose: Class Library. I named project as "CSharpLib".
1.5 Into the project properties we may set an automatic registering our assembly for COM interoperability. We can find this option under: Build -> Register for COM interop. If for some reason you don't want to do this automatically then use regasm.exe (for further details and some info of how to use it you may look into msdn).
2. Preparing the .NET code
2.1 Now we can finally add new class to the C# project - for example: NetClass with some properties and with explicit default constructor. We need also an interface (INetClassComVisible) because through it we will call the class members later.
using System;
using System.Runtime.InteropServices;
namespace CSharpLib
{
[ComVisible(true)]
[Guid("1A741C67-8D57-4a83-8E0B-834D6D526D0C")]
[ClassInterface(ClassInterfaceType.None)]
public class NetClass : INetClassComVisible
{
public string StringValue
{
get
{
return "Example string";
}
}
public string CreationTime
{
get;
internal set;
}
public int IntegerValue
{
get
{
return 50;
}
}
public string CustomValue
{
get;
internal set;
}
public int Add(int x, int y)
{
return x + y;
}
public NetClass() { }
}
[ComVisible(true)]
[Guid("C27A0124-BA12-4323-B83C-68C27536F989")]
public interface INetClassComVisible
{
string StringValue { get; }
string CreationTime { get; }
int IntegerValue { get; }
string CustomValue { get; }
int Add(int x, int y);
}
}
Short code explanation:
[ComVisible(true)]
This attribute controls the visibility and accessibility to the COM locally. That means, only the marked type with this attribute will be visible, while the others will not. If you want to reverse this behavior (everything visible and marked is not visible) then you can check checkbox in project properties: Application->Assembly Information-Make assembly COM-Visible or in AssemblyInfo.cs change entry from:
[assembly: ComVisible(false)]
to:
[assembly: ComVisible(true)]
and of course mark the type that you want to be invisible with [ComVisible(false)]
attribute.
--------------------------------------------------------
[Guid("1A741C67-8D57-4a83-8E0B-834D6D526D0C")]
This is necessary because GUID participate in connecting our COM and C worlds. It must be declared explicity because we need the same GUID on both sides. You can use GUID Generator tool (guidgen.exe) which could help in creation of classes/interfaces guids or under VisualStudio->Tools->Create GUID: Registry Format.
--------------------------------------------------------
[ClassInterface(ClassInterfaceType.None)]
This attribute could help us to provide access to type member through explicitly defined interface which is implemented by our COM visible class.
--------------------------------------------------------
Everything is clear? Good, so let's move on.
3. Generate bridge between C and COM worlds
3.1 Open OLE/COM Object Viewer (C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\OleView.exe) then click 'View TypeLib' option (three red triangles button) and choose CSharpLib.tlb - this file should be created together with dll if you check 'Register for COM interop'. You could also create tlb from regasm (I mentioned about this earlier).
3.2 Afterwards new window should popup with some content. This content is in fact generated Interface Definition Language code which is language-independed description of interfaces. Sadly, I don't know why but 'Save As' option from ITypeLib Viewer doesn't work (for me). File saved in this way has a trimmed content and it will cause further errors (God.. how much time I spent to figure out a reason of these errors..). Solution is simple - we need to create the file manually. Select all the content from viewer then copy and paste it to new file. Save it as CSharpLib.IDL
3.3 Now we need the MIDL compiler to take an action.
"The Microsoft Interface Definition Language (MIDL) defines interfaces between client and server programs. MIDL can be used in all client/server applications based on Windows operating systems. It can also be used to create client and server programs for heterogeneous network environments."
So let's make use of it. Let's run Visual Studio 2008 Command Prompt (Start -> Microsoft Visual Studio 2008 -> Visual Studio Tools -> Visual Studio 2008 Command Prompt). Then change current directory to place with our compiled library:
cd C:\PlainC\CSharpLib\bin\Debug
and run midl compiler with following parameters:
midl /Oicf CSharpLib.IDL /h CSharpLib.h
it should create 2 files: CSharpLib.h and CSharpLib_i.c
Little explanation:
/Oicf
Specifies the codeless proxy method of marshaling that includes all the features provided by /Oi and /Oic but uses a new interpreter (fast format strings) that provides better performance than /Oi or /Oic. This switch includes recent RPC enhancements and is recommended for modern RPC scenarios.
/h
The /h switch specifies filename as the name for a header file that contains all the definitions contained in the IDL file, without the IDL syntax. This file can be used as a C or C++ header file.
More details: MIDL Command-Line Reference
4. Preparing the C code
4.1 Now we must include compiler-generated files at our C project. To do so, move them to PlainC project directory and from VS use 'add existing files' option. The simpliest code can look like this:
#include "stdafx.h"
#include "CSharpLib.h"
int _tmain(int argc, _TCHAR* argv[])
{
BSTR bstrVal = SysAllocString(L"");
HRESULT hr = S_OK;
INetClassComVisible* dotNetClass = NULL;
hr = CoInitialize(NULL);
hr = CoCreateInstance(&CLSID_NetClass, NULL, CLSCTX_INPROC_SERVER,
&IID_INetClassComVisible, (void**) &dotNetClass);
hr = dotNetClass->lpVtbl->get_StringValue(dotNetClass,&bstrVal);
CoUninitialize();
return 0;
}
Debugging the code will show that the dotNetClass
is instantiated correctly and also calling the get_StringValue method works correctly. bstrVal
is successfully filled with proper getter value ("Example string"). More complex examples are provided in the sample code so check it out.
Little explanation of CoCreateInstance method:
HRESULT CoCreateInstance(
_In_ REFCLSID rclsid, - CLSID associated with type which we want to create
_In_ LPUNKNOWN pUnkOuter, - if not NULL it should point to object's IUnknown interface
_In_ DWORD dwClsContext, - indicate the context to manage newly created object
_In_ REFIID riid, - IID to interface to communicate through it
_Out_ LPVOID *ppv - address of pointer variable that receives return value
);
MSDN reference: CoCreateInstance function
Remember, You don't need to hold CSharpLib.dll at the same place where PlainC.exe is. COM model handles all that stuff related to the location of lib. Of course if you move it from the location where it was registered then it won't work (you will get HRESULT = System cannot find the file specifed). However if COM is registered without 'codebase' flag then the library must reside at the same location as exe from where it is called.
Points of Interest
As you can see merging pure C environment with COM is really painful but it could be done. In the next article I'll try to show how it looks like in C++ (luckily it is easier there).