Building XML-RPC Clients in C
October 31, 2001
XML-RPC is a useful protocol for building cross-platform, client-server applications. Often XML-RPC is demonstrated with high-level interpreted languages like Perl and Python. In this article, Eric Kidd's XML-RPC C library is used to build a simple, yet powerful debugging client. Special care is taken to bring programmers with rusty C-hacking skills up to speed.
When It Absolutely Has to Be in C
XML-RPC is a wire protocol that describes an XML serialization format that clients and servers use to pass remote procedure calls to each other. There are two features that make this protocol worth knowing. The first is that the details of parsing the XML are hidden from the user. The second is that clients and servers don't need to be written in the same language. For more background information on XML-RPC, check out the Resources listed at the end of this article.
Many articles written about XML-RPC use Java, Perl, Python, or PHP to demonstrate building Web Services. However, real life programming requirements often mitigate against the programmer's first choice of implementation language, perhaps because access to a resource whose only API is a C library is required. In this case, building an XML-RPC server to this resource opens it up to any client that supports XML-RPC.
In high-level languages like Perl or Python, development can be rapid because the compile-run-edit test cycle is almost as fast as opening one's editor. In C, this isn't the case. Compiling a program can take a lot of time. But sometimes it's the only tool for the job. If your C skills are rusty, this article is for you. Because XML-RPC is a high-level protocol, it takes a good number of other C libraries to make the magic happen. Remember, even a simple client needs to be able to talk HTTP and parse XML, both of which are far beyond the built-in facilities of C. The first step is installing Eric Kidd's C library.
Installing the C XML-RPC Library
Eric Kidd's C/C++ interface to XML-RPC requires the W3 Consortium's WWW library. I used version 0.9.9 of the XML-RPC
library and version 5.3.2 of the W3C's libwww for this article. Both packages compiled
cleanly on my Red Hat 7.1 box using the standard ./configure && make
,
but there was a catch during the make install
phase. The names of the shared
libraries had an spurious '.0' appended to them. Those already familiar with the way
shared
libraries are found on a Linux box may skip the next section.
Joe Johnston has also written an article on Binary Data to Go: Using XML-RPC to Serve Up Charts on the Fly, which looks at how XML-RPC and Web services can create simple charts for client programs.
Both Kidd's and the W3C's libraries install shared libraries. These differ from the
static
libraries you might be more familiar with in that they are loaded during a program's
runtime rather than being built into a monolithic static binary. In order for the
system to find these shared libraries, you need to make sure that
/etc/ld.so.conf
lists the path to your shared libraries. In the case of the
XML-RPC libraries listed above, /usr/lib
must be listed. For existing binaries,
you can check whether the system can locate the shared libraries by using the
ldd
command:
$ ldd <executable_name>
In the case of the program presented here, the ldd output will look something like:
[jjohn@marian src]$ ldd xmlrpc_debug libwwwxml.so.0 => /usr/lib/libwwwxml.so.0 (0x4002f000) libxmltok.so.0 => /usr/lib/libxmltok.so.0 (0x40032000) libxmlparse.so.0 => /usr/lib/libxmlparse.so.0 (0x40044000) libwwwzip.so.0 => /usr/lib/libwwwzip.so.0 (0x4004b000) libwwwinit.so.0 => /usr/lib/libwwwinit.so.0 (0x4004e000) libwwwapp.so.0 => /usr/lib/libwwwapp.so.0 (0x40051000) libmd5.so.0 => /usr/lib/libmd5.so.0 (0x40067000) libwwwhtml.so.0 => /usr/lib/libwwwhtml.so.0 (0x4006a000) libwwwtelnet.so.0 => /usr/lib/libwwwtelnet.so.0 (0x40076000) ...
If a library doesn't have a hex address next to it, the system can't find it. This
can be
solved by making sure that path in /etc/ld.so.conf
is listed and then running
ldconfig
as root. Run the ldd
command again to make sure all
your shared libraries are visible.
As I mentioned above, the gotcha is that Kidd and W3C libraries don't name the shared
libraries correctly (they install with an extra '.0' on the name). You will need to
create
symlinks with the kind of names that ldconfig
expects. I used the Perl script
in Listing 1 to do this, but any scripting tool will do.
Listing 1: Fixing library names
1 #!/usr/bin/perl -- 2 # Look for these libs; ensure symlinks are right 3 4 use strict; 5 use constant LIB_DIR => '/usr/lib'; 6 7 my @libs = qw( 8 libwwwxml libxmltok libxmlparse libwwwzip 9 libwwwinit libwwwapp libmd5 libwwwhtml libwwwtelnet 10 libwwwnews libwwwhttp libwwwmime libwwwgopher libwwwftp 11 libwwwfile libwwwdir libwwwcache libwwwstream libwwwmux 12 libwwwtrans libwwwcore libwwwutils libdl libz 13 libxmlrpc libxmlrpc_xmlparse libxmlrpc_xmltok 14 ); 15 if( $> != 0 ){ 16 warn "ERROR: Must be root\n"; 17 } 18 19 chdir LIB_DIR || die "ERROR: cd: $!"; 20 21 for my $lib (@libs){ 22 print "Looking for $lib\n"; 23 my @candidates = glob("$lib*0"); 24 for(@candidates){ 25 my ($link, $real); 26 if( -l $_ ){ 27 $real = readlink($_); 28 $link = "(is a symlink to $real)"; 29 } 30 print "\t$_ $link\n"; 31 32 if( $real ){ 33 my $new = substr($_, 0, length($_) - 2); 34 print "\tcreating a new symlink($new) to $real\n"; 35 unless( symlink $real, $new ){ 36 warn "WARN: symlink: $!\n"; 37 } 38 } 39 } 40 } |
Once you've squared this detail away, you're ready to start coding.
The Debug Client
When learning a new XML-RPC implementation, the most important thing to learn is how it deals with the XML-RPC datatypes listed in Table 1.
|
Recall that XML-RPC libraries map language-specific datatypes to XML-RPC datatypes. It makes sense, then, to build a client that has to deal with all possible XML-RPC datatypes. This program is normally invoked like this:
$ ./xmlrpc_debug http://somewhere.com/RPC2 get_array
The first argument is a URL to an XML-RPC server. The second argument is the name of a remote procedure that doesn't require input. The code for this client begins in Listing 2. As is typical of most C programs, various header files are pulled in.
Listing 2: xmlrpc_debug.c, part 1
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 5 #include <string.h> 6 #include <xmlrpc.h> 7 #include <xmlrpc_client.h> 8 |
Listing 3 defines some constants and function prototypes. As a
point of style, I try to make default values constants. This makes them easy to change,
since most #define
statements are listed at the beginning of code.
Listing 3: xmlrpc_debug.c, part 2
9 #define NAME "DEBUG XML-RPC C Client" 10 #define VERSION "0.1" 11 #define SERVER_URL "http://127.0.0.1:3080/RPC2" 12 #define DEFAULT_RPC "status" 13 14 void die_if_fault_occurred(xmlrpc_env*); 15 void print_values(xmlrpc_env*, xmlrpc_value*); 16 int get_int(xmlrpc_env*, xmlrpc_value*); 17 int get_boolean(xmlrpc_env*, xmlrpc_value*); 18 double get_double(xmlrpc_env*, xmlrpc_value*); 19 char* get_timestamp(xmlrpc_env*, xmlrpc_value*); 20 char* get_string(xmlrpc_env*, xmlrpc_value*); 21 char* get_base64(xmlrpc_env*, xmlrpc_value*); 22 void get_array(xmlrpc_env*, xmlrpc_value*); 23 void get_struct(xmlrpc_env*, xmlrpc_value*); 24 |
The main line begins in Listing 4. Line 26 declares an important
variable that is used by nearly every XML-RPC function. The xmlrpc_env
variable
env
will contain error messages and other transactional information. You
don't need to worry about manipulating it directly, though. It's a kind of black box.
Visit xml.oreilly.com for a complete list of O'Reilly's books on XML.
Line 32 initializes some internal flags, and line 33 sets up the xmlrpc_env
variable. These lines are mostly a stock piece; you won't need to change them much
for your
own code.
Listing 4: xmlrpc_debug.c, part 3
25 int main (int argc, char** argv){ 26 xmlrpc_env env; 27 xmlrpc_value *result; 28 char* url; 29 char* rpc; 30 31 /* Start up our XML-RPC client library. */ 32 xmlrpc_client_init(XMLRPC_CLIENT_NO_FLAGS, NAME, VERSION); 33 xmlrpc_env_init(&env); 34 |
Listing 4 examines the command line for parameters. If none are detected, default values are used. Again, if you are used to scripting languages, you might be surprised by the rather lengthy measures needed to deal with strings in C.
Listing 5: xmlrpc_debug.c, part 4
35 /* figure out URL */ 36 if(argc > 0){ 37 url = argv[1]; 38 }else{ 39 if( (url = (char *) malloc(sizeof(SERVER_URL))) ){ 40 strcpy(url, SERVER_URL); 41 }else{ 42 printf("ERROR: Couldn't malloc\n"); 43 exit(1); 44 } 45 } 46 47 if(argc > 1){ 48 rpc = argv[2]; 49 }else{ 50 if( (rpc = (char *) malloc(sizeof(DEFAULT_RPC))) ){ 51 strcpy(rpc, DEFAULT_RPC); 52 }else{ 53 printf("ERROR: Couldn't malloc\n"); 54 exit(1); 55 } 56 } 57 |
In Listing 5 after 60 lines of code, I'm ready to make the remote
procedure call. In your own code, you will be dealing with xmlrpc_client_call
since it is the way to send your request to the server. This function's third argument
is a
kind of format string that tells the library how to map C data into XML-RPC data.
Although
the code in Listing 5 doesn't actually pass any data, here's a
brief snippet that passes one string to a remote procedure:
result = xmlrpc_client_call( &env, url, "some_func", "(s)", "my great argument" )
Here's a snippet that creates a struct:
result = xmlrpc_client_call( &env, url, "some_func", "({s:i,s:i})", "my great argument", 1, "my next argument", 2 )
See the "overview.txt" document that's included with Kidd's XML-RPC library for more
details. The meat of this program occurs on line 70 when print_values
is
invoked. Given the result of the remote procedure call, it simply decodes and prints
out the
values it received. I'll show the implementation of this function below.
Listing 6: xmlrpc_debug.c, part 5
58 printf("Calling %s\n", url); 59 60 /* Call our XML-RPC server. */ 61 result = xmlrpc_client_call(&env, 62 url, 63 rpc, 64 "()" 65 ); 66 67 die_if_fault_occurred(&env); 68 69 /* What did we get? */ 70 print_values(&env, result); 71 |
Listing 7 hints at the reference counting that Kidd's XML-RPC
library does. He tries to shield the user from the details of memory management, but
you
must tell the system when you are done with xmlrpc_value
data (see line 73).
There are also two convenient functions (lines 76, 77) for cleaning up other memory
that was
allocated behind the scenes.
Listing 7: xmlrpc_debug.c, part 6
72 /* Dispose of our result value. */ 73 xmlrpc_DECREF(result); 74 75 /* Shutdown our XML-RPC client library. */ 76 xmlrpc_env_clean(&env); 77 xmlrpc_client_cleanup(); 78 79 free(url); 80 81 return 0; 82 } 83 |
Listing 8 shows how the six simplest data types are decoded. The
tricky part is knowing which "format" string to pass to xmlrpc_parse_value
. You
can use this code as a guide, but also look at the aforementioned overview.txt for
the gory details. Notice that after calling this conversion function, any errors that
may
have occurred will be reported by die_if_fault_occurred
(which is code ripped
entirely from Eric Kidd's examples).
Listing 8: xmlrpc_debug.c, part 7
84 /* 85 subs 86 */ 87 void print_values( xmlrpc_env* env, xmlrpc_value* in ){ 88 89 /* What did we get back? */ 90 switch (xmlrpc_value_type(in)) { 91 case (XMLRPC_TYPE_INT): 92 printf("Got an integer: %d\n", get_int(env, in)); 93 break; 94 case (XMLRPC_TYPE_BOOL): 95 printf("Got a boolean: %d\n", get_boolean(env, in)); 96 break; 97 case (XMLRPC_TYPE_DOUBLE): 98 printf("Got a double: %g\n", get_double(env, in)); 99 break; 100 case (XMLRPC_TYPE_DATETIME): 101 printf("Got an ISO8601 timestamp: %s\n", get_timestamp(env, in)); 102 break; 103 case (XMLRPC_TYPE_STRING): 104 printf("Got a string: %s\n", get_string(env, in)); 105 break; 106 case (XMLRPC_TYPE_BASE64): 107 printf("Got a base64 string: %s\n", get_base64(env, in)); 108 break; 109 case (XMLRPC_TYPE_ARRAY): 110 printf("Got an array:\n"); 111 get_array(env, in); 112 break; 113 case (XMLRPC_TYPE_STRUCT): 114 printf("Got a struct\n"); 115 get_struct(env, in); 116 break; 117 case (XMLRPC_TYPE_C_PTR): 118 printf("Got a C pointer?!\n"); 119 break; 120 case (XMLRPC_TYPE_DEAD): 121 printf("Got a 0xDEADr?!\n"); 122 break; 123 default: 124 printf("UNKNOWN XML-RPC DATATYPE\n"); 125 } 126 } |
Listing 9 is a bit long, but it's a pretty simple switch
statement. The XML-RPC library defines several constants with which the
xmlrpc_value
that was passed in can be identified. Most of these types should
be self-evident. In order to map XML-RPC values to C values, we need to call
xmlrpc_parse_value
. Examples of this will be seen in the small helper
functions that begin with "get_". The last two cases are internal datatypes that won't
normally occur, but are shown for the sake of completeness.
Listing 9: xmlrpc_debug.c, part 8
127 int get_int(xmlrpc_env* env, xmlrpc_value* in){ 128 int i; 129 xmlrpc_parse_value(env, in, "i", &i); 130 die_if_fault_occurred(env); 131 return(i); 132 } 133 134 135 int get_boolean(xmlrpc_env* env, xmlrpc_value* in){ 136 int i; 137 xmlrpc_parse_value(env, in, "b", &i); 138 die_if_fault_occurred(env); 139 return(i); 140 } 141 142 double get_double(xmlrpc_env* env, xmlrpc_value* in){ 143 double d; 144 xmlrpc_parse_value(env, in, "d", &d); 145 die_if_fault_occurred(env); 146 return(d); 147 } 148 149 150 char* get_timestamp(xmlrpc_env* env, xmlrpc_value* in){ 151 char *s; 152 153 xmlrpc_parse_value(env, in, "8", &s); 154 die_if_fault_occurred(env); 155 return(s); 156 } 157 158 char* get_string(xmlrpc_env* env, xmlrpc_value* in){ 159 char* s; 160 161 xmlrpc_parse_value(env, in, "s", &s); 162 die_if_fault_occurred(env); 163 return(s); 164 } 165 166 char* get_base64(xmlrpc_env* env, xmlrpc_value* in) { 167 char *s; 168 169 xmlrpc_parse_value(env, in, "6", &s); 170 die_if_fault_occurred(env); 171 return(s); 172 } 173 |
Listing 10 shows how to handle the simplest aggregate type, the
array. Naively calling xmlrpc_parse_value
won't work here because each element
can be a different data type. Fortunately, the size of the array can be determined
with a
call to xmlrpc_array_size
. Then, it's a simple matter to iterate through the
array, retrieving each element with a call to xmlrpc_array_get
. In a fine
display of code reuse, this element is passed to print_values
.
Listing 10: xmlrpc_debug.c, part 9
174 void get_array(xmlrpc_env* env, xmlrpc_value* in){ 175 int i, size = 0; 176 xmlrpc_value *el; 177 178 size = xmlrpc_array_size(env, in); 179 die_if_fault_occurred(env); 180 181 for(i=0; i < size; i++){ 182 el = xmlrpc_array_get_item( env, in, i); 183 die_if_fault_occurred(env); 184 print_values(env, el); 185 } 186 } 187 |
Listing 11 demonstrates how to deal with structs. Much like
arrays, you can iterate over key-value pairs in structs. The function
xmlrpc_struct_size
returns the number of key-value pairs available. A call to
xmlrpc_get_key_and_value
will point the two passed-in pointers to
xmlrpc_value
pointers at the n-th key-value pair. Although keys are almost
always strings, I err on the side of caution here by asking print_values
to
display the key's value.
Listing 11: xmlrpc_debug.c, part 10
188 void get_struct(xmlrpc_env* env, xmlrpc_value* in){ 189 int i, size = 0; 190 xmlrpc_value *key, *value; 191 192 size = xmlrpc_struct_size(env, in); 193 die_if_fault_occurred(env); 194 195 for(i=0; i < size; i++){ 196 xmlrpc_struct_get_key_and_value(env, 197 in, 198 i, 199 &key, 200 &value); 201 die_if_fault_occurred(env); 202 203 printf("\tkey: "); 204 print_values(env, key); 205 printf("\tvalue: "); 206 print_values(env, value); 207 } 208 } 209 |
The program concludes with Listing 12. This is a routine that conveniently catches errors. It was also ripped in whole from the example code in the XML-RPC C library.
Listing 12: xmlrpc_debug.c, part 11
210 void die_if_fault_occurred (xmlrpc_env *env){ 211 212 if (env->fault_occurred) { 213 fprintf(stderr, "XML-RPC Fault: %s (%d)\n", 214 env->fault_string, env->fault_code); 215 exit(1); 216 } 217 } |
The Makefile
• Binary Data to Go: Using XML-RPC to Serve Up Charts on the Fly |
Once the source code is ready, you'll want to compile it. There are a lot of shared libraries that need to be linked into any XML-RPC client program. Eric Kidd's documentation suggests that you initialize your variables like this:
$ CLIENT_CFLAGS=`xmlrpc-c-config libwww-client --cflags` $ CLIENT_LIBS=`xmlrpc-c-config libwww-client --libs`
I have simply expanded these so that I can track down missing library errors more easily. Listing 13 shows the barebones Makefile used to compile this debug client. All warnings are turned on and it should compile without warnings under gcc.
1 CLIENT_CFLAGS=-I/usr/include 2 CLIENT_LIBS=-L/usr/lib -lxmlrpc_client \ 3 -lwwwxml -lxmltok -lxmlparse -lwwwzip \ 4 -lwwwinit -lwwwapp -lmd5 -lwwwhtml -lwwwtelnet \ 5 -lwwwnews -lwwwhttp -lwwwmime -lwwwgopher -lwwwftp \ 6 -lwwwfile -lwwwdir -lwwwcache -lwwwstream -lwwwmux \ 7 -lwwwtrans -lwwwcore -lwwwutils -ldl -lz \ 8 -lxmlrpc -lxmlrpc_xmlparse -lxmlrpc_xmltok -Wl,--rpath -Wl,/usr/lib 9 10 CFLAGS=-Wall -ansi -pedantic -g 11 12 debug: 13 gcc $(CFLAGS) $(CLIENT_CFLAGS) -o \ xmlrpc_debug xmlrpc_debug.c $(CLIENT_LIBS) |
A Simple Perl Server
The test harness for the debug client is a simple Perl XML-RPC server. It returns sample data of various formats. Like all Frontier::Daemon programs, it runs as a single-threaded HTTP server, listening, in this case, to TCP port 3080. The remote procedures this server handles are given in the methods hash, which is passed to Frontier::Daemon during initialization. Because these functions are so simple, anonymous subroutines are used instead of creating named subroutines and passing references to them. Normally, the Frontier::RPC2 library is good about guessing how to encode Perl scalars, but base64 strings need manual intervention. Line 26 in Listing 14 below shows this in action.
1 #!/usr/bin/perl -- 2 # just return sample data 3 4 use strict; 5 use Frontier::Daemon; 6 use MIME::Base64; 7 8 use constant PORT => 3080; 9 use constant SERVER_URL => 'http://localhost:'.PORT.'/RPC2'; 10 11 print "Starting: ", SERVER_URL, "\n"; 12 my $coder = Frontier::RPC2->new; 13 Frontier::Daemon->new( 14 methods => { 15 str => sub{ return "AB "x512 }, 16 int => sub{ return 12345 }, 17 double => sub{ return 1.234 }, 18 array => sub{ return [(0..128)] }, 19 struct => sub{ return { 20 key1 => 1, 21 key2 => "value2", 22 key3 => 4.56, 23 } 24 }, 25 base64 => sub{ return 26 $coder->base64( 27 encode_base64('I love Lucy') 28 ) 29 }, 30 31 mixed => sub { 32 return [ 33 { key1 => 1.0 }, 34 { key2 => 2.0 }, 35 { key3 => 'three'}, 36 ]; 37 }, 38 }, 39 40 LocalPort => PORT, 41 Reuse => 1, 42 ); |
What's Next?
This code should give the aspiring C XML-RPC programmer a good starting point for building a real application. It's hard to beat a C program for execution speed. A program similar to the debug client shown here, but written in Perl or Python, will run five to ten times slower. Of course, the Perl or Python script is five to ten times faster to write. You can save either CPU time or programmer time, but not both. Happy hacking.
Joe Johnston is an independent contractor and freelance writer. A graduate of the University of Massachusetts in Boston with a B.A. in computer science, he is a teacher, a Web designer, and an author of articles for the Perl Journal and other publications. Joe coauthored O'Reilly's Programming Web Services with XML_RPC. He can be emailed at jjohn@cs.umb.edu.
O'Reilly & Associates recently released (June 2001) Programming Web Services with XML-RPC.
-
Sample Chapter 3, Client-Server Communication: XML-RPC in Java, is available free online.
-
You can also look at the Table of Contents, the Index, and the Full Description of the book.
-
For more information, or to order the book, click here.