Introduction
The
Windows Web Services API is a native implementation of SOAP and can be used
to interop transparently with existing WCF services and clients, in addition to
offering the ability to completely achieve a client-server implementation in
pure native code. I have been longing to play with it ever since I heard
Nikola Dudar talk
about it at the MVP Summit earlier this year. It’s natively included with
Windows 7, but can also be installed and used from older OSes such as XP, Vista,
2003 and 2008. You can write pure native clients using WWS that can connect to
an existing managed WCF service, and also write a WWS native service that can be
consumed by a WCF client. It’s so compatible that you can replace either a WCF
client or a WCF service with a WWS equivalent without the other party being
aware of it. In this article, I'll talk about a simple WCF service and its WCF
client, and then show how to use WWS to write a native client that can consume
the WCF service. I'll then show how the WCF service itself can be replaced
transparently with an equivalent WWS service, and how both the WCF and WWS
clients can connect to this WWS service without any changes in code.
Note : The examples were written on a 64 bit Windows 7 RC machine
running VS 2010 beta 1.
The example WCF service
The first thing to do is to create a very simple WCF service. For our example
I'll use a string reversing service that exposes a single method that accepts a
string and returns the reversed string. Here’s the service interface :
[ServiceContract]
interface IStringService
{
[OperationContract]
string Reverse(string s);
}
class MyStringService : IStringService
{
public string Reverse(string s)
{
return new string(s.Reverse().ToArray());
}
}
Here’s the code that shows how the service is created and run.
WSHttpBinding binding = new WSHttpBinding();
binding.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard;
binding.Security.Mode = SecurityMode.None;
Uri baseAddress = new Uri("http://localhost:8000/StringService");
using (ServiceHost serviceHost =
new ServiceHost(typeof(MyStringService), baseAddress))
{
ServiceMetadataBehavior smb =
serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy12;
serviceHost.Description.Behaviors.Add(smb);
serviceHost.AddServiceEndpoint(
ServiceMetadataBehavior.MexContractName,
MetadataExchangeBindings.CreateMexHttpBinding(),
"mex"
);
serviceHost.AddServiceEndpoint(
typeof(IStringService), binding, baseAddress);
serviceHost.Open();
Console.WriteLine("The service is running. Press any key to stop.");
Console.ReadKey();
I wanted to avoid creating a config file and so everything's done in code,
including adding the MEX endpoint. There's not much to explain there, I've used
a standard WSHttpBinding
and have disabled security (to keep the
example simple).
Note : WWS supports several security modes compatible with WCF - so
this is not an issue, but the default WCF security mode (Message
)
is not supported! So make sure that you don't trip on that one.
Simple WCF client in C#
Here's a simple C# console client that uses WCF to connect to the above WCF
service and invoke the Reverse
method.
[ServiceContract]
interface IStringService
{
[OperationContract]
string Reverse(string s);
}
class Program
{
static void Main(string[] args)
{
WSHttpBinding binding = new WSHttpBinding();
binding.HostNameComparisonMode = HostNameComparisonMode.StrongWildcard;
binding.Security.Mode = SecurityMode.None;
Uri baseAddress = new Uri("http://localhost:8000/StringService");
EndpointAddress address = new EndpointAddress(baseAddress);
ChannelFactory<IStringService> channelFactory =
new ChannelFactory<IStringService>(binding, address);
IStringService channel = channelFactory.CreateChannel();
Console.WriteLine(channel.Reverse("This is a test string."));
((IChannel)channel).Close();
Console.ReadKey();
}
}
It doesn't do much but I wanted to show it here first because later on I'll
show how this same client can consume the WWS service that'll replace the above
WCF service. At this point we have a WCF service and a WCF client that consumes
it. Next, let's write a simple WWS client (in native code) that'll connect to
this WCF service.
Writing the native WWS client
Prior to creating the project, you need to have the Windows 7 RC
SDK installed and configured. (Note : On older OSes,
you'll also need to download and install the
WWS RC API)
You'll also need to appropriately
point your C++ include and library search directories. In VS
2010 this is done on a per-project level. For information on how
to do this in VS 2010, see this blog entry I wrote :
Setting VC++ directories in VS 2010.
Now there are two steps to do before we start writing the WWS
native client :
Use
svcutil to generate WSDL from the WCF service
Run this command : svcutil /t:metadata
http://localhost:8000/StringService
You will get the following files generated for you :
- schemas.microsoft.com.2003.10.Serialization.xsd
- tempuri.org.wsdl
- tempuri.org.xsd
Here's a snip from the wsdl file that shows our service
description :
<wsdl:message name="IStringService_Reverse_InputMessage">
<wsdl:part name="parameters" element="tns:Reverse" />
</wsdl:message>
<wsdl:message name="IStringService_Reverse_OutputMessage">
<wsdl:part name="parameters" element="tns:ReverseResponse" />
</wsdl:message>
<wsdl:portType name="IStringService">
<wsdl:operation name="Reverse">
<wsdl:input wsaw:Action="http://tempuri.org/IStringService/Reverse"
message="tns:IStringService_Reverse_InputMessage" />
<wsdl:output
wsaw:Action="http://tempuri.org/IStringService/ReverseResponse"
message="tns:IStringService_Reverse_OutputMessage" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="WSHttpBinding_IStringService"
type="tns:IStringService">
<wsp:PolicyReference URI="#WSHttpBinding_IStringService_policy" />
<soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="Reverse">
<soap12:operation
soapAction="http://tempuri.org/IStringService/Reverse"
style="document" />
<wsdl:input>
<soap12:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap12:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="MyStringService">
<wsdl:port name="WSHttpBinding_IStringService"
binding="tns:WSHttpBinding_IStringService">
<soap12:address location="http://localhost:8000/StringService" />
<wsa10:EndpointReference>
<wsa10:Address>http://localhost:8000/StringService</wsa10:Address>
</wsa10:EndpointReference>
</wsdl:port>
</wsdl:service>
Next step is to generate the proxy files.
Use the Windows WebServices compiler tool (wsutil)
to generate proxies (c/h files).
Run this command : wsutil *.xsd *.wsdl
You'll see a c/h file pair generated for each file that's passed to
wsutil - and since we have 3 files, it'll give us 6 files back. Once the
files are generated, open the native C++ console project you created and add
all the generated c/h files (and remember to disable precompiled
headers for the C files). For error free compilation, put your
#include
-s in this order :
#include "stdafx.h"
. . .
#include "WebServices.h"
#include "schemas.microsoft.com.2003.10.Serialization.xsd.h"
#include "tempuri.org.xsd.h"
#include "tempuri.org.wsdl.h"
And remember to include WebServices.lib in your additional linker
modules. The first thing is to do now is to declare some
variables and also specify the service URL :
HRESULT hr = ERROR_SUCCESS;
WS_ERROR* error = NULL;
WS_HEAP* heap = NULL;
WS_SERVICE_PROXY* proxy = NULL;
WS_ENDPOINT_ADDRESS address = {};
WS_STRING url= WS_STRING_VALUE(L"http://localhost:8000/StringService");
address.url = url;
WS_STRING
is a simple struct that has a WCHAR*
(that will point to the string) and a
ULONG
that will represent the length. I’ve used the WS_STRING_VALUE
macro to initialize the string there.
The WWS APIs provide rich error info through the WS_ERROR
structure and so we create a
WS_ERROR
struct using the WsCreateError
API call (using
default arguments).
hr = WsCreateError(NULL, 0, &error);
if (FAILED(hr))
{
}
We also need to create a WS_HEAP
object which
represents an opaque heap structure (error handling not shown) :
hr = WsCreateHeap(2048, 512, NULL, 0, &heap, error);
The next step is to create the service proxy :
WS_HTTP_BINDING_TEMPLATE templ = {};
hr = WSHttpBinding_IStringService_CreateServiceProxy(&templ, NULL, 0, &proxy, error);
WSHttpBinding_IStringService_CreateServiceProxy
is a proxy function that was generated by wsutil. Internally it
calls the WWS API WsCreateServiceProxyFromTemplate
but it saves us the hassle of correctly and properly filling up
the various arguments. Here's the generated code :
HRESULT WSHttpBinding_IStringService_CreateServiceProxy(
__in_opt WS_HTTP_BINDING_TEMPLATE* templateValue,
__in_ecount_opt(proxyPropertyCount) const WS_PROXY_PROPERTY* proxyProperties,
__in const ULONG proxyPropertyCount,
__deref_out_opt WS_SERVICE_PROXY** _serviceProxy,
__in_opt WS_ERROR* error)
{
return WsCreateServiceProxyFromTemplate(
WS_CHANNEL_TYPE_REQUEST,
proxyProperties,
proxyPropertyCount,
WS_HTTP_BINDING_TEMPLATE_TYPE,
templateValue,
templateValue == NULL ? 0 : sizeof(WS_HTTP_BINDING_TEMPLATE),
&tempuri_org_wsdl.policies.WSHttpBinding_IStringService,
sizeof(tempuri_org_wsdl.policies.WSHttpBinding_IStringService),
_serviceProxy,
error);
}
As you can see, the proxy function saved us the hassle of specifying
all those properties. Next, we open the service proxy (connects
us to the service endpoint) :
hr = WsOpenServiceProxy(proxy, &address, NULL, error);
At this point we are ready to make calls into the service for
which we again use the proxy functions that are generated by
wsutil.
WCHAR* result;
hr = WSHttpBinding_IStringService_Reverse(
proxy, L"Nishant Sivakumar", &result,
heap, NULL, 0, NULL, error);
if (FAILED(hr))
{
}
wprintf(L"%s\n", result);
WSHttpBinding_IStringService_Reverse
is
generated for us and is a pretty simple function to use, and it
internally wraps the call to the WsCall
WWS API
function, including correctly wrapping up all the arguments and
the return value. I am not showing the actual proxy function
here but it'll be in tempuri.org.wsdl.c.
Well, that’s pretty much it. Once you are
done, just call all the close/free APIs :
if (proxy)
{
WsCloseServiceProxy(proxy, NULL, NULL);
WsFreeServiceProxy(proxy);
}
if (heap)
{
WsFreeHeap(heap);
}
if (error)
{
WsFreeError(error);
}
I was absolutely thrilled when I ran the console app for the first
time and it
successfully connected to the WCF service. The native client is
about twice as long (number of lines of code) as the equivalent
managed WCF client but that’s a small price to
pay for the ability to consume a WCF service in pure native
code. In the next section I will write about how the WCF
service itself can be replaced with an identical WWS service
(both WCF and WWS clients will continue to work the same).
Rewriting the WCF service in WWS
Once we rewrite the service using WWS, existing clients (whether WWS, WCF, or other) would continue
to behave the same. I am going to use the same c/h files that
wsutil generated from the wsdl file. There is a function
signature generated for us to match the service contract
methods. In our case there’s just one -
WSHttpBinding_IStringService_ReverseCallback
.
typedef HRESULT (CALLBACK* WSHttpBinding_IStringService_ReverseCallback) (
__in const WS_OPERATION_CONTEXT* _context,
__in_opt __nullterminated WCHAR* s,
__out_opt __deref __nullterminated WCHAR** ReverseResult,
__in_opt const WS_ASYNC_CONTEXT* _asyncContext,
__in_opt WS_ERROR* _error);
So the
first thing is to add a method that matches this signature, and
this will reverse a string just like the WCF service (except we
write it in C or C++).
HRESULT CALLBACK Reverse(
__in const WS_OPERATION_CONTEXT* context,
__in WCHAR* s,
__out WCHAR** reverse,
__in_opt const WS_ASYNC_CONTEXT* asyncContext,
__in_opt WS_ERROR* error)
{
WS_HEAP* heap = NULL;
HRESULT hr = WsGetOperationContextProperty(
context,
WS_OPERATION_CONTEXT_PROPERTY_HEAP,
&heap,
sizeof(heap),
error);
if (FAILED(hr))
{
return hr;
}
hr = WsAlloc(
heap,
sizeof(WCHAR) * (wcslen(s) + 1),
(void**)reverse,
error);
if (FAILED(hr))
{
return hr;
}
wcscpy(*reverse, s);
wcsrev(*reverse);
return ERROR_SUCCESS;
}
I first use WsGetOperationContextProperty
to get
the heap and then use WsAlloc
to allocate memory
for the reversed string on this heap. We do not ever allocate
memory using standard memory allocation mechanisms unless it’s
memory we will have the option to delete/free when we are done
using it. Instead we use the WWS heap which frees us from
worrying about memory leaks - the memory will be released when
the heap is reset or freed.
Now let’s get to creating the service. I will not show
the error handling code (to save space) but every
HRESULT
return value must be checked for success before
proceeding further. The first thing is to create the error and
heap objects just as we did when writing the WWS client.
WS_ERROR* error = NULL;
HRESULT hr = WsCreateError( NULL, 0, &error);
if (FAILED(hr))
{
}
WS_HEAP* heap = NULL;
hr = WsCreateHeap( 100000, 0, NULL, 0, &heap, error);
if (FAILED(hr))
{
}
The next step is to create a service endpoint.
WSHttpBinding_IStringServiceFunctionTable functions = { Reverse };
WS_STRING url = WS_STRING_VALUE(L"http://localhost:8000/StringService");
WS_HTTP_BINDING_TEMPLATE templateValue = {};
WS_SERVICE_ENDPOINT* serviceEndpoint;
hr = WSHttpBinding_IStringService_CreateServiceEndpoint(&templateValue,
&url, &functions, NULL, NULL, 0,
heap, &serviceEndpoint, error);
if (FAILED(hr))
{
}
Notice how we use the generated WSHttpBinding_IStringServiceFunctionTable
to specify the
list of functions (just one in our case). Now I use the proxy
WSHttpBinding_IStringService_CreateServiceEndpoint
to create the end point (and it internally calls
WsCreateServiceEndpointFromTemplate
). I have used default
values for other arguments, but there is a lot of custom
configuration that can be done. We’ll now create the service
host:
WS_SERVICE_HOST* host;
const WS_SERVICE_ENDPOINT* serviceEndpoints[1];
serviceEndpoints[0]= serviceEndpoint;
hr = WsCreateServiceHost( serviceEndpoints, 1,
NULL, 0, &host, error);
if (FAILED(hr))
{
}
We only have one endpoint, but the service host can host
multiple endpoints. While I have called
WsCreateServiceHost
with default arguments (basically
passing
NULL
) it’s possible to set various service
properties at this point. The last step is to open the service
host.
hr = WsOpenServiceHost(host, NULL, error);
The above code will open the service and start listening on
each of the endpoints (just one in our example). For the example console app
we'll use a _getch()
so the app won’t exit.
wprintf(L"Press any key to stop the service...\n");
_getch();
Once the app’s done, close and free the service host :
WsCloseServiceHost(host, NULL, error);
WsFreeServiceHost(host);
And also free the heap/error objects :
if (heap)
{
WsFreeHeap(heap);
}
if (error)
{
WsFreeError(error);
}
Run the service, and then run the WWS client which will be able connect to it and invoke the reverse function
successfully. You can also run the simple WCF C# client and
it’ll connect to this and execute the string reversal method. So now we
can connect either of the two clients to either of the two services - sweet!
Conclusion
I do agree that all these proxies, having to create/free structures, handling HRESULT
s etc. may seem a tad foreign if
you are coming from a pure C# or VB.NET world. But if you are
not put off by C++, and keeping your service or client code
native is important to you, then WWS sure seems to be a great
way to do it.
History
- Article 1st draft : 7/25/2009
- Article published : 7/27/2009