Wednesday, February 16, 2011

 

Working with C++ dll's from ObjectPascal


It's a well known fact that there are many excelent libraries written in C++, and we, as Object Pascal programmers can take advantage of them.

Many times, I have found in forums or mailing lists, questions such as "How can I translate XYZ library to ObjectPascal?", or "How can I use a C++ from Object Pascal?". Well, it's not necessary to translate a library from C++, we can use it just by creating a C wrapper around it.

Why I need to wrap the C++ library to C?

Because of name mangling, if you look inside the library (using TDump included in Delphi, or by using "nm" on linux), you'll get something like this:


000DB314 794 0319 _ZN12DcmAgeStringC1ERKS_
000DB274 795 031A _ZN12DcmAgeStringC2ERK6DcmTagm
000DB2F0 796 031B _ZN12DcmAgeStringC2ERKS_
000DB398 797 031C _ZN12DcmAgeStringD0Ev
000DB368 798 031D _ZN12DcmAgeStringD1Ev
000DB338 799 031E _ZN12DcmAgeStringD2Ev
000DB3C8 800 031F _ZN12DcmAgeStringaSERKS_


In the snippet from above, you can see names like _ZN12DcmAgeStringaSERKS. Those names may reffer to functions, methods, constants or variables we can call from Object Pascal by using GetProcAddress. The name in the example, is the "mangled" version of DcmAgeStringa.

If the C++ doesn't expose classes, but functions, you can call the functions just by passing the mangled name to GetProcAddress, but what about working with classes?.

There's no way to directly use a C++ class from Object Pascal, before using it is mandatory to create a "C" wrapper around it.

Let's start by defining a simple C++ class with only one method:


#include
using namespace std;

class Test
{
public:
Test();
void getHello(char * &AString);
};

Test::Test()
{
}

void Test::getHello(char * &AString)
{
const char * str = "Hello World from C++";
strcpy(AString, str);
}


The class above, can be used with something like this:


int main(){
Test * t = new Test();
char * str;
t->getHello(str);
cout << str;
}


It just gets change the value of the string "str" and print its contents.

As I mentioned earlier, there's no way to create an instance of a C++ class from Object Pascal, but there's a workaround, to create a "C" wrapper around it, and export it from the library.

Creating our first "C" wrapper around a C++ class

The idea is to create plain C functions in charge of creating an instance of the Test class, then return a pointer to that instance, then create another function that receive the pointer as param, and use its getHello function.

Here's the code:


#include
#include
using namespace std;

class Test
{
public:
Test();
void getHello(char * &AString);
};

Test::Test()
{
}

void Test::getHello(char * &AString)
{
const char * str = "Hello World from C++";
strcpy(AString, str);
}

extern "C"{
void createTestInstance(void * &instance)
{
instance = new Test();
}

void doHello(void * instance, char * &AString)
{
Test * lInstance = (Test *) instance;
lInstance->getHello(AString);
}

void deleteTestInstance(void * instance)
{
delete (Test *)instance;
instance = NULL;
}
}


If you look at the snippet above, you can see I added code inside an extern "C" block, there will reside the exported functions. I added three functions to create an instance of our class, to use the method getHello, and one to delete the instance.

To create a dll from this code, just do this (I'm using Linux here, but you can use MinGW from Windows using the same command):


g++ -fPIC -shared test.cpp -o test.so


That command compiles our code and creates a shared object file, in Windows you must replace "-o test.so" with "-o test.dll".

Now you can check the contents of the library by using nm or tdump. The result should be something like this:


...
0000000000000c08 T _fini
00000000000008b0 T _init
0000000000000990 t call_gmon_start
0000000000201068 b completed.7424
0000000000000ac5 T createTestInstance
0000000000000b4e T deleteTestInstance
0000000000000b21 T doHello
0000000000201070 b dtor_idx.7426
0000000000000a30 t frame_dummy
U strcpy@@GLIBC_2.2.5
U strlen@@GLIBC_2.2.5
...


You can see, that createTestInstance, deleteTestInstance and doHello now aren't mangled as in the plain C++ library.

Now the fun part!

The last step of this journey, is to create an Object Pascal program that loads and use the shared library. The program should do this:

1) Load the library and store a reference to its handler.
2) Execute the method createTestInstance and store a reference to the pointer it returns.
3) Execute the method doHello by passing the pointer stored in point 2 as parameter.
4) Delete the instance by executing deleteTestInstance and passing the same pointer.

Here's the code:


program Test;

{$mode objfpc}{$H+}

uses
dynlibs;

type
TCreateInstance = procedure (var AInstance: Pointer); cdecl;
TdoHello = procedure (AInstance: Pointer; var AString: PAnsiChar); cdecl;
TDeleteInstance = procedure (AInstance: Pointer); cdecl;

var
lCreateInstance: TCreateInstance;
ldoHello: TdoHello;
lDeleteInstance: TDeleteInstance;
lHandle: TLibHandle;
lInstance: Pointer;
lStr: PAnsiChar;

begin
lHandle := LoadLibrary('./test.so');
if lHandle <> NILHandle then
begin
writeln('Library loaded successfully!.');
lInstance := nil;
// First, create the instance
Pointer(lCreateInstance) := GetProcAddress(lHandle, 'createTestInstance');
if @lCreateInstance <> nil then
lCreateInstance(lInstance);
// Second, use the instance
Pointer(ldoHello) := GetProcAddress(lHandle, 'doHello');
if @ldoHello <> nil then
begin
GetMem(lStr, 255);
ldoHello(lInstance, lStr);
writeln(lStr);
FreeMem(lStr);
end;
// Third, delete the instance and unload the library
Pointer(lDeleteInstance) := GetProcAddress(lHandle, 'deleteTestInstance');
if @lDeleteInstance <> nil then
begin
lDeleteInstance(lInstance);
UnloadLibrary(lHandle);
end;
writeln('Done.');
end
else
writeln('Cannot load library.');
end.


That's all. Now you can use all your loved C++ libraries from Object Pascal!.
Comments:
At deployment, we will need to ship the original C++ dll, the plain C wrapper dll and the exe, rigth?
 
In the example, the C++ classes are wrapped INSIDE the dll, so, you'll have to deploy only two files, one dll, and one .exe.
 
On the face of it this looks like a dangerous example as it seems to ignore the issue of memory ownership (or worse - give the impression that it can *be* ignored).

In particular the use of a PANSIChar typed parameter *out* of the C DLL worries me... at the very least I believe you leak a string every time you call "doHello()" from the Delphi side.
 
Still not useful when the code lurks in a .LIB file.

AND you have to stub in a LOT of missing standard library calls that are not provided because you are using Delphi, not C.

The divide between C/C++ and Delphi is simply huge, and the makers of Delphi have never once down anything significant to reduce that divide allowing for easier intermixing of languages.

Unless you count Anders leaving for Microsoft and helping to build dotNet.
 
Xepol, the purpouse of this example is just to show a way to interact between an Object Pascal program and a C++ shared library, just that. I never said it's an easy method, in fact, it would be nice to find something easier ;).

I agree with you in your comment about the hughe divide between both languages. I preffer this, instead of trying to rewrite a C++ library in Object Pascal.
 
Jolyon, thanks for the comment, you are right.

I adapted the code to handle memory allocation from Delphi side.
 
I just wanted to point out that with Delphi you can put dll inside exe, so you can actually deploy only exe.
 
@Xepol: There's not actually much that they could do; the problem here is an inhernt flaw in the C++ language standard. There's no standardized ABI, which means among other things that there's no One True name mangling convention

This isn't a Delphi problem; C++ libraries can't talk to eaach other either without a C wrapper unless both sides were built with the same compiler.
 
@mart. You are right, that's a creative method of using C++ and Delphi in the same .exe.
 
Wouldn't it be nicer/cleaner to use procedures/functions with static declarations?

procedure CreateTestInstance(var AInstance: Pointer); cdecl; external 'test.so' name 'createTestInstance';
 
@Tood, when I start dealing with this kind of stuff, I usually begin by dynamically loading the dll and using GetProcAddress. For me, this approach is easier for debugging.

When I'm sure everything works ok, then I replace the dynamic calls by static.
 
Post a Comment

Links to this post:

Create a Link



<< Home

This page is powered by Blogger. Isn't yours?