59
votes

I need to get unmanaged Windows C++ clients to talk to a WCF service. C++ clients could be running on Win2000 and later. I have a control over both WCF service and which C++ API is being used. Since it's for a proprietary application, it is preferable to use Microsoft stuff where possible, definitely not GNU licensed APIs. Those of you who have it working, can you share a step-by-step process how to make it working?

I have researched following options so far:

  • WWSAPI - not good, will not work on Win 2000 clients.
  • ATL Server, used following guide as a reference. I followed the steps outlined (remove policy refs and flatten WSDL), however the resulting WSDL is still not usable by sproxy

Any more ideas? Please answer only if you actually have it working yourself.

Edit1: I apologize for anyone who I might have confused: what I was looking for was a way to call WCF service from client(s) where no .NET framework is installed, so using .NET-based helper library is not an option, it must be pure unmanaged C++

5
Sorry for the delay. I've updated my answer. Hope it helps.Matt Davis
You could modify the WCF service to offer both SOAP and REST endpoints, then use the REST endpoint from C++. (As long as your data types are easily parsed in C++). See: stackoverflow.com/questions/186631/…Jesse Chisholm

5 Answers

60
votes

The basic idea is to write the WCF code for your clients in C# (it's just easier this way) and use a C++ bridge dll to bridge the gap between your unmanaged C++ code and the managed WCF code written in C#.

Here is the step-by-step process using Visual Studio 2008 along with .NET 3.5 SP1.

  1. The first thing to do is create the WCF Service and a means to host it. If you already have this, skip to Step 7 below. Otherwise, create a Windows NT Service following the steps from here. Use the default names offered by VS2008 for the project and any classes that are added to the project. This Windows NT Service will host the WCF Service.

    • Add a WCF Service named HelloService to the project. To do this, right-click the project in the Solution Explorer window and select the Add|New Item... menu item. In the Add New Item dialog, select the C# WCF Service template and click the Add button. This adds the HelloService to the project in the form of an interface file (IHelloService.cs), a class file (HelloService.cs), and a default service configuration file (app.config).

    • Define the HelloService like this:

``

    [ServiceContract]
    public interface IHelloService
    {
        [OperationContract]
        string SayHello(string name);
    }
    public class HelloService : IHelloService
    {
        public string SayHello(string name)
        {
            return String.Format("Hello, {0}!", name);
        }
    }
  • Modify the Service1 class created in Step 1 above to look like this:

    using System.ServiceModel;
    using System.ServiceProcess;
    public partial class Service1 : ServiceBase
    {
        private ServiceHost _host;
        public Service1()
        {
            InitializeComponent();
        }
        protected override void OnStart( string [] args )
        {
            _host = new ServiceHost( typeof( HelloService ) );
            _host.Open();
        }
        protected override void OnStop()
        {
            try {
                if ( _host.State != CommunicationState.Closed ) {
                    _host.Close();
                }
            } catch {
            }
        }
    }
    
  • Build the project.

  • Open the Visual Studio 2008 command prompt. Navigate to the output directory for the project. Type the following: `installutil WindowsService1.exe' This installs the Windows NT Service on your local machine. Open the Services control panel and start the Service1 service. It is important to do this in order for Step 9 below to work.

    1. Open another instance of Visual Studio 2008 and create an MFC application, which is about as far away as you can get from WCF. As an example, I simply created a dialog MFC application and added a Say Hello! button to it. Right-click the project in the Solution Explorer and select the Properties menu option. Under the General settings, change the Output Directory to ..\bin\Debug. Under the C/C++ General settings, add ..\HelloServiceClientBridge to the Additional Include Directories. Under the Linker General settings, add ..\Debug to the Additional Library Directories. Click the OK button.
  • From the File menu, select the Add|New Project... menu item. Select the C# Class Library template. Change the name to HelloServiceClient and click the OK button. Right-click the project in the Solution Explorer and select the Properties menu option. In the Build tab, change the output path to ..\bin\Debug so the assembly and app.config file will be in the same directory as the MFC application. This library will contain the service reference, i.e., the WCF proxy class, to the WCF Hello Service hosted in the Windows NT Service.

  • In the Solution Explorer, right-click the References folder for the HelloServiceClient project and select the Add Service Reference... menu option. In the Address field, type the address of Hello Service. This should be equal to the base address in the app.config file created in Step 2 above. Click the Go button. The Hello Service should show up in the Services list. Click the OK button to automatically generate the proxy class(es) for the Hello Service. NOTE: I seem to always run into compilation problems with the Reference.cs file generated by this process. I don't know if I'm doing it wrong or if there is a bug, but the easiest way to fix this is modify the Reference.cs file directly. The problem is usually a namespacing issue and can be fixed with minimal effort. Just be aware that this is a possibility. For this example, I've changed the HelloServiceClient.ServiceReference1 to simply HelloService (along with any other required changes).

  • To allow the MFC Application to interact with the WCF service, we need to build a managed C++ "bridge" DLL. From the File menu, select the Add|New Project... menu item. Select the C++ Win32 Project template. Change the name to HelloServiceClientBridge and click the OK button. For the Application Settings, change the Application Type to DLL and check the Empty project checkbox. Click the Finish button.

  • The first thing to do is modify the project properties. Right-click the project in the Solution Explorer and select the Properties menu option. Under the General settings, change the Output Directory to ..\bin\Debug and change the Common Language Runtime Support option to Common Language Runtime Support (/clr). Under the Framework and References settings, add a reference to the .NET System, System.ServiceModel, and mscorlib assemblies. Click the OK button.

  • Add the following files to the HelloServiceClientBridge project - HelloServiceClientBridge.h, IHelloServiceClientBridge.h, and HelloServiceClientBridge.cpp.

  • Modify the IHelloServiceClientBridge.h to look like this:

    #ifndef __IHelloServiceClientBridge_h__
    #define __IHelloServiceClientBridge_h__
    
    #include <string>
    
    #ifdef HELLOSERVICECLIENTBRIDGE_EXPORTS
    #define DLLAPI __declspec(dllexport)
    #else
    #define DLLAPI __declspec(dllimport)
    #pragma comment (lib, "HelloServiceClientBridge.lib") // if importing, link also
    #endif
    
    class DLLAPI IHelloServiceClientBridge
    {
    public:
        static std::string SayHello(char const *name);
    };
    
    #endif // __IHelloServiceClientBridge_h__
    
  • Modify the HelloServiceClientBridge.h to look like this:

    #ifndef __HelloServiceClientBridge_h__
    #define __HelloServiceClientBridge_h__
    
    #include <vcclr.h>
    #include "IHelloServiceClientBridge.h"
    
    #ifdef _DEBUG
    #using<..\HelloServiceClient\bin\Debug\HelloServiceClient.dll>
    #else
    #using<..\HelloServiceClient\bin\Release\HelloServiceClient.dll>
    #endif
    
    class DLLAPI HelloServiceClientBridge : IHelloServiceClientBridge
    { };
    
    #endif // __HelloServiceClientBridge_h__
    
  • The syntax for the .cpp file uses managed C++, which takes some getting used to. Modify the HelloServiceClientBridge.cpp to look like this:

    #include "HelloServiceClientBridge.h"
    
    using namespace System;
    using namespace System::Runtime::InteropServices;
    using namespace System::ServiceModel;
    using namespace System::ServiceModel::Channels;
    
    std::string IHelloServiceClientBridge::SayHello(char const *name)
    {
        std::string rv;
        gcroot<Binding^> binding = gcnew WSHttpBinding();
        gcroot<EndpointAddress^> address = gcnew EndpointAddress(gcnew String("http://localhost:8731/Design_Time_Addresses/WindowsService1/HelloService/"));
        gcroot<HelloService::HelloServiceClient^> client = gcnew HelloService::HelloServiceClient(binding, address);
        try {
            // call to WCF Hello Service
            String^ message = client->SayHello(gcnew String(name));
            client->Close();
            // marshal from managed string back to unmanaged string
            IntPtr ptr = Marshal::StringToHGlobalAnsi(message);
            rv = std::string(reinterpret_cast<char *>(static_cast<void *>(ptr)));
            Marshal::FreeHGlobal(ptr);
        } catch (Exception ^) {
            client->Abort();
        }
        return rv;
    }
    
  • The only thing left to do is update the MFC application to invoke the SayHello() WCF service call. On the MFC form, double-click the Say Hello! button to generate the ButtonClicked event handler. Make the event handler look like this:

    #include "IHelloServiceClientBridge.h"
    #include <string>
    void CMFCApplicationDlg::OnBnClickedButton1()
    {
        try {
            std::string message = IHelloServiceClientBridge::SayHello("Your Name Here");
            AfxMessageBox(CString(message.c_str()));
        } catch (...) {
        }
    }
    
  • Run the application and click the Say Hello! button. This will cause the application to invoke the SayHello() method of the WCF Hello Service hosted in the Windows NT Service (which should still be running, by the way). The return value is then displayed in a message box.

Hopefully you can extrapolate from this simple example to fit your needs. If this does not work, please let me know so I can fix the post.

12
votes

For those who are interested, I found one semi-working ATL Server solution. Following is the host code, notice it is using BasicHttpBinding, it's the only one which works with ATL Server:

        var svc =  new Service1();
        Uri uri = new Uri("http://localhost:8200/Service1");
        ServiceHost host = new ServiceHost(typeof(Service1), uri);

        var binding = new BasicHttpBinding();
        ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IService1), binding, uri);
        endpoint.Behaviors.Add(new InlineXsdInWsdlBehavior());

        host.Description.Behaviors.Add(new ServiceMetadataBehavior() { HttpGetEnabled = true });
        var mex = host.AddServiceEndpoint(typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
        host.Open();

        Console.ReadLine();

code for InlineXsdInWsdlBehavior could be found here . One important change needs to be done to the InlineXsdInWsdlBehavior in order for it to work properly with sproxy when complex types are involved. It is caused by the bug in sproxy, which does not properly scope the namespace aliases, so wsdl cannot have repeating namespace aliases or sproxy will crap out. Here's the functions which needs to change:

    public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
    {
        int tnsCount = 0;

        XmlSchemaSet schemaSet = exporter.GeneratedXmlSchemas;

        foreach (WsdlDescription wsdl in exporter.GeneratedWsdlDocuments)
        {
            //
            // Recursively find all schemas imported by this wsdl
            // and then add them. In the process, remove any
            // <xsd:imports/>
            //
            List<XmlSchema> importsList = new List<XmlSchema>();
            foreach (XmlSchema schema in wsdl.Types.Schemas)
            {
                AddImportedSchemas(schema, schemaSet, importsList, ref tnsCount);
            }
            wsdl.Types.Schemas.Clear();
            foreach (XmlSchema schema in importsList)
            {
                RemoveXsdImports(schema);
                wsdl.Types.Schemas.Add(schema);
            }
        }
    }


    private void AddImportedSchemas(XmlSchema schema, XmlSchemaSet schemaSet, List<XmlSchema> importsList, ref int tnsCount)
    {
        foreach (XmlSchemaImport import in schema.Includes)
        {
            ICollection realSchemas = schemaSet.Schemas(import.Namespace);
            foreach (XmlSchema ixsd in realSchemas)
            {
                if (!importsList.Contains(ixsd))
                {
                    var new_namespaces = new XmlSerializerNamespaces();
                    foreach (var ns in ixsd.Namespaces.ToArray())
                    {
                        var new_pfx = (ns.Name == "tns") ? string.Format("tns{0}", tnsCount++) : ns.Name;
                        new_namespaces.Add(new_pfx, ns.Namespace);
                    }

                    ixsd.Namespaces = new_namespaces;
                    importsList.Add(ixsd);
                    AddImportedSchemas(ixsd, schemaSet, importsList, ref tnsCount);
                }
            }
        }
    }

Next step is to generate C++ header:

sproxy.exe /wsdl http://localhost:8200/Service1?wsdl

and then C++ program looks like this:

using namespace Service1;

CoInitializeEx( NULL, COINIT_MULTITHREADED  );

{
    CService1T<CSoapWininetClient> cli;
    cli.SetUrl( _T("http://localhost:8200/Service1") );

    HRESULT hr = cli.HelloWorld(); //todo: analyze hr
}

CoUninitialize();
return 0;

Resulting C++ code handles complex types pretty decently, except that it cannot assign NULL to the objects.

2
votes

I would create a C# managed class to do the WCF work and expose the class as a COM object to the C++ clients.

1
votes

You can implement a SOAP client somewhat easily using the deprecated MS Soap Toolkit. Unfortunately, there doesn't seem to be a replacement for this outside of moving to .NET.

0
votes

Can you publish a REST Web Service and use the MSXML COM library -- should be already installed, has an XML parser, and an HTTP library.

http://msdn.microsoft.com/en-us/library/ms763742.aspx