I finished the task of calling the unmanaged dll functions from the C# application today. After I found out that my solution described before does not work, I spent almost two days on the task. Looking at what I've done, I can see that the whole solution takes maybe 2-3 dozen lines of code. And while I understand that lines of code is not the best way to measure productivity, I could have done it in half a day if I always chose the right way to do things. Why did it take me that long? Well, there are some reasons.
- I have not touched C++ in the last 8 years, so every little complaint from the linker or compiler was a challenge.
- I could not find a complete solution for my case, so I had to compile a few together: learn how to write a dll in C++, learn how to write a wrapper class, learn how to call the wrapper class from the managed code etc.
- There are numerous solutions on the internet, but choosing the right one is the problem. Some of the approaches I tried led me nowhere but took some time.
Now, when I'm done with this bucket of excuses, here is how I solved my particular case:
First of all, I had to understand that the straightforward DllImport solution does not work. I came across a comment that suggested that when a class member has to be called from a dll, a managed wrapper dll has to be written to allow access to the class members. From the C++ sample code I had I could see that yes, a class member is called.
CcnOCRsdk *ocr;
CString code;
RECO_DATA data;
GetDlgItemText(IDC_CODE,code);
char _code[200];
WideCharToMultiByte(CP_UTF8, 0, code, -1, (char *)_code, 200, NULL, NULL);
ocr->convertHKID_Name(_code,&data)
So, I looked at this example of creating a C++ dll
Walkthrough: Creating and Using a Dynamic Link Library
and at this example of writing a wrapper class
Calling Managed Code from Unmanaged Code and vice-versa
and made my first attempt at writing a wrapper. The wrapper in my initial solution exported the member function of a wrapped class. It worked to the point where the function was being called, and then threw the 'AccessViolationException'. Here I got stuck again.
Next thing to understand was that I have to export the whole class, including the constructor. To get access to the member of the class, I would have to return a pointer to the instance of the unmanaged class, and then pass this pointer to the function, that exports the member of the unmanaged class (I hope I'm describing it properly, but I'm not completely sure). The answers to this question pointed me to the right direction.
using a class defined in a c++ dll in c# code
After that, it was really simple, because I only had to apply my DllImport skills. So here we go:
First step, create a C++ DLL project in Visual Studio 2005.
- Start Visual Studio
- From the File menu, select New and then Project….
- From the Project types pane, under Visual C++, select Win32.
- From the Templates pane, select Win32 Console Application.
- Choose a name for the project and enter it in the Name field. Choose a name for the solution, and enter it in the Solution Name field.
- Press OK to start the Win32 application wizard. From the Overview page of the Win32 Application Wizard dialog, press Next.
- From the Application Settings page of the Win32 Application Wizard, under Application type, select DLL if it is available or Console application if DLL is not available.
- From the Application Settings page of the Win32 Application Wizard, under Additional options, select Empty project.
- Press Finish to create the project.
There were some setting I had to change for my project in Project->Properties:
- Under General->Project Defaults, set Use of MFC to 'Use MFC in a Shared DLL' as the unmanaged code was using MFC
- Under General->Project Defaults set Common Language Runtime support to 'Common Language Runtime Supportj(/clr)'
- Under Linker->Input->Additional Dependencies enter the name of the lib file for the unmanaged dll.
Second step, write a wrapper class for the unmanaged dll.
This is how the unmanaged class looks like:
class CNOCRSDK_API CcnOCRsdk {
public:
CcnOCRsdk(void);
bool convertHKID_Name(char *code,RECO_DATA *o_data); //hkid
//more member functions
~CcnOCRsdk();
private:
RECT *regionList;
RECT *chRect,*enRect;
//more member functions
};
This is the wrapper class I wrote, that exports the class constructor and one of the member functions:
include "stdafx.h"
#using
#include "cnOCRsdk.h"
using namespace System::Runtime::InteropServices;
using namespace System;
namespace TSSL
{
public class __declspec(dllexport) Wrapper
{
public:
CcnOCRsdk* SDKCreate()
{
return new CcnOCRsdk();
}
bool CcnOCRsdk_HKID(CcnOCRsdk* pSDK, char *code, RECO_DATA *o_data)
{
return pSDK->convertHKID_Name(code, o_data);
}
void SDKDelete(CcnOCRsdk* pSDK)
{
delete pSDK;
}
};
}
The __declspec(dllexport) at the class level exports all public class member in the dll. If it was applied on the member level, it would only export the member it was applied to.
Third step, run the Dumpbin utility and find out what are the 'mangled' names of the functions exported by the dll.
1 0 00001240 ??4Wrapper@TSSL@@QAEAAV01@ABV01@@Z = __t2m@??4Wrapper@TSSL@@QAEAAV01@ABV01@@Z ([T2M] public: class TSSL::Wrapper & __thiscall TSSL::Wrapper::operator=(class TSSL::Wrapper const &))
2 1 00001220 ?CcnOCRsdk_HKID@Wrapper@TSSL@@QAE_NPAVCcnOCRsdk@@PADPAURECO_DATA@@@Z = __t2m@?CcnOCRsdk_HKID@Wrapper@TSSL@@QAE_NPAVCcnOCRsdk@@PADPAURECO_DATA@@@Z ([T2M] public: bool __thiscall TSSL::Wrapper::CcnOCRsdk_HKID(class CcnOCRsdk *,char *,struct RECO_DATA *))
3 2 00001200 ?SDKCreate@Wrapper@TSSL@@QAEPAVCcnOCRsdk@@XZ = ?SDKCreate@Wrapper@TSSL@@QAEPAVCcnOCRsdk@@XZ (public: class CcnOCRsdk * __thiscall TSSL::Wrapper::SDKCreate(void))
4 3 00001410 ?SDKDelete@Wrapper@TSSL@@QAEXPAVCcnOCRsdk@@@Z = ?SDKDelete@Wrapper@TSSL@@QAEXPAVCcnOCRsdk@@@Z (public: void __thiscall TSSL::Wrapper::SDKDelete(class CcnOCRsdk *))
Fourth step, write another wrapper, now in C#, which will define the DllImports for the unmanaged functions and the structures required to call them
public class TSSLWrapper
{
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct RECO_DATA
{
/// wchar_t[200]
[MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = 200)]
public string FirstName;
/// wchar_t[200]
[MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = 200)]
public string Surname;
}
[DllImport(@"TSSL.dll", EntryPoint = "?SDKCreate@Wrapper@TSSL@@QAEPAVCcnOCRsdk@@XZ")]
public static extern IntPtr SDKCreate();
[DllImport(@"TSSL.dll", EntryPoint = "?CcnOCRsdk_HKID@Wrapper@TSSL@@QAE_NPAVCcnOCRsdk@@PADPAURECO_DATA@@@Z")]
public static extern bool CcnOCRsdk_HKID(IntPtr ptr, string num, out RECO_DATA o_data);
}
RECO_DATA is the structure I have to send from C# to C++. Note how the SDKCreate returns the IntPtr which is, in my understanding, a pointer to the instance of the unmanaged class. To call a member function of this class, I pass this pointer to the function.
And fifth and last step, is to call the C# wrapper class from the C# application and enjoy the results.
TSSLWrapper.RECO_DATA recoData = new TSSLWrapper.RECO_DATA();
string num = "262125355174";
IntPtr ptr = TSSLWrapper.SDKCreate();
bool res = TSSLWrapper.CcnOCRsdk_HKID(ptr, num, out recoData);
(To make the solution complete, I should also wrap the destructor and call it after I don't need the class any more, I'll do it soon)
Here we go, my longest post ever has arrived.
by Evgeny. Also posted on my website
No comments:
Post a Comment