Link to home
Start Free TrialLog in
Avatar of inversojvo
inversojvoFlag for Germany

asked on

Calling dll from VBA and getting a big complex UDT as a return value - "bad calling convention" error.

I've a C dll (from the third company - so I can't change and recompile it) accessing a GIS-System database. This is not a COM dll - so the functions can be accessed from VBA only with "Declare". This dll has 6 functions, that have to be called in a logical order to get my query to GIS db "answered".

From the C header file the parts that seem to be relevant (I'm not a C programmer - so I may make a mistake), look as following:

1) UDT structures:

typedef struct SVersionsInfo
{
    char Version[6];
    char Zusatz[6];
    TDatum DatTabellen;
    char Beschreibung[256];
} TVersionsInfo;

// Attribute der Entitдt "GisRisiko":
typedef struct SGisRisiko
{
    char Sturm[2];
    char Erdsenkung[2];
    char Ueberschwemmung[2];
} TGisRisiko;

// Attribute der Entitдt "GisAbfrageParameter":
typedef struct SGisAbfrageParameter
{
    TBool befuelleTabellen;
    TBool KzRisikoKlassifizierung;
    TBool exakteSuche;
    char PLZ[7];
    char ID_PLZ[8];
    char Ort[51];
    char ID_Ort[8];
    char Strasse[51];
    char ID_Strasse[8];
    char Hausnummer[51];
    char ID_Hausnummer[8];
} TGisAbfrageParameter;

// Attribute der Entitдt "TGisDatenSet":
typedef struct SGisDatenSet
{
    TBool befuellt;
    //TBool gefunden;
    TBool eindeutig;
    short Status_PLZ;
    char ID_PLZ[8];
    short Status_Ort;
    char ID_Ort[8];
    short Status_Strasse;
    char ID_Strasse[8];
    short Status_Hausnummer;
    char ID_Hausnummer[8];
    TAlphanumListbox LbPLZ;
    TAlphanumListbox LbOrt;
    TAlphanumListbox LbStrasse;
    TAlphanumListbox LbHausnummer;
    TGisRisiko RisikoKlassifizierung;
} TGisDatenSet;

// Attribute der Entitдt "TGisAbfrage":
typedef struct SGisAbfrage
{
    TMeldung *Meldung;
    TVersionsInfo *VersionsInfo;
    TGisAbfrageParameter *GisAbfrageParameter;
    TGisDatenSet *GisDatenSet;
} TGisAbfrage;

// Boolean-Typ
typedef short TBool;                            // 19.01.98, S.J.
#ifndef SWIG
#define false   0                               // 19.01.98, S.J.
#define true    1                               // 19.01.98, S.J.
#endif

// Datum-Typ
typedef struct SDatum
{
    short           Jahr;
    unsigned short  Monat;
    unsigned short  Tag;
} TDatum;

// Wertepaar von zwei Zeichenketten
typedef struct SAlphanumWertepaar
{
    char    Schluessel[51]; // 18.12.97, S.J.
    char    Wert[51];
} TAlphanumWertepaar;

// Listbox mit alphanumerischem Schlьssel
typedef struct SAlphanumListbox
{
    short               Anzahl;
    TAlphanumWertepaar  *Werte;
    char                AktuellerSchluessel[51];    // 18.12.97, S.J.
} TAlphanumListbox;

// Definition der Ьbergabestruktur fьr Meldungen der DLLs
typedef struct SMeldung
{
    char    Typ;        // 18.12.97, S.J.
    short   Nummer;     // 18.12.97, S.J.
    char    Text[256];  // 15.06.98, S.J.
    char    Hilfe[51];  // 15.06.98, S.J.
} TMeldung;

2) exported functions (in the order of a usual call):

//Exportierte Funktionen
//
short API_EXPORT initGisAbfrage(char* PfadDLL, char *PfadDaten);

TGisAbfrage* API_EXPORT erzeugeAbfrageStruktur(void);

TBool API_EXPORT ausfuehrenAbfrage(TGisAbfrage*);

TBool API_EXPORT loescheAbfrage(TGisAbfrage*);

TBool API_EXPORT loescheAbfrageStruktur(TGisAbfrage*);

TBool API_EXPORT exitGisAbfrage(void);

----------------------------------------------------------------------------------------------------------------
In VBA the test module has the following code:

Public Type Bool
    Bool As Integer
End Type

Public Type Datum
    Jahr As Integer
    Monat As Integer
    Tag As Integer
End Type

Public Type AlphanumWertepaar
    Schluessel As String * 51
    Wert As String * 51
End Type

Public Type AlphanumListbox
    Anzahl As Integer
    Werte As AlphanumWertepaar
    AktuellerScluessel As String * 51
End Type

Public Type VersionsInfo
    Version As String * 6
    Zusatz As String * 6
    DatTabellen As Datum
    Beschreibung As String * 256
End Type

Public Type GisRisiko
    Sturm As String * 2
    Erdsenkung As String * 2
    Ueberschwemmung As String * 2
End Type

Public Type GisAbfrageParameter
    befuelleTabellen As Bool
    KzRisikoKlassifizierung As Bool
    exakteSuche As Bool
    PLZ As String * 7
    ID_PLZ As String * 8
    Ort As String * 51
    ID_Ort As String * 8
    Strasse As String * 51
    ID_Strasse As String * 8
    Hausnummer As String * 51
    ID_Hausnummer As String * 8
End Type

Public Type GisDatenSet
    befuellt As Bool
    eindeutig As Bool
    Status_PLZ As Integer
    ID_PLZ As String * 8
    Status_Ort As Integer
    ID_Ort As String * 8
    Status_Strasse As Integer
    ID_Strasse As String * 8
    Status_Hausnummer As Integer
    ID_Hausnummer As String * 8
    LbPLZ As AlphanumListbox
    LbOrt As AlphanumListbox
    LbStrasse As AlphanumListbox
    LbHausnummer As AlphanumListbox
    RisikoKlassifizierung As GisRisiko
End Type

Public Type Meldung
    Typ As Byte
    Nummer As Integer
    Text As String * 256
    Hilfe As String * 51
End Type

Public Type GisAbfrage
    Meldung As Meldung
    VersionsInfo As VersionsInfo
    GisAbfrageParameter As GisAbfrageParameter
    GisDatenSet As GisDatenSet
End Type

Const PfadDll As String = "d:\Gis"
Const PfadDaten As String = "d:\Gis\Daten"

Declare Function initGisAbfrage Lib "D:\Gis\GisAbfrage32.dll" (ByVal str1 As String, ByVal str2 As String) As Integer
Declare Function erzeugeAbfrageStruktur Lib "D:\Gis\GisAbfrage32.dll" () As GisAbfrage
Declare Function ausfuerenAbfrage Lib "D:\Gis\GisAbfrage32.dll" (GAbfrage As GisAbfrage) As Boolean
Declare Function loescheAbfrage Lib "D:\Gis\GisAbfrage32.dll" (GAbfrage As GisAbfrage) As Boolean
Declare Function loescheAbfrageStruktur Lib "D:\Gis\GisAbfrage32.dll" (GAbfrage As GisAbfrage) As Boolean
Declare Function exitGisAbfrage Lib "D:\Gis\GisAbfrage32.dll" () As Boolean

Sub test()

Dim intResult As Integer
Dim GAbfrage As GisAbfrage

Dim blnTmp As Boolean
Dim intTmp As Integer
Dim lngTmp As Long

Const sProcSig As String = MODULE_NAME & "test"
   
On Error GoTo PROC_ERR
    intResult = RESULT_OK

    intTmp = initGisAbfrage(PfadDll, PfadDaten)
    GAbfrage = erzeugeAbfrageStruktur()
    blnTmp = ausfuerenAbfrage(GAbfrage)
    blnTmp = loescheAbfrage(GAbfrage)
    blnTmp = loescheAbfrageStruktur(GAbfrage)
    blnTmp = exitGisAbfrage()
   

PROC_EXIT:
    Exit Sub
   
PROC_ERR:
    intResult = RESULT_ERROR
    If errTypeReport(sProcSig) <> Critical Then Resume Next
    Resume PROC_EXIT
    'Resume
End Sub
--------------------------------------------------------------------------------------------------------------------

I get an error "bad calling convention", Nr. 49 for this line:

    GAbfrage = erzeugeAbfrageStruktur()

and think, that the UDT, that is to be created with erzeugeAbfrageStruktur() in C dll differs from the UDT I've tried to mirror in VBA module. The UDT GAbfrage has a complex structure (appr. 60 primitive variables, differently grouped). How can I make the test function run?

Some colleague of mine had made this dll work for Java programm, but for mapping the UDT from C to Java they used "SWIG" tool (http://www.swig.org). I haven't found smth similar to map the structure to VB-VBA.
I assume that the order and the variables type in my UDT are the keys, but how to check the conformity of allocated memory in C and in VBA? Are there some special tools? As the UDT is big and complex, I can't test it part by part (or just don't know, how to test it by parts :( ). The function runs or doesn't run and to have it run, I have to have the whole UDT correct.

So, please help.

Avatar of AzraSound
AzraSound
Flag of United States of America image

Can you declare erzeugeAbfrageStruktur to return a Long (so that you get a pointer to the data)?  Or to return a Variant?  Or possibly even "As Any"?  It's hard to follow what the original structure looks like to see if its possible to mimic with a VB UDT directly.
SOLUTION
Avatar of EDDYKT
EDDYKT
Flag of Canada image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hi inversojvo,
If you can't recompile the DLL, you're probably out of luck... unless the functions were exported with the pascal calling convention then they will be popped off the stack in the opposite order that VB pushed them onto the stack... thus the bad calling convention... about the only other thing you could do is create a "wrapper" dll which would have functions which are exported with the pascal calling convention, and those functions in turn call the original dll, get the results back and relay it back to your application.

Cheers!
Avatar of inversojvo

ASKER

Unfortunately, can't prove the ideas immediately - code will be available again only on Monday :(.

2 AzraSound : I'll try with long and Variant and will report results. I've already tried with Any and got an error (compillation error, as far as I remember).

2 EDDYKT : does such type exist? I've just tried in VBA under Access and also got "Compillation Error" - "End of expression expected" (all error messages are translated from German)

2 mdougan : yeas, you've right (I read about it just today), that there are 2 conventions (and that one of them can cause this error 49). But I looked throught the C header file and , and noticed smth. with _std.., not with _cdecl, so this aspect seems to be OK.

Well, if you've been able to call any of the function successfully, then I'd have to agree with you.  Calling C Dlls is further complicated by the fact that String handling in VB is dynamic, whereas C is expecting Fixed Length strings, so, often, you have to be careful declaring string variables to send to the C program, and you have to use the CopyMemory API to copy any strings that were created in the C DLL and passed back (unless you passed a string buffer by reference which the DLL filled).  The best source of samples for calling complex C DLL routimes with complex structures is Karl Peterson's website...

I haven't been there for .NET code yet, but you might gain a lot from reading though one of his samples...

http://www.mvps.org/vb/

download the sample NetUser.zip or NetWksta.zip ... even if you don't have VB 6 installed, at least you can edit the class files in notepad or something and see what he does.
So, I've tried with "long" as a return type and at least I didn't get an error - seems to be the right direction. Suppose, that now I get the "pointer" to the structure as long:
lngTmp = erzeugeAbfrageStruktur()

Resuming the state, in test functions:

    intTmp = initGisAbfrage(PfadDll, PfadDaten)
    GAbfrage = erzeugeAbfrageStruktur()
    blnTmp = ausfuerenAbfrage(GAbfrage)
    blnTmp = loescheAbfrage(GAbfrage)
    blnTmp = loescheAbfrageStruktur(GAbfrage)
    blnTmp = exitGisAbfrage()

2 first functions run without error and now I get the error 453 (dll entry point is not found) already in 3-rd function:
blnTmp = ausfuerenAbfrage(GAbfrage)

I remind, that in C header the correspondent fucntion looks so:
TBool API_EXPORT ausfuehrenAbfrage(TGisAbfrage*);

I made 2 corrections, trying to debug the error:
1)  changed the type of return value to Integer, as TBool in C is of type "short" in fact
2) changed the type of parameter in declaration and in calling:

Declare Function ausfuerenAbfrage Lib "D:\Gis\GisAbfrage32.dll" (ByVal lng As Long) As Integer
...
Sub test()
...
    intTmp = ausfuerenAbfrage(ByVal lngTmp)
...
End Sub

but without success :( - the same error 453.

Might it be, that the error this time is caused by the fact, that the structure, pointed with this lngTmp, is not filled correctly with my values (it's not filled at all at this stage, in fact)? The pointed structure is divided (I think) according to C-types, with all these "unsigned short" and TBool that-is-Integer-in-fact, and so on. But it has to say smth about wrong arguments or again about "bad calling conventions", not that the "entry point is not found".

What do you think?

SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
OK, I did it, but the error is the same - 453, when I try to run
blnTmp = ausfuerenAbfrage(GAbfrage)

I'm not sure, that the structure, pointed by lngPointer (created by dll function), fits exactly to the structure of my GAbfrage. How can I see the dll structure (or, at least, the length of this structure)?
After the CopyMemory call, set a breakpoint and look at the contents of the GAbfrage structure and see if it looks correct or not.
In fact, the question :
"How can I recognize the structure of UDT, got from C dll, if I get the pointer to this structure"
seems to be big enough to make it a separate question, worth, f.ex. 400 points, doesn't it ? :)
I don't know, what has to be there :(.
The initialized structure has to be an empty structure, in fact. Then I have to fill some parts of it with my values, and these values have to play a role of parameters ("Abfrage" means "Query" in German). And then I have to treat the values of some other structure's fields as a result (or results, if there are some).
Error 453 means your function declaration is incorrect?  Which function is it that raises the error?
damn!! How stupid am I!
Sorry, AzraSound, you pointed right - the name was written wrong, in fact. So, I corrected it and now I'm trying to make the function ausfuerenAbfrage() run.


Here are all relevant declarations for this functon:

Public Type Bool       ' type Bool corresponds to C declaration:      typedef short TBool;
    Bool As Integer
End Type

Declare Function ausfuerenAbfrage Lib "D:\Gis\GisAbfrage32.dll" (ByVal lng As Long) As Bool
' corresponds to C declaration:     TBool API_EXPORT ausfuehrenAbfrage(TGisAbfrage*);

And the test function:

Sub test()

Dim GAbfrage As GisAbfrage
Dim b As Bool
Dim lngTmp As Long

    intTmp = initGisAbfrage(PfadDll, PfadDaten)
    lngTmp = erzeugeAbfrageStruktur()
    CopyMemory ByVal GAbfrage, lngTmp, LenB(GAbfrage)

    b = ausfuehrenAbfrage(ByVal lngTmp)
...
End Sub

So, while trying to execute the expression:      b = ausfuerenAbfrage(ByVal lngTmp)
I get the crash Access error :
"The expression in "0x00000000" refers to memory address in "0x00000000". A try to "read" can't be executed for the memory. Click OK - to close the programm, Click Cancel - to debug"
When I click OK, the Access is shut down.

So, I think, I give the wrong structured (and wrong sized) argument to the function ausfuehrenAbfrage().

I've just opened as the extra question "how to get in VBA the UDT structure, if I have only the pointer":
https://www.experts-exchange.com/questions/21163658/How-can-I-recognize-parse-the-structure-of-UDT-returned-from-C-dll-if-I-get-the-pointer-to-this-structure-in-a-VBA-function.html

What about the initial declaration?

Declare Function ausfuerenAbfrage Lib "D:\Gis\GisAbfrage32.dll" (GAbfrage As GisAbfrage) As Boolean

Can you call:

b = ausfuerenAbfrage(GAbfrage)
No, the same error <.. memory "0x00000000" ...> and Access crashes. :(.

Don't you think, that the unproper structure could cause it?
It does sound like it.  Part of its is the char datatypes that you are turning into fixed length strings.  Strings in VB are unicode, two bytes, versus one byte that a char is.  EDDYKT's initial suggestion was along the right path, just the syntax was a bit off.  E.g., instead of:

s As String * 10

you would use

s(1 to 10) As Byte


However these arrays would introduce some issues with our CopyMemory idea, but if we can get the structure into a workable form, you may be able to use the structure directly as you originally intended to do.
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
OK. Then enough for today, and tomorrow in the morning I'll try the following:
I'll list the whole structure somewhere in a column of Excel sheet (all elements, a line for each elementar type), write in next column the sizes in C, in next one - the sizes in VB. If any of size can't be mapped direct, I'll create a small UDT made from Byte array of proper size to map it. Smth like:

Type uStr10
    uVal(1 to 10) as Byte
End Type

And after that I'll try CopyMemory again.

Could you please, confirm the following correspondences btw. C and VBA?:

char [NN] --> uStrNN
short --> int
unsigned short  --> int (?? The value might have to be read differently, but the size anyway has to be OK, hasn't it?)

There is another thing I'm afraid of - the DWORD alignment (4 Bytes alignment) in structures.. But we'll think of it, if all abovementioned will fail.



2 Mike - while I was writing my answer, you posted smth interesting. I'm now away for some hours, then will read your advice attentively - it sounds rather promissing, what you've written!
2 Mike:
Yes, you are right about long pointer instead of UDT. To check it I made a dummy dump array (1 to 2000) of bytes and copied there first 2000 bytes beginning from the pointed address. And I got only 8 first bytes filled with values > 0. The following were just zeros.
So, it's clear for me now that I shoud fill all members with its own CopyMemory. But what have I do, if the pointer is somewhere deeper in the structure? I'll explain what I mean:

In GisAbfrage I have a pointer to the member GisDatenSet, that has in its own structure another member of type AlphanumListbox:

{
Bool befuellt
...
AlphanumListbox LbPLZ
AlphanumListbox ...
}

and this type AlphanumListbox has 3 members:

{
short Anzahl
AlphanumWertepaar *Werte
char AktuelleSchluessel
}

One of them - AlphanumWertepaar *Werte is also a pointer to another substructure of 2 variables of type char[51].

May I resume your advise so, that to fill the whole "GisAbfrage" correct, I have to fill one after another 3 levels of structures:

1-st level:  UDT GisAbfrage with 4 long values, as they are pointers to 4 members

2-nd level: each of these 4 members, according to their structures, at that all AlphanumListbox will made of  
       - int (analog of short)
       - long (as a pointer to the structure AlphanumWertepaar)
       - byte(1 to 51) (analog of char[51])

3-rd level: all UDT of type AlphanumListbox with 2 arrays of bytes(1 to 51)

?

Yes, that would be my understanding... but you only need to do this for the UDTs that contain pointers to other UTDs, or pointers to strings.  Many of the Microsoft APIs use LPSTR and it's necessary to do the same for them.
This sample from NetDomain.zip of Karl Peterson seems really great and can be a good prototype. I hope tomorrow the test will run OK. :)
Great!  Let us know how it goes!
so, the reading from C UDT at first glance seems OK, but there are 2 strange behaviours, that I don't understand.
To check the correspondence between created structures & memory containt at least somehow I filled the structures AND also the dump of bytes from the same memory fragments.
(Type str_NN is array of NN bytes)

1. in the substructure "Meldung":

Public Type Meldung
    Typ As Byte
    Nummer As Integer
    Text As str_256
    Hilfe As str_51
End Type
...
dim GMeldung as Meldung

And now, when I check the lenght of elements and the length of the whole structure, I've the discrepence in total lenght:

?len(GMeldung.Typ)
 1
?lenb(GMeldung.Typ)
 1

?len(GMeldung.Nummer)
 2
?lenb(GMeldung.Nummer)
 2
 
?len(GMeldung.Hilfe)
 51
?lenb(GMeldung.Hilfe)
 51

?len(GMeldung.Text)
 256
?lenb(GMeldung.Text)
 256

?len(GMeldung)
 310
?lenb(GMeldung)
 312                  (!!!)

I think, that it was such called "padding"-alignment, and the size of structure was aligned to fit 4-bytes blocks.


2. Substructure VersionsInfo:

Public Type VersionsInfo
    Version As str_6
    Zusatz As str_6
    DatTabellen As Datum
    Beschreibung As str_256
End Type

where:

Public Type Datum
    Jahr As Integer
    Monat As Integer
    Tag As Integer
End Type

Here the C structure Datum was declared as:

typedef struct SDatum
{
short Jahr;
unsigned short Monat;
unsigned short Tag;
} TDatum

(Jahr-Monat-Tag are Year-Month-Day)

For Year in dump I have 211 in the first byte and 7 in the second one. In Datum I see 2003. Have I undertand it correct, that the bytes are stored in memory in inversed order?
211 in binar is (1101 0011)
7    in binar is (0000 0111)

but 2003  is (0000 0111 1101 0011)
--------------------------------------------------------------------------------------------------
So, I'm asking to confirm my abovementioned understanding.
--------------------------------------------------------------------------------------------------

Unfortunately, I continue to get the Acces crashed when I try to run the next function ausfuehrenAbfrage(lngPointer) :(.

BUT: inspite of it, I would like to close THIS question, and, probably open ANOTHER question today-tomorrow about reverse task - creating the structure to pass to C dll from VB UDT (if I can't solve it alone). That's why I would like to thank ALL who participated in this thread, especially AzraSound and mdougan - guys, you are smart and were very helpful!!

I propose to split the points as following:

mdougan - 300
AzraSound - 150
EDDYKT - 50

Does it seem fair to you or have I change the sharing rate somehow ?


Yuriy.
Two details were forgotten -
1) the ASCII code for 2 extra symbols (that seems to be a "padding") is 253.
Is it always with this symbol does the structures padding occur ?
2) the size of dump for "VersionsInfo" seems to be 274 bytes - not quite divisible by 4, hm...
>>I think, that it was such called "padding"-alignment, and the size of structure was aligned to fit 4-bytes blocks

That is correct.  As far as the symbol used for this "padding", I would assume it would consistent, at least in this instance, but I cannot verify for certain.


>>Have I undertand it correct, that the bytes are stored in memory in inversed order?

Yes this is the discrepancy between internal Little Endian and Big Endian.  In my projects I am often calling routines to swap byte orders for use.
Hi,

Couple of things... yes, the difference between len and lenb is that it will be aligning the contents on a byte boundry... but that is why you should not use lenb unless the functions you are calling require this alignment.

Your problem with integer in the sdatum structure may be caused by the fact that both Visual Basic's integer and long, under 32-bit operating systems actually take up the same amount of storage, 4 bytes, it's just that VB packs the 2 low order bytes with data for integers and uses all 4 bytes for longs... which is why it is just as efficient under a 32-bit OS to use longs as integers, it's just that integers will be limited in contents to a number that can be represented in 2 bytes as opposed to 4.  Len, is probably looking only at the contents, ignoring the binary zeros in the high order bytes and giving you a length of 2.

You should check with Microsoft on the actual storage size of integers, but I believe that you will find that this is true... in which case, you probably need to change the definition of all columns that map to a short to something else... maybe a byte array of 2?

If you have the windows.h file available, you can go and look at the function declarations for the Windows API functions, look for one that references a short data type... then, in your Visual Studio, bring up the Windows API declaration viewer which already shows the translated function declarations... you can see what Microsoft used to translate the short data type to....

As for filling the UDT and sending it to the C DLL, my comment above where I used the VARPTR (undocumented VB function) is how you'd go about creating the structure to send, except before making  your call to the DLL, you'd fill the values in your local instances of the structures.

AzraSound got you pretty far on a lot of the CopyMemory stuff, so, I don't mind sharing more points with them.
by the way, does somebody need a Gmail invitation? I can issue at least 5.