LexYW

Hi, my current project requires me to access web services provided by a mail archiver from C++. I've done some research but am still not sure if there exists a "proper" way of doing this. Shall I choose SoapToolkit or manually formatting Soap messages in XML I was imagining that there may be some tool like WSDL which automatically generates proxy code given a wsdl description. But this is only available for managed code right Can anybody give me some suggestions Thanks.


Re: Visual C++ General Consume Web Services via C++?

Johan Levin

I tried this a while ago, and I was surprised how simple it was.

There is a wizard that generates proxy code for you. In visual studio 2005, go to the solution explorer and right click on your project. Then choose "Add Web Reference...". From there on it's pretty self explanatory.





Re: Visual C++ General Consume Web Services via C++?

Bill Cumming

I agree with Johan - it is very easy. It automatically uses SOAP and does all the initialization and cleanup for you. Very nice. You will have to deal with all the COM-like interface issues like using BSTR instead of CString (ugh) etc. I'm not sure, but you might need to install MSXML Parser v6.0 to use this (although that is forcibly installed by Studio 2005 anyway, but your target systems might need it).

I created an error decoding routine (see the end of this posting) that decodes all the error messages that you get back from SOAP and the web server, which can really help you track down where problems are. They are still pretty cryptic, but after making a list of all the situations I've encountered I can usually match up a combination of the cryptic errors with (usually) configuration or network (or coworker software) problems.

But I do have a very minor complaint. The auto-generated proxy code hard-codes the URL of the web service that you are referencing. That's OK if it is an Internet web service. But my coworker is developing the web service, and sometimes I have it installed on my box. This becomes worse when considering how to deploy my software since it needs a way to modify that URL to point to the installation wherever that may be. So every time I update the reference to the Web Service (every time my coworker changes the interface during development, which is a lot) I have to manually change the automatically generated constructor from the hard-coded URL:

CService1T(ISAXXMLReader *pReader = NULL)
:TClient(_T("http://localhost/MGate/Service1.asmx"))
{
SetClient(true);
SetReader(pReader);
}

to the following to add the URL as a parameter:

CService1T(LPCTSTR sWebServiceURL = "http://localhost/MGate/Service1.asmx", ISAXXMLReader *pReader = NULL)
:TClient(_T(sWebServiceURL))
{
SetClient(true);
SetReader(pReader);
}

EVERY time the Web reference gets updated. Note also that if you are using Source Safe you need to checkout both the auto-generated proxy file (localhost.h in my case since I had the web service running on localhost i.e. on my box) and the Service1.wsdl file BEFORE you update the web reference. My own habit is to patch the constructor described above, copy in all the Source Safe history comments at the top of the file, save it off to a different filename and use that different filename as the actual include for the project (i.e. ignore the auto-generated one).

It's not a huge deal, but I don't see any automated way around it. I wish their auto-generated proxy code had defaulted to making the URL a default parameter.

I hope our esteemed forum moderator is listening....

Mr. Bill



Handy error decoding routine:


void DecodeSoapErrorMessage(CService1& MGateWebService)
{
//// From VC\atlmfc\include\Atlsoap.h
//enum SOAP_ERROR_CODE
//{
// SOAP_E_UNK=0,
// SOAP_E_VERSION_MISMATCH=100,
// SOAP_E_MUST_UNDERSTAND=200,
// SOAP_E_CLIENT=300,
// SOAP_E_SERVER=400
//};
//// client error states
//enum SOAPCLIENT_ERROR
//{
// SOAPCLIENT_SUCCESS=0, // everything succeeded
// SOAPCLIENT_INITIALIZE_ERROR, // initialization failed -- most likely an MSXML installation problem
// SOAPCLIENT_OUTOFMEMORY, // out of memory
// SOAPCLIENT_GENERATE_ERROR, // failed in generating the response
// SOAPCLIENT_CONNECT_ERROR, // failed connecting to server
// SOAPCLIENT_SEND_ERROR, // failed in sending message
// SOAPCLIENT_SERVER_ERROR, // server error
// SOAPCLIENT_SOAPFAULT, // a SOAP Fault was returned by the server
// SOAPCLIENT_PARSEFAULT_ERROR, // failed in parsing SOAP fault
// SOAPCLIENT_READ_ERROR, // failed in reading response
// SOAPCLIENT_PARSE_ERROR // failed in parsing response
//};
//

CString sTemp, sSoapError;
char* sSoapErrors[11] = {
"No errors.",
"Initialization failed. Possibly caused by an MSXML installation problem.",
"Out of memory.",
"Failed to generate the response.",
"Failed to connect to the server.",
"Failed to send the message.",
"Server error.",
"The server returned a SOAP fault.",
"Failed to parse a SOAP fault.",
"Failed while reading the response.",
"Failed while parsing the response."};

SOAPCLIENT_ERROR scError = MGateWebService.GetClientError();
if((scError >= 0) && (scError < 11))
sSoapError.Format("MGateWebService.GetClientError = %d (%s)", scError, sSoapErrors[scError]); // see SOAPCLIENT_ERROR
else
sSoapError.Format("MGateWebService.GetClientError = %d (unknown code)", scError);
CommandLineErrorLog(sSoapError);

CString strDetail = CW2A(MGateWebService.m_fault.m_strDetail); // CW2A macro: convert wide char to ascii
CString strFaultActor = CW2A(MGateWebService.m_fault.m_strFaultActor);
CString strFaultCode = CW2A(MGateWebService.m_fault.m_strFaultCode);
CString strFaultString = CW2A(MGateWebService.m_fault.m_strFaultString);
sSoapError.Format("Error code %d\n" // from SOAP_ERROR_CODE
"FaultCode: %s\n"
"FaultString: %s",
MGateWebService.m_fault.m_soapErrCode, // oddly enough, this is NOT the same as GetClientError( ) - go figure.
strFaultCode, // text corresponding to SOAP_ERROR_CODE in m_soapErrCode
strFaultString);
if( ! strDetail.IsEmpty())
{
sTemp.Format("Details: %s\n", strDetail);
sSoapError += sTemp;
}
if( ! strFaultActor.IsEmpty())
{
sTemp.Format("FaultActor: %s\n", strFaultActor);
sSoapError += sTemp;
}
if(scError == SOAPCLIENT_SOAPFAULT)
{
sTemp.LoadString(IDS_DLG_WORKLIST_CONFIG_ERR);
sSoapError += sTemp;
}

CommandLineErrorLog(sSoapError);
}







Re: Visual C++ General Consume Web Services via C++?

LexYW

Thanks Bill. I just want to make sure, what does "Add web reference" do Because I tried to use WSDL to generate C++ proxy code and the resultant header file is in managed C++, which is no good for me. I have to ensure that this way doesn't involve .Net framework.



Re: Visual C++ General Consume Web Services via C++?

Bill Cumming

I feel your pain. I have been trying to avoid Managed C++ so that I can continue to link statically. The very good news is that Add Web Reference does NOT drag .Net into the picture and generates a proxy that is totally clean C++ (along with all those ^(*^&(*& BSTR objects). So not to worry.

Mr. Bill




Re: Visual C++ General Consume Web Services via C++?

LexYW

I have to admit that I'm not very familiar to COM programming, either. Could you please give me or point me to a simple tutorial of how to get started Thanks a lot!



Re: Visual C++ General Consume Web Services via C++?

Bill Cumming

Alas, I'm in the same boat with COM. I just stumble through enough trial and error coding to get the interface to the web service to compile and hope it doesn't blow up in my face. For example the CW2A macro in the code I posted converts wide char to ascii so CString can use it (CString doesn't tolerate copying a BSTR directly).

Another class I've used in this Web Service context is CComBstr:

// Use CComBSTR wrapper and wide strings to make BSTR happy
CComBSTR bstrLocalIP(sLocalIP);
bstrLocalIP.CopyTo(&siRequestor.Id);

In the above case I passed a CString (sLocalIP) to the CComBSTR constructor. Then I used the CopyTo member function to copy it to the Bstr member of the siRequestor structure (that was auto-generated by Add Web Reference). I think CComBSTR deals with all the string allocation and destruction for you automatically.

Another odd quirk I discovered was I had to create a CComBSTR with an EMPTY string to avoid crashes:

// this indicates "no valid date, use blank string"
holterOrder.Patient.BirthDate = CComBSTR("").Copy();
// MANDATORY field (even if blank - else it crashes in wcslen)

Again the holterOrder structure was auto-generated by Add Web Reference. Similar to explicitly setting a field using a "blank" string, I also found that EVERY field in the auto-generated structures had to be filled in with a valid value or the call to the web service would fail.

Hopefully all the above should save you a few hours of head-banging.

Mr. Bill