Recent

Author Topic: how to use a class from a DLL  (Read 6846 times)

Rik

  • Jr. Member
  • **
  • Posts: 84
how to use a class from a DLL
« on: December 16, 2022, 02:24:28 pm »
I want to use a DLL (Oriel_USB.dll) to control a Oriel monochromator, where the public functions of the DLL are inside a class.
The header file is
Quote
#ifdef __cplusplus
extern "C"
{
#else
#typedef INT32 bool;  /* C does not know bool, only C++ */
#endif

#ifdef ORIEL_USB_EXPORTS
#define ORIEL_USB_API __declspec(dllexport)
#else
#define ORIEL_USB_API __declspec(dllimport)
#endif

// This class is exported from the Oriel_USB.dll
class ORIEL_USB_API COriel_USB {
   /////////////////// FUNCTIONS
   char * GetLibraryVersion(void);
   INT32 Send(const char* strCommand, INT32 bStaticDelay = false);
   INT32 Read(LPSTR strReturn);
   INT32 Query(const char* strCommand, char* strReturn, INT32 bStaticDelay = false);
   INT32 getCommandDelay(const char * command);
};
#ifdef __cplusplus
}
#endif

What I have done so far:
- I tried H2pas, but that produced just a lot of errors.

- Ignored the class and tried to acces the functions directly:
Code: Pascal  [Select][+][-]
  1. function GetLibraryVersion: Pchar; {$IFDEF MSWINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; external 'Oriel_USB.dll';
Returns an error "The procedure entry point GetLibraryVersion coeld not be located in Oriel_USB.dll"

- Looked up the function index with DLL expert viewer:
Code: Pascal  [Select][+][-]
  1. function GetLibraryVersion: Pchar; {$IFDEF MSWINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; external 'Oriel_USB.dll' index 5;
That works (no error) but the returned string is "No Device Found."

The monochromator is connected to the PC (and running) and can be controlled by the dedicated Oriel software. So hardware and driver are OK.

I looked on the web and on this forum for some pascal code exampled, but couldn't find any.
« Last Edit: December 16, 2022, 02:30:31 pm by Rik »

jamie

  • Hero Member
  • *****
  • Posts: 7762
Re: how to use a class from a DLL
« Reply #1 on: December 16, 2022, 02:46:00 pm »
Unless the DLL supports exporting the needed members as C/Stdcall functions I think you are up a creek without a paddle, sort of anyways!

  If you have a $MS C++ compiler you can make a proxy DLL with standard calls.

 Otherwise it would be a hack because you need to know the THIS pointer value etc.
The only true wisdom is knowing you know nothing

Thaddy

  • Hero Member
  • *****
  • Posts: 19249
  • Glad to be alive.
Re: how to use a class from a DLL
« Reply #2 on: December 16, 2022, 02:46:28 pm »
Binding to C++ is not supported. Binding to C is. Usually one would flatten the C++ class to C and subsequently use the C interface. I don't suppose you have the C++ sourcecode?
objects are fine constructs. You can even initialize them with constructors.

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #3 on: December 16, 2022, 04:05:26 pm »
Dear Thaddy, Jamie,

thanks for your replies.
I don't have the C++ source code.
Attached the information from Oriel.
For C# they refer to the cornerstone.dll. This file can be downloaded from their website, but there is no header file (cornerstone.h). So I cannot use H2pas.
And DLL export viewer does not show any functions either.
The link to the DLL is https://download.newport.com/#/Oriel/Monochromators%20and%20Spectrographs/Cornerstone%20Monochromator%20Series/API/V3.01/ (login as anonymous. The password has to stay empty).

BTW: the DLL expert viewer outcome for the Oriel_USB.dll is
Quote
private: int __thiscall COriel_USB::Connect(void)   0x10002c20   0x00002c20   3 (0x3)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
private: int __thiscall COriel_USB::Disconnect(void)   0x10002ef0   0x00002ef0   4 (0x4)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
private: int __thiscall COriel_USB::Initialize(void)   0x10002e30   0x00002e30   6 (0x6)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
private: int __thiscall COriel_USB::QueryString(char const *,char *,int)   0x10003480   0x00003480   8 (0x8)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
private: int __thiscall COriel_USB::SendString(char const *)   0x10003200   0x00003200   11 (0xb)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
private: void __thiscall COriel_USB::stringToUpper(class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > &)   0x10003a70   0x00003a70   14 (0xe)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: __thiscall COriel_USB::COriel_USB(void)   0x100034d0   0x000034d0   1 (0x1)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: __thiscall COriel_USB::~COriel_USB(void)   0x100035e0   0x000035e0   2 (0x2)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: char * __thiscall COriel_USB::GetLibraryVersion(void)   0x100035f0   0x000035f0   5 (0x5)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: int __thiscall COriel_USB::getCommandDelay(char const *)   0x10003620   0x00003620   13 (0xd)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: int __thiscall COriel_USB::Query(char const *,char *,int)   0x10003430   0x00003430   7 (0x7)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: int __thiscall COriel_USB::Read(char *)   0x10003340   0x00003340   9 (0x9)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: int __thiscall COriel_USB::Send(char const *,int)   0x10003040   0x00003040   10 (0xa)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: void __thiscall COriel_USB::__autoclassinit2(unsigned int)   0x10002c00   0x00002c00   12 (0xc)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
As mentioned before I can get some response from the GetLibraryVersion function, using the index.
But as the response is "No Device Found." I think I have to access the function via the class.
The user manual says
Quote
Oriel_USB.h file provides a COriel_USB class that links DLL functions to connect and control the Cornerstone.
So I assume that
Quote
public: __thiscall COriel_USB::COriel_USB(void)   0x100034d0   0x000034d0   1 (0x1)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: __thiscall COriel_USB::~COriel_USB(void)   0x100035e0   0x000035e0   2 (0x2)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
refers to the class.
« Last Edit: December 16, 2022, 04:13:44 pm by Rik »

jamie

  • Hero Member
  • *****
  • Posts: 7762
Re: how to use a class from a DLL
« Reply #4 on: December 16, 2022, 04:41:50 pm »
have you tried using the GetProcAddress on specific API? it's possible that is a Static type of class, and those members are exported.
The only true wisdom is knowing you know nothing

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #5 on: December 16, 2022, 05:36:41 pm »
Just did a brief test:
Code: Pascal  [Select][+][-]
  1. uses
  2.   Windows,dynlibs;
  3.  
  4. type
  5.   tMyFunc = function: PChar; stdcall;    
  6.  
  7. procedure TMainForm.FormActivate(Sender: TObject);
  8. var
  9.   MyHandle: THandle;
  10.   MyFunc: TMyFunc;
  11.   p: PChar;
  12.   s: string;
  13. begin
  14.   MyHandle:= LoadLibrary('Oriel_USB.dll');
  15.   try
  16. //    MyFunc:= tMyFunc(GetProcedureAddress(MyHandle,'GetLibraryVersion')); // throws error
  17.     MyFunc:= tMyFunc(GetProcedureAddress(MyHandle,5));
  18.     p:= MyFunc();
  19.     s:= StringReplace(StrPas(p),#13,'',[rfReplaceAll]);
  20.     Memo.Lines.Add(s);
  21.   finally
  22.     FreeLibrary(MyHandle);
  23.   end;
  24. end;
It returns "Cornerstone C++ DLL version 3.01" instead of "No Device Found.".
Looks promising.

I will see if I can get to control the monochromator that way and report back (probably on monday).

Thank you for the hint!

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #6 on: December 19, 2022, 03:44:54 pm »
Still struggling ...

This code works as expected (shows "Cornerstone C++ DLL version 3.01")
Code: Pascal  [Select][+][-]
  1. function MyGetLibraryVersion: PChar; {$IFDEF MSWINDOWS}stdcall{$ELSE}cdecl{$ENDIF}; external Oriel_USB_DLL index 5;
  2.  
  3. procedure TMainForm.FormCreate(Sender: TObject);
  4. const
  5.   Oriel_USB_DLL = 'Oriel_USB.dll';
  6. var
  7.   hOriel_USB_DLL: THandle;
  8.   p: PChar;
  9.   s: string;
  10. begin
  11.   hOriel_USB_DLL:= LoadLibrary(PChar(Oriel_USB_DLL));
  12.   p:= MyGetLibraryVersion();
  13.   s:= StringReplace(StrPas(p),#13,'',[rfReplaceAll]);
  14.   Memo.Lines.Add(s);
  15.   FreeLibrary(hOriel_USB_DLL);
  16. end;
  17.  
  18. end.
Without line 28 (hOriel_USB_DLL:= LoadLibrary(PChar(Oriel_USB_DLL));) it shows "No Device Found."

If I change the code in FormCreate to
Code: Pascal  [Select][+][-]
  1.   hOriel_USB_DLL:= LoadLibrary(PChar(Oriel_USB_DLL));
  2.   p:= MyGetLibraryVersion();
  3.   s:= StringReplace(StrPas(p),#13,'',[rfReplaceAll]);
  4.   Memo.Lines.Add(s); // "Cornerstone C++ DLL version 3.01"
  5.   p:= MyGetLibraryVersion();
  6.   s:= StringReplace(StrPas(p),#13,'',[rfReplaceAll]);
  7.   Memo.Lines.Add(s); // "No Device Found."
  8.   FreeLibrary(hOriel_USB_DLL);
the 1st call of MyGetLibraryVersion shows "Cornerstone C++ DLL version 3.01" and the 2nd call shows "No Device Found."
After adding another "hOriel_USB_DLL:= LoadLibrary(PChar(Oriel_USB_DLL));" before the 2nd call to MyGetLibraryVersion I get "Cornerstone C++ DLL version 3.01" twice.
It seems that the DLL needs to be loaded again for every function call, is that to be expected?

Furthermore: when calling other DLL functions (that actually require data exchange with the monochromator) such as "Send" or "Query" (see earlier post) the program hangs when calling the function.
So I have the impression that I can access the DLL functions, but that these do not communicate with the monochromator.

In C code snippet provided by Oriel a "COriel_USB" class object is created and the DLL functions are called via this object:
Quote
COriel_USB myDevice;
myDevice.Send(strCommand);
Maybe that is the way it should be done.
But how to do that in Freepascal?

"COriel_USB" is found in the DLL with DLL expert viewer:
Quote
public: __thiscall COriel_USB::COriel_USB(void)   0x100034d0   0x000034d0   1 (0x1)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
public: __thiscall COriel_USB::~COriel_USB(void)   0x100035e0   0x000035e0   2 (0x2)   Oriel_USB.dll   C:\aaa\Oriel_USB.dll   Exported Function   
But as a function?
« Last Edit: December 19, 2022, 03:47:32 pm by Rik »

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1315
Re: how to use a class from a DLL
« Reply #7 on: December 19, 2022, 04:47:15 pm »
What interface does it uses? USB? RS-232? I would try to interface with it directly. If you want to know what goes over the line, you can use a packet snooper. And perhaps there is a communication specification?

Trying to interface C++ classes is a big pain, as others have said.

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #8 on: December 19, 2022, 05:02:14 pm »
Quote
What interface does it uses? USB? RS-232?
: USB
C++: they have a C# and a C++ DLL.
The C++ DLL is very poor documented, but using DLL expert viewer I can get some information about the functions.
The C# DLL is not documented at all and unfortunately also DLL expert viewer returns no information.
Any suggestions how to retrieve information from a C# DLL?

balazsszekely

  • Guest
Re: how to use a class from a DLL
« Reply #9 on: December 19, 2022, 05:26:27 pm »
@Rik

You cannot use the c++ dll directly, you have to create a proxy dll first: https://blogs.embarcadero.com/how-to-use-a-c-dll-in-any-delphi-program/

Warfley

  • Hero Member
  • *****
  • Posts: 2066
Re: how to use a class from a DLL
« Reply #10 on: December 19, 2022, 07:20:59 pm »
The problem with using C++ classes from a library is that this is not intendet to be possible. Not even from a C++ side, as C++ does not define a standardized memory layout (for inheritance) nor a standardized function mangling and calling conventions for linking. Some C++ compilers have added support for this, e.g. Microsoft with it's MSVC++ compiler adding the keyword __declspec(dllexport), as is used in the C++ code you posted.

But as a Microsoft only solution this only works with MSVC++ and nothing else. So to get this working with FPC you have basically two options, as MSVC++ guarantees a certain wrapping, you can just implement the calls yourself. Microsoft specifies the used calling convention __thiscall here https://learn.microsoft.com/en-us/cpp/cpp/thiscall?view=msvc-170
Quote
Under __thiscall, the callee cleans the stack, which is impossible for vararg functions. Arguments are pushed on the stack from right to left. The this pointer is passed via register ECX, and not on the stack.
So with some ASM you can implement these calls yourself. But you still cannot assume the memory layout of the class, and therefore just access functions, not the member fields directly

The other option as was already mentioned is to flatten the C++ class. For this you would need to create a C++ library (also with MSVC++ to be compatible with the library at hand), and to then provide wrapper functions that wrap around the C++ class. So for with the class you posted, the wrapper functions would be:
Code: C  [Select][+][-]
  1. ...
  2. class ORIEL_USB_API COriel_USB {
  3.    /////////////////// FUNCTIONS
  4.    char * GetLibraryVersion(void);
  5.    INT32 Send(const char* strCommand, INT32 bStaticDelay = false);
  6.    INT32 Read(LPSTR strReturn);
  7.    INT32 Query(const char* strCommand, char* strReturn, INT32 bStaticDelay = false);
  8.    INT32 getCommandDelay(const char * command);
  9. };
  10.  
  11. char *Oriel_USB_Class$$GetLibraryVersion(void *instance) {
  12.   return reinterpret_cast<COriel_USB *>(instance)->GetLibraryVersion();
  13. }
  14.  
  15. INT32 Oriel_USB_Class$$Send(const char* strCommand, INT32 bStaticDelay) {
  16.   return reinterpret_cast<COriel_USB *>(instance)->Send(strCommand, bStaticDelay);
  17. }
  18.  
  19. ...

Once you have this you can compile this to either another dll which you then load in FPC, or a static library which you can statically link into the application (and therefore do not need a second DLL file in your application directory)

PascalDragon

  • Hero Member
  • *****
  • Posts: 6396
  • Compiler Developer
Re: how to use a class from a DLL
« Reply #11 on: December 19, 2022, 11:26:12 pm »
From what I can tell the header provided with the library is incomplete and wrong:
  • the extern "C" is useless considering that a class is used
  • the default visibility of a class is not public so the identifiers are not visible
  • the library exports more methods, especially a constructor and a destructor

As it's a class you need to create an instance which is then passed as an additional parameter to the methods (you instantiate some memory and then pass that to the constructor). For i386 that requires that the this is passed in the eax register which is normally not used with the cdecl calling convention. This can't be easily replicated with FPC (except with the not fully implemented cppclass). For x86_64 you can abuse this by declaring the functions with an additional first this parameter as that will be passed in rcx based on the Win64 ABI calling convention.

So the following will likely work, but only on x86_64-win64:

Code: Pascal  [Select][+][-]
  1. program toriel;
  2.  
  3. {$mode objfpc}
  4.  
  5. {$ifndef cpux86_64}
  6. {$error Code only supported for X86_64}
  7. {$endif}
  8.  
  9. const
  10.   LibName = 'Oriel_USB.dll';
  11.  
  12. procedure COriel_USB_Ctor(This: Pointer); cdecl; external LibName name '??0COriel_USB@@QEAA@XZ';
  13. procedure COriel_USB_Dtor(This: Pointer); cdecl; external LibName name '??1COriel_USB@@QEAA@XZ';
  14. function COriel_USB_GetLibraryVersion(This: Pointer): PChar; cdecl; external LibName name '?GetLibraryVersion@COriel_USB@@QEAAPEADXZ';
  15. function COriel_USB_Send(This: Pointer; strCommand: PChar; bStaticDelay: LongBool): LongBool; cdecl; external LibName name '?Send@COriel_USB@@QEAAHPEBDH@Z';
  16. function COriel_USB_Read(This: Pointer; strReturn: PChar): LongBool; cdecl; external LibName name '?Read@COriel_USB@@QEAAHPEAD@Z';
  17.  
  18. function COriel_USB_Query(This: Pointer; strCommand: PChar; strReturn: PChar; bStaticDelay: LongBool = false): LongBool; cdecl; external LibName name '?Query@COriel_USB@@QEAAHPEBDPEADH@Z';
  19. function COriel_USB_getCommandDelay(This: Pointer; command: PChar): LongInt; cdecl; external LibName name '?getCommandDelay@COriel_USB@@QEAAHPEBD@Z';
  20.  
  21. var
  22.   oriel: Pointer;
  23. begin
  24.   oriel := GetMem(1);
  25.  
  26.   COriel_USB_Ctor(oriel);
  27.  
  28.   Writeln(COriel_USB_GetLibraryVersion(oriel));
  29.   Writeln(COriel_USB_GetLibraryVersion(oriel));
  30.  
  31.   COriel_USB_Dtor(oriel);
  32.  
  33.   FreeMem(oriel);
  34. end.

At least it prints the library version twice for me. ;)

The problem with using C++ classes from a library is that this is not intendet to be possible. Not even from a C++ side, as C++ does not define a standardized memory layout (for inheritance) nor a standardized function mangling and calling conventions for linking. Some C++ compilers have added support for this, e.g. Microsoft with it's MSVC++ compiler adding the keyword __declspec(dllexport), as is used in the C++ code you posted.

Strange that the StdC++ libraries for MSVC, GCC and LLVM are all compiled as dynamic libraries then when you say that it's not intended to be possible. Might be like the myth that bumblebees are not supposed to be able to fly, but nobody told them. :P

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #12 on: December 20, 2022, 11:38:53 am »
Quote
Posted by: PascalDragon
So the following will likely work, but only on x86_64-win64:
Thank you for replying.
And please excuse my ignorance, but this is all unexplored territory for me.
To get started somewhere I just copied your code and compiled it. Running it (from the cmd window) results in a "The application was unable to start correctly (0xc000007b)" error.
Maybe it has something to do with "but only on x86_64-win64"?
I am using Lazarus v2.2.2. After the error occurred with the default options I changed Target CPU family from default to x86_64, but the error remains.
Windows version is Win 7 Pro (64 bit).

Thaddy

  • Hero Member
  • *****
  • Posts: 19249
  • Glad to be alive.
Re: how to use a class from a DLL
« Reply #13 on: December 20, 2022, 11:44:59 am »
The compiler also needs to be 64 bit.... not 32 bit.
Also note that sometimes you can call through the mangled names.
objects are fine constructs. You can even initialize them with constructors.

Rik

  • Jr. Member
  • **
  • Posts: 84
Re: how to use a class from a DLL
« Reply #14 on: December 20, 2022, 12:09:20 pm »
Quote
The compiler also needs to be 64 bit.... not 32 bit.
I thought that Lazarus 2.2.2 was 64 bit by default.
To be sure I changed the Target OS to Win64 (Project -> Project Options -> Compiler Options -> Config and Target -> Target OS).
Or is there more to do for 64 bit compilation?

Quote
Also note that sometimes you can call through the mangled names.
Sorry, what do you mean?
As said before  this is all unexplored territory for me.
« Last Edit: December 20, 2022, 12:13:29 pm by Rik »

 

TinyPortal © 2005-2018