String aus C++ dll an VBA übergeben



  • Vielleicht hat sich noch ein C++ Builder Nutzer mit diesem Problem beschäftigt:

    Ich habe einige Funktionen mit dem C++ Builder entwickelt, die aus einer dll aufgerufen werden und einen String zurückgeben.
    Diese Funktionen werden z. a. von Office-VBA-Makros aufgerufen.

    Beispiel:
    extern "C" __declspec(dllexport)char* ReadText (char* FileName);
    ...
    Funktion ReadText liest einen Text in einen AnsiString namens Ergebnis und gibt ihn schließlich zurück:
    ...
    return Ergebnis.c_str();

    In Visual Basic oder VBA wird das dann so eingebunden:

    Private Declare Function ReadText Lib "MeineDLL.DLL" (ByVal Datei As String) As String

    Das hat viele Jahr so gut funktioniert, der String wurde an Programme übergeben, die mit Visual Basic 6 oder VBA in den Office Versionen 97 bis 2003 entwickelt wurden. (Die dll funktioniert so auch problemlos beim Aufruf aus anderen C++ Builder oder Delphi Programmen)
    Bei Office 2007 und 2010 klappt das jetzt aber leider nicht mehr, sondern führt zu einem Absturz des Word-Programms.

    Hat jemand eine Idee, was da geändert sein könnte und wie man das lösen könnte ? (Ich bin erst mal dazu übergegangen, die Strings per kleine Textdateien zu übergeben, aber vielleicht geht das ja doch eleganter)



  • W. Posur schrieb:

    Das hat viele Jahr so gut funktioniert, der String wurde an Programme übergeben, die mit Visual Basic 6 oder VBA in den Office Versionen 97 bis 2003 entwickelt wurden.

    Ja, zumindest das kann ich bestätigen. Nach VB6 habe ich das allerdings nie mehr probiert.

    Hast du mal versucht die DLL zu debuggen, in dem du Word als Host-Anwendung einstellst (so heißt der Eintrag zumindest bei XE2) ? Der Builder sollte beim Debuggen dann Word starten. Kommt er dann, wenn du einen Breakpoint an den Anfang der Funktion in der DLL setzt, beim Aufruf dahin, oder stürzt Word vorher schon ab ? Wenn er reingeht, steht in dem Stringparameter das drin, was du von VB hinschickst ?



  • Der String aus Word VBA kommt korrekt in der DLL an, denn diese reagiert entsprechend darauf.
    Der Absturz von Word erfolgt dann, wenn der *char aus der DLL empfangen werden soll in einen VBA-String.
    Wenn du noch selbst solche DLL-Funktionen hast, kannst du es ja mal spaßeshalber mit einer neueren Word Version probieren.



  • W. Posur schrieb:

    Wenn du noch selbst solche DLL-Funktionen hast, kannst du es ja mal spaßeshalber mit einer neueren Word Version probieren.

    Ja, werde ich tun, wenn ich mal Zeit habe. Ein Versuch auf die Schnelle mit einer Test-DLL scheiterte, weil er der Einsprungpunkt für die Funktion nicht findet. Meine C++ Builder und VBA Kenntnisse sind etwas eingerostet, beides keine aktiven Technologien mehr. 🙂



  • So, hatte doch noch Zeit zum Test

    Code für C++ Builder XE2 (Calling Convention auf stdcall geändert)

    extern "C" __declspec(dllexport) int Foo1(char* data)
    {
    	strcpy(data, "qwertz");
    
    	return 42;
    }
    
    extern "C" __declspec(dllexport) char* Foo2(char* data)
    {
    	return "123456";
    }
    

    Code für VS2010 bzw. 2012

    extern "C" __declspec(dllexport) int  __stdcall Foo1(char* data)
    {
    	strcpy(data, "qwertz");
    
    	return 42;
    }
    
    extern "C" __declspec(dllexport) char*  __stdcall Foo2(char* data)
    {
    	return "123456";
    }
    

    Moduldefinitionsdatei für VS

    LIBRARY TestDll
    
    EXPORTS
    	Foo1
    	Foo2
    

    und dann in Word 2010

    Private Declare Function Foo1 Lib " (mein Pfad) " (ByVal Data As String) As Integer
    Private Declare Function Foo2 Lib " (mein Pfad) " (ByVal Data As String) As String
    

    geht mit

    Sub TestMitDll()
    '
    ' TestMitDll Makro
    '
    '
    Dim strA As String
    Dim strB As String
    Dim n As Integer
    
    strA = "AbcdeF"
    strB = String$(100, vbNullChar)
    
    n = Foo1(strA)
    n = Foo1(strB)
    
    End Sub
    

    aber etwas wie

    Sub TestMitDll()
    '
    ' TestMitDll Makro
    '
    '
    Dim strA As String
    Dim strB As String
    
    strA = "AbcdeF"
    strB = String$(100, vbNullChar)
    
    strB = Foo2(strA)
    
    End Sub
    

    Gibt im C++ Builder einen Crash von Word.

    In Visual Studio die Meldung "Windows hat einen Breakpoint in Winword.exe ausgelöst. [...] Wahrscheinlich wurde der Heap beschädigt."

    Fazit: String übergeben geht nur per Parameter, so hatte ich das auch noch in Erinnerung. In VB muss man vor dem Aufruf genügen Platz reservieren.



  • Danke für den Test.
    Leider verstehe ich nicht den Satz:
    String übergeben geht nur per Parameter

    Das Beispiel zeigt für mich doch nur, dass Funktionen funktionieren, die einen Integer zurückgeben (auch ein Boolean geht), nicht aber, die einen String zurückgehen.
    Foo1 geht, Foo2 macht einen Crash.

    Edit: Sorry, habs jetzt kapiert, probiere mal aus, ob String Übergabe ByVal funktioniert incl. stdcall
    Edit 2: Leider gibt es den Word Absturz auch beim Versuch, einer String-Übergabe als Referenz



  • Also bei mir funktioniert die Rückgabe, das

    strcpy(data, "qwertz");
    

    kommt im

    n = Foo1(strA)
    

    im String an und ich kann damit in VBA weiterarbeiten:

    Dim strA As String
    Dim strB As String
    Dim n As Integer
    
    strA = "AbcdeF"
    
    n = Foo1(strA)
    strB = strA + strA  
    
    'strB ist jetzt "qwertzqwertz"
    

    Beachte aber, dass ich sechs Zeichen in einen 6 Zeichen großen String gepackt habe.

    Wenn du da was größeres reinkopierst, musst du vor dem Auftruf der DLL entsprechenden Platz schaffen, so wie hier

    strB = String$(100, vbNullChar)
    

    wegen der abschliessenden Null immer mindestens ein Zeichen mehr, als du reinkopieren willst. In der Praxis macht es sicher Sinn, die Größe des Zielpuffers als zusätzlichen Parameter zu übergeben, wie in der Doku
    http://msdn.microsoft.com/en-us/library/office/aa141440%28v=office.10%29.aspx

    W. Posur schrieb:

    Edit 2: Leider gibt es den Word Absturz auch beim Versuch, einer String-Übergabe als Referenz

    ByRef ist definitiv falsch. Du übergibst einen Pointer, das muss ByVal passieren.



  • Danke,
    so scheint es jetzt zu gehen.



  • Falls dieser Thread mal gesucht wird, möchte ich abschließend noch berichten, wie ich das Problem letztlich gelöst habe (früher war es viel einfacher):
    Der rückzugebende String muß als Parameter übergeben werden und zuvor in VBA mit Nullen vorgefüllt werden. Zur Vermeidung von Speicherproblemen wird die Länge des vorgefüllten Strings von VBA aus übergeben und in C++ ggf. auf dieses Maß gekürzt. Die Funktion, die den String manipuliert gibt als Ergebnis einen Integer-Wert zurück, der die Länge des Ergebnis-Strings ausgibt. In VBA wiederum wird der manipulierte String durch die Left-Funktion auf diese Länge abgeschnitten, da er sonst rechts weitere Nullen enthält.

    Im BCB:

    extern  "C" __declspec(dllexport) __stcall int CProgramm (int Pufferlaenge, char* VBVariable);
    
    char* CProgramm (int Pufferlaenge, char* VBVariable)
    {
    AnsiString EndErgebnis = „Ergebnis“;
    int Laenge = EndErgebnis.Length();
      //wenn das Ergebnis länger ist als die übergebene Pufferlänge wird das Ergebnis entsprechend gekürzt
      if (Laenge > Pufferlaenge - 1)
       {
       EndErgebnis.Delete (Pufferlaenge, EndErgebnis.Length() - Pufferlaenge);
       Laenge = EndErgebnis.Length();
       }
      strcpy (VBVariable, EndErgebnis.c_str()); //das Ergebnis in den übergebenen String schreiben
      return Laenge; //die Länge des Strings als Integer zurückgeben
    }
    

    In VBA:

    Private Declare Function CProgramm Lib "MeineDLL.DLL" (ByVal Pufferlaenge As Integer, ByVal VBAString As String) As Integer
    
    Function CFunktion (sUebergabesgtring As String) As String
     Dim iErgebnislaenge As Integer
     Dim iPuffer As Integer
     iPuffer = 1500 'Höchstmögliche Länge des Strings
     sUebergabestring = String$(iPuffer, vbNullChar) 'den String  mit Nullen vorfüllen
     iErgebnislaenge = CProgramm (iPuffer, sUebergabestring) 'gibt als Resultat die Stringlänge zurück
     sUebergabestring = Left(sUebergabestring, iErgebnislaenge)  'rechts die nicht benötigten Zeichen abschneiden
    CProgramm = sUebergabstring
    End Function
    


  • char* als Rückgabetyp ist wohl falsch 😉

    Früher war gar nichts einfacher, das war schon immer so, schon zu VB3.0 und Turbo C++ 😮 Wenn man in Speicher schreibt der einem nicht gehört macht es immer BUMM.



  • Danke, war ein Tippfehler, hab es korrigiert.
    Früher war es doch einfacher. Die C++ Funktion hat ein char* zurückgegeben und selbst für die Speicherreservierung gesorgt. Es handelt sich ja nur um einen Pointer auf eine Zeichenkette. Diese Funktion in VBA importiert konnte dort das Ergebnis String liefern. Das hat noch in VB6 und in VBA bis Office 2003 so gut funktioniert.



  • W. Posur schrieb:

    Danke, war ein Tippfehler, hab es korrigiert.
    Früher war es doch einfacher. Die C++ Funktion hat ein char* zurückgegeben und selbst für die Speicherreservierung gesorgt. Es handelt sich ja nur um einen Pointer auf eine Zeichenkette. Diese Funktion in VBA importiert konnte dort das Ergebnis String liefern. Das hat noch in VB6 und in VBA bis Office 2003 so gut funktioniert.

    Sicher nicht, wenn das bei Dir so funktioniert hat, dann war das Zufall.
    Dann hsat Du keine variable langen Strings in VBA sondern fixlange Strings verwendet.



  • An Zufall glaube ich nicht. Es funktioniert ja immer noch mit Strings variabler Länge in älteren VBA-Versionen sowie in OpenOffice und LibreOffice neuen Datums. Stringlängen über 65000 habe ich allerdings nicht ausprobiert. Warum sollte ein vernünftiges Basic auch nicht in der Lage sein, zu erkennen, daß es von einer externen Funktion einen Zeiger auf eine Null-terminierte Zeichenfolge bekommt und diese in einen Basic-String umzuwandeln ?
    Wie auch immer, ich vermute mal, die Änderungen im VBA sind durch die Unicode Funktionalität verursacht.
    Mittels weiterer Recherche habe ich jetzt doch noch eine viel elegantere Methode gefunden, damit VB und VBA einen String bekommen. Ich liefere eine BSTR anstatt einen char* zurück, damit kommt auch das neue VBA klar.

    Im BCB:

    extern  "C" __declspec(dllexport) BSTR _stdcall CProgramm (); 
    
    BSTR _stdcall CProgramm () 
    { 
    AnsiString EndErgebnis = „Ergebnis“;  //das ist der AnsiString, den meine Funktion ermittelt hat und nun an VBA liefern soll 
    BSTR BErgebnis; // diesen BSTR String wird die Funktion zurückgeben 
    int iLaenge = EndErgebnis.Length(); //die Länge des Ergebnis-AnsiStrings wird ermittelt 
    
    LPSTR strSrc = (LPSTR)EndErgebnis.c_str(); //Umwandlung Ergebnis in LPSTR
    LPSTR strDst = (LPSTR) BErgebnis; //dito für Ausgabestring
    for(int i=0; i<= iLaenge; i++)
     *strDst++ = *strSrc++; //Umkopieren
    
    return BErgebnis; //der Ergebnisstring vom Typ BSTR wird zurückgeliefert 
    }
    

    Und in VBA ist es jetzt wieder ganz einfach:

    Private Declare Function CProgramm Lib "MeineDLL.DLL" () As String
    


  • Du vergleichst Äpfel mit Birnen 😃

    BSTR ist etwas ganz anderes als char*



  • Nun ja.
    Mein Thema war: Wie schaffe ich es, mit dem BCB eine dll zu schreiben, die einen String zurückgibt, mit dem VBA auch bei neueren Office Versionen etwas anfangen kann.
    Ich habe das Problem für mich gelöst.
    Ich bedanke mich für die konstruktiven Beiträge.



  • hallo,

    habe ein ähnliches problem. teste zunächst mit einer double variable, später soll es mal ein array werden.

    also meine Test-Funktion sieht folgendermaßen aus:

    extern  "C" __declspec(dllexport) double _stdcall square(double & x);
    
    double _stdcall square(double & x)
    {
    	return x*x;
    }
    

    dann braucht man wohl noch ein def-File:

    LIBRARY "square"
    EXPORTS
    square
    

    dann muss man das ganze noch verlinken. hab ich nach dieser anleitung gemacht:
    http://www.forexfactory.com/showthread.php?p=5250053#post5250053

    In VBA dann:

    Declare Function square _
    Lib "E:\C++\dll_datei\Release\square.dll" (ByRef x As Double) As Double
    

    leider läuft es dann in VBA nicht. Die Function taucht zwar auf, aber es erscheint "#WERT!" in der Zelle... Hab ich nen Fehler gemacht?



  • ootobbyoo schrieb:

    dann braucht man wohl noch ein def-File:

    Beim Builder eigentlich nicht, die Beispiele oben mit def-File waren für VC++.

    ootobbyoo schrieb:

    Hab ich nen Fehler gemacht?

    Ich denke "ByRef x as Double" entspricht einem "double* x", glaube nicht, dass VB C++ Referenzen unterstützt.



  • nn schrieb:

    ootobbyoo schrieb:

    dann braucht man wohl noch ein def-File:

    Beim Builder eigentlich nicht, die Beispiele oben mit def-File waren für VC++.

    mh, also ohne def-File läuft es auch nicht. Programmiere mit ecplise und compiliere dann mit dem TDM GCC für 64 bit (http://tdm-gcc.tdragon.net/download).

    Leider auch keine Änderung, wenn ich das "ByRef" weg lasse.

    Ich habe die Funktion in VBA mal in eine Sub eingebaut, da kommt komischerweise die Fehlermeldung:

    "Lautzeitfehler '48':
    Datie nicht gefunden: E:\C++\dll_datei\Release\square.dll"

    Scheint also, als ob er die dll gar nicht erkennt. Da ist sie aber auf jeden Fall...



  • ootobbyoo schrieb:

    Programmiere mit ecplise und compiliere dann mit dem TDM GCC für 64 bit (http://tdm-gcc.tdragon.net/download).

    ...

    Scheint also, als ob er die dll gar nicht erkennt. Da ist sie aber auf jeden Fall...

    Also die Beispiele hier waren für den C++ Builder und 32-Bit.

    Mit 64-Bit Excel habe ich sowas noch nie gemacht, dass ein 32-Bit Excel keine 64-Bit DLL lädt ist klar. In 64-Bit Code sollte doch schon mal __stdcall überflüssig sein ?



  • ALSO,

    erstmal ist es korrekt, dass man mit Excel 32 bit keine 64 bit dll aufrufen kann. Danke für den Hinweis. Der GCC kann aber mit dem Befehl "-m32" auch auf 32 bit comilieren.

    Dann muss der Name der Function in VBA und der Name der Function in C++ gleich sein!!! Und dann war wohl noch an der Syntax was falsch.

    Hab auf jeden Fall eine Lösung gefunden, die dann so aussieht:

    function.cpp:

    extern "C" __declspec(dllexport) double __stdcall square(double x)
    {
        return x*x;
    }
    

    deffile.def:

    LIBRARY "square"
    EXPORTS
    square
    

    Compiler & Linker:

    g++ -m32
    

    und in VBA dann

    Option Explicit
    
    Declare Function square Lib "E:\C++\dll_datei\Release\dll_datei.dll" (ByVal d As Double) As Double
    
    Sub x()
       Dim zw As Double
       zw = square(10)
       MsgBox zw
    End Sub
    

    Und jetzt das ganze mit Arrays...


Log in to reply