Menu

Building XML-RPC Clients in C

October 31, 2001

Joe Johnston

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.

Table 1: XML-RPC Datatypes

Type Description
string ASCII string
int 32-bit signed integer
double double-precision floating point (exact range is implementation dependent)
boolean values can be 0 or 1
dateTime.iso8601 An ASCII string that is date/time stamp. Time-zone information must be negotiated separately
base64 An ASCII string which is base64 encoded
array An order sequence of any valid XML-RPC datatype
struct An unordered set of key-value pairs. Keys are ASCII strings. Values can be any XML-RPC datatype.

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

Resources

Binary Data to Go: Using XML-RPC to Serve Up Charts on the Fly

XML-RPC Home Page

Libwww: the W3C Sample Code Library

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.

Listing 13: Makefile




     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.

Listing 14: Perl Server




     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.