C++ dll for VBA - problem with reading/encoding strings - c++

I need to read entries from old-fashioned INI-file with a DLL and after some manipulation export them to VBA code.
The c++ code for the function looks like this:
BSTR __stdcall GetSectionEntry(LPCSTR sSection, LPCSTR sEntry)
{
LPCTSTR gszINIfilename = "C:\\test\\test.ini";
TCHAR localStr[64];
GetFileAttributes(gszINIfilename);
if (INVALID_FILE_ATTRIBUTES == GetFileAttributes(gszINIfilename) && GetLastError() == ERROR_FILE_NOT_FOUND) {
bstr_t bstrt = "";
return bstrt;
} else {
GetPrivateProfileStringA(sSection, sEntry, "", localStr, sizeof(localStr), gszINIfilename);
//_bstr_t bstrt(localStr);
CComBSTR bstrt(localStr);
return bstrt;
}
}
The VBA (Word 2016):
Declare PtrSafe Function GetSectionEntry Lib "test.dll" (ByVal sSection As String, ByVal sEntry As String) As String
For i = 0 To 5
Debug.Print ("Name = " & StrConv(GetSectionEntry("Name", CStr(i)), vbFromUnicode))
Next i
End finally the INI-file:
[Name]
0=123456789012345678901234567
1=1234567890123456789012345678
2=1234567890123456789012345678901234567890123456789012345678901
3=12345678901234567890123456789012345678901234567890123456789012
I compiled the DLL using VS2019 with either "Character Set" set to "Not Set" or to "Use Multi-Byte Character Set".
Debugging in VS shows that the strings are properly formatted (for example for 1 it's L"1234567890123456789012345678"). I tested it with different sizes of localStr (64..1024). I tried bstr_t and CComBSTR and a few more. In all cases print.debug in VBA shows following result:
Name = 123456789012345678901234567
Name = ?????????????? ????????????
Name = ??????????????????????????????1 ?? ??????????????yu ? rg|A ?
Name = 12345678901234567890123456789012345678901234567890123456789012
Any string that is longer than 27 and shorter than 62 characters is not properly encoded. When I checked them without StrConv(string, vbFromUnicode) they were missing all even null-characters.
For that reason they are then encoded as some Asian characters:
The same happens for string that are 13--14 character long:
I'm very sure that I'm doing something trivial and stupid but I never had to write any C/C++ for windows. I would be grateful if anyone could point my mistake and correct me.

CComBSTR bstrt(localStr);
return bstrt;
This return statement converts the CComBSTR to a BSTR, and returns that, and then destroys the CComBSTR since it's no longer in scope. When the CComBSTR object is destroyed the string is freed.
Try return bstrt.Detach(); which detaches the string from the CComBSTR object so it won't be freed by the destructor.
After you return it, the string "belongs" to VBA and it's VBA's job to free it, which I assume it does automatically.

Related

How to use SHFileOperation() with CString paths

I am trying to convert CString to LPCWSTR and it works well. But something went wrong in the processing of the code.
I want to copy a directory to another path so I am using SHFILEOPSTRUCT:
HWND console = GetConsoleWindow();
SHFILEOPSTRUCT s = { 0 };
s.hwnd = console;
s.wFunc = FO_COPY;
s.fFlags = FOF_SILENT;
CString _folderName("a6_töüst-Oa5Z.OZS-CI5O5235"),
firstPath("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Temp\\"),
lastPart("\\Documents\\*\0"),
firstPathDest("C:\\ORTIM-Daten\\a5Pc 2.0.3\\"),
lastPartDest("Documents\\"),
_folderNameDest("a6_töüst-Oa5Z.OZS-CI5O5235\0");
CString cstrTemp = firstPath + _folderName + lastPart,
cstrTempDest = firstPathDest + lastPartDest + _folderNameDest;
s.pTo = cstrTempDest /*_TEXT("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Documents\\a6_töüst-Oa5Z.OZS-CI5O5235\0")*/;
s.pFrom = cstrTemp /*_TEXT("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Temp\\a6_töüst-Oa5Z.OZS-CI5O5235\\Documents\\*\0")*/;
SHFileOperation(&s);
When I am using CString directly, the copy operation doesn't work, but when I use the _TEXT() macro (as in the comments) to assign the LPCWSTR members in the struct everything works.
EDIT 1
In both variants of source and destination paths the code compiles.
In this variant, the code compiles and does the copy operation:
s.pTo = _TEXT("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Documents\\a6_töüst-Oa5Z.OZS-CI5O5235\0");
s.pFrom = _TEXT("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Temp\\a6_töüst-Oa5Z.OZS-CI5O5235\\Documents\\*\0");
In the other variant, which I actually need, the code compiles too, but the copy operation doesn't take place:
s.pTo = cstrTempDest;
s.pFrom = cstrTemp;
SHFILEOPSTRUCT expects strings ending with two NUL characters, but NUL terminated strings by definition end with one and any additional NUL characters are ignored by CString methods that don't take explicit length argument.
You can force double NUL by adding one manually:
CString cstrTempDest = firstPathDest + lastPartDest + _folderNameDest;
// *** Add NUL manually ***
cstrTempDest.AppendChar( 0 );
s.pTo = cstrTempDest;
// For debuging - verify resulting string with example.
TCHAR* test = _TEXT("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Documents\\a6_töüst-Oa5Z.OZS-CI5O5235\0");
// +2 because we want to check two NULs at end.
ASSERT( memcmp( s.pTo, test, (_tcslen(test)+2)*sizeof(TCHAR) ) == 0 );
Alternative solution can use methods with explicit length argument:
CString cstrTempDest = firstPathDest + lastPartDest
+ CString(_folderNameDest, _tcslen(_folderNameDest)+1);
If your project is configured to use unicode character set, call CString constructors with wide strings:
CString _folderName(_T("a6_töüst-Oa5Z.OZS-CI5O5235")),
firstPath(_T("C:\\ORTIM-Daten\\a5Pc 2.0.3\\Temp\\"))
...
CString in unicode mode automatically converts narrow strings to wide ones, but it can fail when threre is discrepancy between runtime and development codepages. If you plan to go Unicode and never look back, throw away _TEXT, TEXT and _T macros and just use wide literals:
CString _folderName( L"a6_töüst-Oa5Z.OZS-CI5O5235" ),
firstPath( L"C:\\ORTIM-Daten\\a5Pc 2.0.3\\Temp\\" )
...
You should also check SHFileOperation return value.
The answer of user msp0815 on creating double null ended CString solves your issue.
// strings must be double-null terminated
CString from(cstrTemp + (TCHAR)'\0');
PCZZTSTR szzFrom= from;
s.pFrom= szzFrom;
CString dest(cstrTempDest + (TCHAR)'\0');
PCZZTSTR szzDest= dest;
s.pTo= szzDest;
I generally don't use LPCWSTR that much but here is my idea:
CString TestCSTR = "Hello world";
LPCWSTR TestLPC;
TestLPC = (LPCWSTR)_TEXT(TestCSTR.GetString());
It works as expected in fact the variable TestLPC holds "Hello world" or to be more precise a long pointer to it. It should be possible to remove _TEXT without consequences but I'm not sure, the result is the same btw.

VBA/Excel, and C++ DLL, specifically problems with strings

I am working on a project for serial communications between Excel and an Arduino. The following is what I have in VBA for the reading of data from the Arduino which is where my problem lies.
Private Declare Function readFromSerialPort Lib "C:PathToDll.dll"(ByRef Buffer As String) As String
And in a looping function within my VBA I have the following which sets a cell value to a string which is my buffer.
BigData.Range("Buf").Value = "B "
Another cell called dataWindow takes in Buf as an argument so it updates when this is called.
=readFromSerialPort(Buf)
And here is the C++ code on the other end in the DLL.
DLL_EXPORT BSTR WINAPI readFromSerialPort(LPBSTR bufferTemp) {
char errorMsg[] = "read failed";
char mbstring[MAX_STRING_SIZE];
BSTR wcstring = *bufferTemp;
int sLen = wcstombs(mbstring,wcstring,MAX_STRING_SIZE);
char charString[MAX_STRING_SIZE];
DWORD dwBytesRead = 0;
if (hSerial == INVALID_HANDLE_VALUE) {
ErrorExit("ReadFile (port not open)");
int wLen2 = mbstowcs(*bufferTemp,errorMsg,strlen(errorMsg));
return *bufferTemp;
}
if(!ReadFile(hSerial, charString, sLen, &dwBytesRead, NULL)) {
ErrorExit("ReadFile");
int wLen2 = mbstowcs(*bufferTemp,errorMsg,strlen(errorMsg));
return *bufferTemp;
}
int wLen2 = mbstowcs(*bufferTemp,charString,sLen);
return *bufferTemp;
}
The issue is that this works when called from a cell but not when I change it to declaring a string in VBA and calling the read from there.
GSerg has solved this issue. I had not understood that VBA automatically converts when passing as String, so that in the DLL I am not dealing with BSTRs but with ASCII char* or LPSTRs. Also that due to a bug in excel, calling functions from within a cell does not do this conversion.

DLL function not working in a VBA environment but working in Excel VBA

I have the following function contained in a DLL I wrote (c++) that I debugged in Excel, and worked just fine:
float _stdcall ReturnT(LPCSTR FileName)
{
// Extracts the generic language string from the (importing BSTR
// would import kanji or whatever) and converts it into a wstring
wstring str = CA2T(FileName);
// Sets the string to find as _t or _T followed by 2 or 3 digits and a subsequent _ or .
wregex ToFind(L"_[tT]\\d{2,3}(_|.)");
wsmatch TStr;
regex_search(str, TStr, ToFind); // Now the wsmatch variable contains all the info about the matching
wstring T = TStr.str(0).erase(0, 2); // Removes the first 2 characters
T.erase(T.end() - 1); // Removes the last character
// Checks if T is 3 digits or not (2 digits) and eventually add a "."
wstring TVal = L"";
if (T.size() == 3)
{
TVal += T.substr(0, 2) + L"." + T.substr(2, 3);
}
else if (T.size() == 2)
{
TVal += T;
}
// Converts T string to a float
const float TValue = (float) _wtof(TVal.c_str());
return TValue;
}
If FileName is for example foo_T024.lol, this function correctly returns a float (in C++, or Single in VBA) with the value of 2.4 .
I call the function from VBA (both from Excel and the other environment) as:
Private Declare Function ReturnT Lib "[myDLLname]" (ByVal FileName As String) As Single
If I do the same from the other environment and use the function on the same string, I get an **ERROR** and sadly nothing else, because I can't debug (being this a proprietary application).
What could be the problem?
EDIT: I found out that this other environment is actually SAX, which is basically identical to VBA.
EDIT: I managed to link Visual Studio with the application, so I could check what's imported and what is wrong. FileName looks correctly imported, (I used also a VARIANT-input approach to see if that was the issue, which it wasn't) but I receive an error at this line:
wregex ToFind(L"_[tT]\\d{2,3}(\\_|\\.)");
The error is:
Unhandled exception at 0x75F0C54F in NSI2000.exe: Microsoft C++ exception: std::regex_error at memory location 0x0018E294.
And it stops by xthrow.cpp at this point:
#if _HAS_EXCEPTIONS
#include <regex>
_STD_BEGIN
_CRTIMP2_PURE _NO_RETURN(__CLRCALL_PURE_OR_CDECL _Xregex_error(regex_constants::error_type _Code))
{ // report a regex_error
_THROW_NCEE(regex_error, _Code);
} <--- Code stops here
_STD_END
#endif /* _HAS_EXCEPTIONS */
EDIT: My VS version is 2013, the platformtoolset is the "Visual Studio 2013 - Windows XP (v120_xp)". My compiler version is: "Version 18.00.21005.1 for x64"
Turns out something was wrong with the string I've been importing. I do not understand what, since when I debugged the program, they looked fine. I solved by importing a SAFEARRAY of strings (which required me to change the whole function and the VBA code as well), whose BSTR value could be accessed as follows:
int _stdcall FilenameSort(LPSAFEARRAY* StringArray)
{
// Fills a vector with the wstring values
char** StrPtr = 0;
long LowerBound = 0; SafeArrayGetLBound(*StringArray, 1, &LowerBound);
long UpperBound = 0; SafeArrayGetUBound(*StringArray, 1, &UpperBound);
const long Dimension = UpperBound - LowerBound;
SafeArrayAccessData(*StringArray, reinterpret_cast<void**>(&StrPtr));
BSTR element;
vector<wstring> wstrArr;
for (long i = 0; i <= Dimension; ++i)
{
SafeArrayGetElement(*StringArray, &i, &element);
wstring ws(element, SysStringLen(element));
wstrArr.push_back(ws);
}
Having converted all the BSTRs in wstrings correctly, I could work with wregexs without any problem.

Error in converting BSTR to CString

I using Visual C++ 2008 and ADO to access a database and obtain a field value like this:
_variant_t vtValue;
AfxVariantInit(&vtValue);
vtValue = m_pRecordset->Fields->GetItem(_variant_t(strFieldName))->GetValue();
If (vtValue.vt == VT_BSTR)
{
strValue = vtValue.bstrVal;
TRACE(_T(“Field value is %s.\r\n”), strValue); // Cause CrtDbgReport: String too long or IO Error
}
else
{
.. other codes…
}
The TRACE statement for strValue will cause the following error:
“CrtDbgReport: String too long or IO Error”
I just check strValue and found it is a Chinese string with only 6 characters, nothing special. Why it will cause the error?
Thanks
A BSTR is a composite data type that consists of a length prefix, a data string, and a terminator, you can not assign it to CString directly as CString doesn't have the length prefix.
You can use the smart point class _bstr_t to do the conversion.
strValue = (TCHAR*)(_bstr_t)vtValue;

Double check my knowledge: Unicode

there is this method: SCardListReaders, http://msdn.microsoft.com/en-us/library/windows/desktop/aa379793(v=vs.85).aspx
On MSDN there is an example.
After this call:
LPTSTR pmszReaders = NULL;
LPTSTR pReader;
lReturn = SCardListReaders(hSC,
NULL,
(LPTSTR)&pmszReaders,
&cch );
Such code is present which extracts strings from a multi string pmszReaders (multistring is terminated with double null characters).
pReader = pmszReaders;
while ( '\0' != *pReader )
{
// Display the value.
printf("Reader: %S\n", pReader );
// Advance to the next value.
pReader = pReader + wcslen((wchar_t *)pReader) + 1;
}
Just my question is. I think above code only works for Unicode right? If I disable Unicode support in my project, I think above code snippet will not correctly extract reader names? Isn't it?
For instance when I checked wcslen returns 22 when Unicode is set and 24 when Unicode is not set.
If you want the function to work in either a Unicode or MBCS build, use Microsoft specific _tcslen instead of wcslen.
If you want a compile time error if someone tries to use the function without Unicode, change LPTSTR to LPWSTR.