XML-RPC: Calling Everything, Everywhere, and at Once

We had fifty operating systems, a dozen programming languages, and an endless number of libraries and frameworks of all sorts and colors, as well as coffee, a little time, and a pinch of common sense. Not that this was a necessary reserve for network development, but once you start collecting game, it’s hard to stop.

From left to right: a Python client, a running standalone server, a Java client, and at the very bottom a C++ client, all merged into a single burst of network call.

From left to right: a Python client, a running standalone server, a Java client, and at the very bottom a C++ client, all merged into a single burst of network call.

Round trip calls

What do you know about remote procedure calls?

Just be honest and without searching on Google?

If you are from the younger generation of developers, you have hardly even heard of this term – in modern development they have been replaced by REST and JSON, which are used with or without reason.

Many of the older readers will remember the monstrous SOAP. They will remember and immediately shudder – from the memories of numerous failures, generation of a bunch of garbage code from WSDL and sudden differences in implementation (hello Microsoft), popping up at the most inopportune moment during project delivery.

Even older representatives of the profession will remember the network COM+ And CORBA – even more creepy and even more difficult to use.

But there is something that unites all generations of developers, namely the idea born of practice:

to call a method from one program to another over the network is difficult

Therefore, trying to do this yourself, without any ready-made solutions, is definitely not worth it. Well, it's time to destroy myths again, attention to the screen:

Total ~500 lines of Java code on the server and client parts and you can easily call methods on any OS and in any environment.

As usual, without any external libraries or frameworks.

Why didn't anyone tell me about this during those sleepless nights, killed on cursed SOAR? Where were all the “mega-experts on all issues”, gurus and development professionals then?

Of course they were, but the trend in the 90s and 2000s towards deliberately overcomplicating corporate software simply didn’t give them a chance.

XML-RPC

I'll start with quotes:

XML-RPC (from English eXtense Markup Language Remote Procedure Call (XML Remote Procedure Call) – standard/protocol remote procedure callusing XML to encode their messages and HTTP as a transport mechanism[1]

Is the progenitor SOAPis distinguished by its exceptional ease of use.

XML-RPC, like any other Remote Procedure Call (RPC) interface, defines a set of standard data types and commands that a programmer can use to access the functionality of another program located on friend computer V networks.

In fact, behind these dry lines lies another epic story:

The XML-RPC protocol was originally developed by Dave Winer of UserLand Software in collaboration with MicrosoftV 1998However, Microsoft soon found this protocol too simplistic and began to expand its functionality.

Doesn't this remind you of anything?

For example, the famous story with extensions for HTML from Microsoft. Or from extensions for C++ from Microsoft, or with CSS extensions from Microsoft – I think you understand how much this wonderful company loves to expand everything.

After several cycles of expanding functionality, the system now known as SOAP emerged.

Yes, now you also know how it came into being and whose ears are sticking out of WSDL and code generation.

Well, here's the logical ending:

Later, Microsoft began to widely promote and implement SOAP, and the original XML-RPC was abandoned.

Despite all the efforts made (for example, there is still no official implementation of XML-RPC for any of the Microsoft products, only those created by third-party developers), the XML-RPC project was never completely buried – the protocol is alive and actively used to this day, including by the author himself.

This is what the challenge looks like:

<?xml version="1.0"?>
 <methodCall>
   <methodName>examples.getStateName</methodName>
   <params>
     <param>
         <value><i4>41</i4></value>
     </param>
   </params>
 </methodCall>

With such simplicity of implementation, XML-RPC is available for almost all known languages ​​that support networking (which I will show below), and XML-RPC has helped the author of this article more than once in projects where using “big” SOAP was problematic and implementing your own protocol or exchanging files was too awkward or time-consuming.

In a word, XML-RPC is a very important and necessary tool for any professional developer, in any language and in any environment, since it provides a predictable option for interaction between systems, even very distant and maximally incompatible ones.

Project

I decided to implement my own version of the XML-RPC client and server, in the smallest possible size and, of course, without any dependencies.

The source code, along with usage examples, is available at Github.

This project is another illustration of the well-known truth that “at the heart of all complex things lie very simple principles.”

New features of Java 17 were used for development, but due to the size and extreme simplicity of the project, everything is easily ported even to Java 1.4 and will definitely work in any environment.

Even XML generation is fully implemented. manuallyand the standard one SAX parser used only for parsing incoming requests.

This implementation can also be used as a model for repetition in any arbitrary language and in any environment that supports network operation – it is that simple.

Library

Actually, the entire library implementing both the client and server sides of XML-RPC consists of three files:

Of course, the internal structure is somewhat more complex and there are nested classes, but the general logic is as follows:

  • XmlRPC — contains common logic for the client and server for parsing requests and generating XML-RPC responses;

  • XmlRpcClient — contains the logic of the client side, primarily the connection and formation of a request to the server;

  • XmlRpcServer — contains server logic, the main one of which is the direct calling of methods.

I'll go over the key parts.

The first thing that catches your eye is this enumeration, which contains a list of all XML-RPC data types:

enum DATA_TYPES { String,Integer,Boolean,Double,
                  Date,Base64,Struct,Array,Nil }  

And a predefined date format:

private static final SimpleDateFormat 
     XMLRPC_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");

You won't believe it, but in general this is All Description of XML-RPC types, everything is so simple here.

This is what it looks like forming a server response:

..
void writeObject(Object what, XmlWriter writer) {
   writer.startEl("value");
  if (what == null) 
     writer.emptyEl("nil");
  else if (what instanceof String) 
     writer.write(what.toString(), true);
  else if (what instanceof Integer)  
     writer.writeEl("int", what.toString());
  else if (what instanceof Boolean b)  
     writer.writeEl("boolean", (b ? "1" : "0"));
  else if (what instanceof Double || what instanceof Float) 
     writer.writeEl("double", what.toString());
  else if (what instanceof Date d) 
     writer.writeEl("dateTime.iso8601", XMLRPC_DATE_FORMAT.format(d));
  else if (what instanceof byte[] b) 
     writer.writeEl("base64", Base64.getEncoder().encodeToString(b));
  else if (what instanceof List<?> v) {
     writer.startEl("array").startEl("data"); 
     for (Object o : v) 
              writeObject(o, writer);
     writer.endEl("data").endEl("array");
  } else if (what instanceof Map<?, ?> h) { 
     writer.startEl("struct");
     for (Map.Entry<?, ?> e : h.entrySet()) {
         if (!(e.getKey() instanceof String nk)) 
                  continue; 
         final Object nv = e.getValue();
         writer.startEl("member").startEl("name")
               .write(nk, false).endEl("name");
         writeObject(nv, writer); writer.endEl("member");
         } 
         writer.endEl("struct");
   } else 
           throw new RuntimeException("unknown type: %s"
                  .formatted(what.getClass())); 
   writer.endEl("value");
 }
 ..

Yes, this one method contains the entire logic of forming the answer.

We simply check the type of data being returned sequentially and manually generate XML tags.

And that's all.

Processing incoming requests It's a bit longer due to the use of a streaming SAX parser, so I'll just list the key parts:

..
@Override
public void startElement(String uri, String localName,
                                 String qName, Attributes attributes) {
            if (LOG.isLoggable(Level.FINE)) 
               LOG.fine("startElement: %s".formatted(qName));
            switch (qName) { 
              case "fault" -> this.fault = true;
              case "value" -> { 
                    final Value v = new Value(); 
                    this.values.push(v);
                    this.cvalue = v; 
                    this.cdata.setLength(0); 
                    this.readCdata = true; 
                    }
                case "methodName", "name", "string" -> { 
                    this.cdata.setLength(0); 
                    this.readCdata = true;
                 }
                case "i4", "int" -> { 
                    this.cvalue.setType(DATA_TYPES.Integer);
                    this.cdata.setLength(0); 
                    this.readCdata = true; 
                    }
                case "boolean" -> { 
                    this.cvalue.setType(DATA_TYPES.Boolean);
                    this.cdata.setLength(0); 
                    this.readCdata = true; 
                    }
                case "double" -> { 
                    this.cvalue.setType(DATA_TYPES.Double);
                    this.cdata.setLength(0); 
                    this.readCdata = true;
                     }
                case "dateTime.iso8601" -> {
                    this.cvalue.setType(DATA_TYPES.Date);
                    this.cdata.setLength(0); this.readCdata = true;
                      }
                case "base64" -> {
                    this.cvalue.setType(DATA_TYPES.Base64); 
                    this.cdata.setLength(0); this.readCdata = true;
                     }
                case "struct" -> this.cvalue.setType(DATA_TYPES.Struct);
                case "array" -> this.cvalue.setType(DATA_TYPES.Array);
                case "nil" -> this.cvalue.setType(DATA_TYPES.Nil);
            }
}
..

Here the tag name is compared to the supported type from the enumeration, for example, when parsing a block like this, the DATA_TYPES.Integer type will be set:

 <value><i4>41</i4></value>

The value itself will be converted according to the type. just below:

...
public void characterData(String cdata) { 
      switch (this.type) {
          case Integer -> this.value = Integer.valueOf(cdata.trim());
          case Boolean -> this.value = "1".equals(cdata.trim());
          case Double -> this.value = Double.valueOf(cdata.trim());
          case Date -> { 
              try { 
                   this.value = XMLRPC_DATE_FORMAT.parse(cdata.trim());
                   } catch (ParseException p) { 
                       throw new RuntimeException(p.getMessage()); 
                   } 
                }
                case Base64 -> this.value = Base64.getDecoder()
                   .decode(cdata.getBytes());
                case String -> this.value = cdata; 
                case Struct -> nextMemberName = cdata;
                default -> throw new IllegalStateException(
                   "Unexpected value: %s".formatted(this.type)); 
            } 
 }
 ...

The above method characterData is called when completion of element processing:

 ..
 @Override
 public void endElement(String uri, String localName, String qName) {
     if (LOG.isLoggable(Level.FINE)) 
          LOG.fine("endElement: %s".formatted(qName));
     if (this.cvalue != null && this.readCdata) {
                this.cvalue.characterData(this.cdata.toString()); 
                this.cdata.setLength(0); this.readCdata = false;
     }
  }
 ..

And… that's it.

This all XML-RPC processing.

Also worth mentioning is the work with the pool of executors (Worker) both on the client and server side, as well as the logic of calling the method using the “Reflection API”.

Pool of performers

Both the client and the server contain this such a variable class:

private final Deque<ServerWorker> pool = new ArrayDeque<>();

This is a pool that stores ready-to-use copies of “executors” – special classes responsible for processing requests.

This was done for load management – to prevent failure due to too many requests being executed. After all, calling a method is not a file transfer, any call can lead to unpredictable results.

It is obviously impossible to cache method call responses, unless of course you want universality

Both on the client and on the server request processing logic looks the same:

..
public byte[] execute(InputStream is, String user, String password) {
        final ServerWorker serverWorker = getWorker();
        // execute call
        try { return serverWorker.execute(is, user, password); } finally {
            this.pool.push(serverWorker);  // push worker back to pool
        }
}
..

First, the first available executor is taken from the pool, it is used for processing, and at the end it is returned to the pool again.

The difference on the client side is in the additional error check in the server response:

..
if (!clientWorker.fault) 
       this.pool.push(clientWorker); 
..

If such an error exists, the performer is not returned to the pool.

Calls and Reflection API

The second interesting feature of the implementation is the method call itself through Reflection API:

..
public Object execute(String methodName, List<Object> params) 
                                             throws Exception {
            final List<Class<?>> argClasses = new ArrayList<>(); 
            final List<Object> argValues = new ArrayList<>();
            if (params != null && !params.isEmpty()) {
                // here we check provided params and try 
                // to unwrap basic types
                for (final Object v : params) { argValues.add(v);
                    if (LOG.isLoggable(Level.FINE))
                            LOG.fine("param class: %s value=%s"
                               .formatted(v.getClass().getName(), v));
                    argClasses.add(v.getClass().isPrimitive()
                            ? MethodType.methodType(v.getClass())
                            .unwrap().returnType() : v.getClass());
                }
            }
            final Method method; // method to call
            if (LOG.isLoggable(Level.FINE)) {  
                LOG.fine("Calling method: %s".formatted(methodName));
                for (int c = 0; c < argClasses.size(); c++)
                    LOG.fine("Parameter %d: %s = %s"
                     .formatted(c, argClasses.get(c), argValues.get(c)));
            }
            // get method via 'Reflection API'
            method = this.targetClass.getMethod(methodName, 
                          argClasses.toArray(new Class[0]));
            try {
                // and try to invoke
                return method.invoke(this.invokeTarget, 
                       argValues.toArray(new Object[0]));
            } catch (InvocationTargetException it_e) {
                throw new RuntimeException(it_e.getTargetException());
            }
        }
    }
 ..

Here it is worth paying attention to the following code:

..
argClasses.add(v.getClass().isPrimitive()
                            ? MethodType.methodType(v.getClass())
                               .unwrap().returnType() : v.getClass());
..                            

It unwraps primitive argument types into their “wrapper” versions:

int -> Integer, boolean -> Boolean and so on.

Such simple logic requires that all handler methods called via the Reflection API have only wrapper classes as parameters, not primitives.

To put it simply, this is how it will work:

..
public Map<String, Object> sumAndDifference(Integer x, Integer y) {
            final Map<String, Object> result = new HashMap<>();
            result.put("sum", x + y);
            result.put("difference", x - y);
            return result;
}
..

but this is no longer the case:

..
public Map<String, Object> sumAndDifference(int x, int y) {
            final Map<String, Object> result = new HashMap<>();
            result.put("sum", x + y);
            result.put("difference", x - y);
            return result;
}
..

Such a serious simplification is required in order to immediately obtain the required method based on its signature alone:

 method = this.targetClass.getMethod(methodName, 
                          argClasses.toArray(new Class[0]));

Without searching and going through options, like such:

..
Method[] allMethods = c.getDeclaredMethods();
	    for (Method m : allMethods) {
		String mname = m.getName();
		if (!mname.startsWith("test")
		    || (m.getGenericReturnType() != boolean.class)) {
		    continue;
		}
..		

Now let's move on to usage examples.

XML-RPC Server Example

I'll start with the most important thing – with the implementation of a standalone XML-RPC server based on this wonderful library.

Source code (as much as one class) SampleServer) is in the subproject tiny-xmlrpc-library-sample-server:

All logic except handlers is located inside the method main:

public static void main(String[] args) throws IOException {
        //check for 'appDebug' parameter
        boolean debugMessages = Boolean
               .parseBoolean(System.getProperty("appDebug", "false"));
        // adjust logging levels to show more messages, 
        // if appDebug was set
        if (debugMessages) { 
            LOG.setUseParentHandlers(false);
            final Handler systemOut = new ConsoleHandler();
            systemOut.setLevel(Level.FINE);
            LOG.addHandler(systemOut); LOG.setLevel(Level.FINE);
            LOG.fine("debug messages enabled");
        }
        // create HTTP-server
        final HttpServer server = HttpServer
                        .create(new InetSocketAddress(8000), 50);
       // initialize default handler
       final DefaultServerHttpHandler dsh = new DefaultServerHttpHandler();
        // add some demo handlers
        dsh.addHandler("example", new DemoXmlRpcHandler());
        // one with authentication enabled
        dsh.addHandler("auth", new SampleAuthenticatedXmlRpcHandler());
        // setup default XML-RPC handler
        dsh.addHandler("$default", new DefaultXmlRpcHandler());
        server.createContext("/", dsh);
        server.setExecutor(null); // creates a default executor
        LOG.info("Started  XML-RPC server on http://%s:%d"
            .formatted(server.getAddress().getHostName(),
                server.getAddress().getPort()));
        server.start(); //finally that the server
    }    

As you can see, the implementation uses the class com.sun.net.httpserverwhich has been built into the JDK/JRE since time immemorial and implements a very simple HTTP server.

Main handlerwhich connects the server with the XML-RPC processing logic looks like this:

..
public static class DefaultServerHttpHandler implements HttpHandler {
        // an instance of XmlRpcServer
        private final XmlRpcServer xrs = new XmlRpcServer(); 
        /**
         * Binds provided handler to XML-RPC server instance
         * @param handlerName
         *              a handler's unique name
         * @param h
         *          handler instance
         */
        public void addHandler(String handlerName, Object h) { 
          this.xrs.addHandler(handlerName, h); 
        }
        /**
         * Handles input HTTP request
         * @param t the exchange containing the request from the
         *                 client and used to send the response
         * @throws IOException
         *          on I/O errors
         */
        public void handle(HttpExchange t) throws IOException {
            // ignore all non POST requests
            if (!"POST".equals(t.getRequestMethod())) {
                t.sendResponseHeaders(400, 0); 
                t.close(); 
                return;
            }
            if (LOG.isLoggable(Level.FINE))
                LOG.fine("got http request: %s"
                        .formatted(t.getRequestURI()));
            // process request
            try (OutputStream so = t.getResponseBody()) {
             String[] creds = null; // check for Basic Auth
             if (t.getRequestHeaders().containsKey("Authorization"))
              creds = this.xrs.extractCredentials(
               t.getRequestHeaders().get("Authorization").get(0));
                // execute call and get result 
                // (there would be XML encoded in byte array)
                final byte[] result = creds!=null? 
                 this.xrs.execute(t.getRequestBody(),creds[0],creds[1]) :
                       this.xrs.execute(t.getRequestBody());           
                // set response 'content-type' header
                t.getResponseHeaders().add("Content-type", "text/xml");
               // send headers
                t.sendResponseHeaders(200, result.length);
                // send body
                so.write(result); so.flush();
            } catch (Exception e) {
                LOG.warning(e.getMessage());
            }
        }
    }
 ..   

The whole logic consists of forwarding POST requests for their subsequent processing in the class XmlRpcServer:

final byte[] result = this.xrs.execute(t.getRequestBody());              

and subsequent delivery of the finished result of the call to the client:

t.getResponseHeaders().add("Content-type", "text/xml");
// send headers
t.sendResponseHeaders(200, result.length);
// send body
so.write(result); so.flush();

Also implemented as an example are several test methods for calling from outside via XML-RPC, for example to check authorization:

..
static class SampleAuthenticatedXmlRpcHandler
            implements XmlRPC.AuthenticatedXmlRpcHandler {
        public Object execute(String method, 
               List<Object> v, 
               String user, String password) throws Exception {
            i
            f ("admin".equals(user) && "admin1".equals(password))
                                return "Hello %s".formatted(user);
            throw new XmlRPC.XmlRpcException(5, "Access denied");
        }
}
..

This is what the call looks like from the client side:

 XmlRpcClient clientAuth = new XmlRpcClient(
                   new URL("https://localhost:8000"));
 //set auth credentials
 clientAuth.setBasicAuthentication("admin","admin1");
 System.out.println(clientAuth.execute("auth.execute", List.of(1,2)));

In addition to the authorization handler, another one was added one test — in the form POJO:

public static class DemoXmlRpcHandler {
        /**
         * Sample method, to call from XML-RPC
         * @param x
         *          some integer
         * @param y
         *          some another integer
         * @return
         *      a map with 2 properties: sum - would contain sum of two provided integers
         *                               difference - would be x - y result
         */
        public Map<String, Object> sumAndDifference(Integer x, Integer y) {
            final Map<String, Object> result = new HashMap<>();
            result.put("sum", x + y);
            result.put("difference", x - y);
            return result;
        }
 }

There are no interfaces or annotations here – all control over the creation of this class is entirely on the developer's side, which means that such a handler can easily be, for example, a container-managed Spring bean.

The call from the client side looks like this:

XmlRpcClient client2 = new XmlRpcClient(new URL("https://localhost:8000"));
  System.out.println(client2.execute("example.sumAndDifference", 
   List.of(15,55)));

Call result:

{difference=-40, sum=70}

XML-RPC server based on Jakarta Servlet

The next example will be implementation of a server based on a regular servlet:

It is even smaller and simpler:

public class SampleServlet extends HttpServlet {
    // an XML-RPC server instance
    protected XmlRpcServer xmlrpc = new XmlRpcServer();
    @Override
    public void init(ServletConfig config) {
        //register our sample handler
        this.xmlrpc.addHandler("hello", new DemoXmlRpcHandler());
    }
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res) 
                            throws IOException {
        // execute XML-RPC call and get response as byte array
        final byte[] result = this.xmlrpc.execute(req.getInputStream());
        // set response content type and length
        res.setContentType("text/xml"); 
        res.setContentLength(result.length);
        // respond to client
        try (ServletOutputStream so = res.getOutputStream()) { 
          so.write(result); so.flush(); 
          }
    }    
}

The logic of operation is completely identical to the standalone version, with the exception that even the type of incoming request is not checked – the servlet itself overrides the method doPostwhich means it only responds to POST requests.

The test handler will return all input parameters in one line:

..
static class DemoXmlRpcHandler implements XmlRPC.XmlRpcHandler {
      
        public Object execute(String methodname, List<Object> params) {
            final StringBuilder out = new StringBuilder("Request was:\n");
            for (Object p :params)
                out.append("param: ").append(p).append('\n');
            return out.toString();
        }
}
..

This is what the client side looks like with the call:

XmlRpcClient client3 = new XmlRpcClient(
                new URL("https://localhost:8080/api/xmlrpc"));
  System.out.println(client3.execute("hello", List.of(15,55,33,77)));

Calls from other languages

Of course, the story about the flexibility and universality of XML-RPC would be incomplete without specific examples of work from other languages. Therefore, the second part of the article is entirely devoted to such examples using a variety of languages ​​and environments.

Remember about ~500 lines of source code and development from scratch – how many problems even such a simple project can solve.

All examples below are client-side, i.e. they implement a call to a test server via the XML-RPC protocol. All code is working at the time of writing, tested from different environments and with different sets of data.

Python 3

A standard package was used for the call. xmlrpcwhich comes with Python:

import xmlrpc.client 

with xmlrpc.client.ServerProxy("http://localhost:8000") as proxy:
    print("call result: %s" % str(proxy.example.sumAndDifference(22,9)))

Perl 5

The call is made using the module XML::RPCinstalled from CPAN:

use XML::RPC;
 
my $xmlrpc = XML::RPC->new('http://localhost:8000');
my $result = $xmlrpc->call( 'example.sumAndDifference', 
                        { state1 => 12, state2 => 28 } );

print $result;

Tcl

The package was used xmlrpc for Tcl, which is present in the repositories of all popular Linux and *BSD distributions:

package require xmlrpc

if {[catch {set res [xmlrpc::call "http://127.0.0.1:8000" 
              "" "example.sumAndDifference" { {int 221} {int 22} }]} e]} {
	puts "xmlrpc call failed: $e"
} else {
	puts "res: $res."
}

This package is also present in assemblies ActiveTcl for Windows.

Common Lisp

The library was used cxml-rpcthis is what the call looks like:

(xrpc:call "http://localhost:8000/" "example.sumAndDifference" 
                                     '(:integer 41 :integer 22))

For comparison, here is an example of calling an external test XML-RPC service:

(xrpc:call "http://betty.userland.com/RPC2" "examples.getStateName" 
                                     '(:integer 41))

C++

Cross-platform library was used ulxmlrpcppthe code is quite voluminous, but this is C++:

#include <ulxmlrpcpp/ulxmlrpcpp.h>
#include <ulxmlrpcpp/ulxr_tcpip_connection.h>
#include <ulxmlrpcpp/ulxr_http_protocol.h>
#include <ulxmlrpcpp/ulxr_requester.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <vector>
#include <sys/time.h>
#include <time.h>

int main(int argc, char **argv)
{
  const std::string ipv4 = "127.0.0.1";
  const unsigned port = 8000;
   
  ulxr::IP myIP;
  myIP.ipv4 = ipv4;
  
  ulxr::TcpIpConnection conn (ipv4, port,
              ulxr::TcpIpConnection::DefConnectionTimeout); 
  ulxr::HttpProtocol prot(&conn);

  ulxr::Requester client(&prot);

  ulxr::MethodCall testcall ("example.sumAndDifference");  
  testcall.addParam(ulxr::Integer(123));
  testcall.addParam(ulxr::Integer(21));

  ulxr::MethodResponse resp = client.call(testcall,"/"); 
  std::cout << "call result: \n"  << resp.getXml(0);
}

Assembly:

g++ -I/opt/src/ulxmlrpcpp test.cpp -o test-xml-rpc 
 /opt/src/ulxmlrpcpp/lib/libulxmlrpcpp.a  -lexpat -lssl -lcrypto -lpthread

Pure C

The most popular library was used xmlrpc-cwhich is present in most Linux distributions and other OS, even rare ones:


#include <stdlib.h>
#include <stdio.h>
#include <xmlrpc-c/base.h>
#include <xmlrpc-c/client.h>

static void
die_if_fault_occurred(xmlrpc_env *const envP,
                      const char *const fun)
{
    if (envP->fault_occurred)
    {
        fprintf(stderr, "%s failed. %s (%d)\n",
                fun, envP->fault_string, envP->fault_code);
        exit(-1);
    }
}

int main(int argc, char **argv)
{
    xmlrpc_env env;
    xmlrpc_value *resultP;
    const char *const method_name = "example.sumAndDifference"; 
    const char *const server_url = "http://localhost:8000";

    xmlrpc_env_init(&env);

    xmlrpc_client_init2(&env, XMLRPC_CLIENT_NO_FLAGS, 
                              "Test XML-RPC", "1.0", NULL, 0);
    die_if_fault_occurred(&env, "xmlrpc_client_init2()");
    
    resultP = xmlrpc_client_call(&env, server_url, method_name,
                                 "(ii)", 
                                 (xmlrpc_int32) 65, 
                                 (xmlrpc_int32) 17);
  
    die_if_fault_occurred(&env, "xmlrpc_client_call()");

    xmlrpc_int32 sum, difference;

    xmlrpc_decompose_value(&env, resultP, "{s:i,s:i,*}",
                       "sum", &sum,
                       "difference", &difference);

    printf("Result is sum: %d ,difference: %d\n", sum,difference);
    
    xmlrpc_DECREF(resultP);

    xmlrpc_env_clean(&env);

    xmlrpc_client_cleanup();

    return 0;
}

This is what the Makefile for the build looks like:

CC = clang
CFLAGS = -Wall -Ofast
LDFLAGS =

XMLRPC_C_CONFIG = xmlrpc-c-config

SOURCE_CLIENT		= test_client.c
EXECUTABLE_CLIENT	= test_client
OBJECTS_CLIENT		= $(SOURCE_CLIENT:.c=.o)
LIBS_CLIENT		= $(shell $(XMLRPC_C_CONFIG) client --libs)
INCLUDES_CLIENT		= $(shell $(XMLRPC_C_CONFIG) client --cflags)

.PHONY: all client clean

.SUFFIXES: .c .o

default: all

.c.o:
	$(CC) $(CFLAGS) -c $< -o $@

$(EXECUTABLE_CLIENT): $(OBJECTS_CLIENT)
	$(CC) $(LDFLAGS) $(LIBS_CLIENT) $(OBJECTS_CLIENT) -o $@

client: $(EXECUTABLE_CLIENT)

all: client

clean:
	rm -f $(OBJECTS_CLIENT)
	rm -f $(EXECUTABLE_CLIENT)

In progress:

Note the call tracing with display of XML request and response – one of the library's features xmlrpc-cincluded from the environment variable.

Ruby

Was used standard librarywhich is included in the Ruby distribution:

require 'xmlrpc/client'
require 'pp'

server = XMLRPC::Client.new2("http://localhost:8000")
result = server.call("example.sumAndDifference", 5, 3)	
pp result

Rust

The XML-RPC call is implemented using “crate” xmlrpcalthough it is difficult for me to judge how standard this method is:

extern crate xmlrpc;

use xmlrpc::{Request, Value};

fn main() {
    let req = Request::new("example.sumAndDifference").arg(22).arg(8);
    let res = req.call_url("https://127.0.0.1:8000");
    println!("Result: {:?}", res);
}

File for assembly Cargo.toml:

[package]
name = "xmlrpc-test"
version = "1.0.0"
edition = "2024"

[dependencies]
xmlrpc = "0.15.1"

Golang

The library was used go-xmlrpcunfortunately Gosha doesn't have a standard one:

package main

import (
	"fmt"

	"alexejk.io/go-xmlrpc"
)

func main() {
	client, _ := xmlrpc.NewClient("http://localhost:8000")

	req := &struct {
		Param1 int
		Param2 int
	}{
		Param1: 12,
		Param2: 45,
	}
	
	resp := &struct {
		Body struct {
			Sum        int
			Difference int
		}
	}{}
	_ = client.Call("example.sumAndDifference", req, resp)
	fmt.Printf("Results, sum: %d ,difference: %d \n",
		resp.Body.Sum, resp.Body.Difference)
}

Haskell

The package was used haxrthe implementation is quite complex, as is Haskell itself:

module Main where

import Network.XmlRpc.Client
import Network.XmlRpc.THDeriveXmlRpcType
import Network.XmlRpc.Internals

server = "http://localhost:8000"

data Resp = Resp { summary :: Int, difference :: Int } deriving Show

instance XmlRpcType Resp where
    fromValue v = do
		  t <- fromValue v
		  n <- getField "sum" t
		  a <- getField "difference" t
		  return Resp { summary = n, difference = a }


add :: String -> Int -> Int -> IO Resp
add url = remote url "example.sumAndDifference"

main = do
       let x = 4
           y = 7
       z <- add server x y
       putStrLn (show x ++ " + " ++ show y ++ " = " ++ show z)

Please note that the field in the structure is called “summary” and not “sum”, this was done to avoid redefine or hide the system sum.

Screenshot showing it in action:

Node.js

Of course everything works easily, simply and beautifully, the library was used davexmlrpc:

const xmlrpc = require ("davexmlrpc");

xmlrpc.client ("http://localhost:8000", 
      "example.sumAndDifference", [53,14], "xml", function (err, data) {
	console.log (err ? err.message : JSON.stringify (data));
});

Ready package.json for assembly:

{
  "name": "xmlrc-nodejs",
  "version": "1.0.0",
  "scripts": {
    "app": "node client.js"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "davexmlrpc": "^0.4.26"
  }
}

In progress:

C# and .NET

Oddly enough, for .NET all the XML-RPC implementations found turned out to be a bit abandoned, although working – apparently the long arm of Microsoft is still trying to kill this protocol.

The implementation was done using the library Kveer.XmlRPCwhich has the most installations in the Nuget repository:

using CookComputing.XmlRpc;

public class Program
{
    [XmlRpcUrl("https://localhost:8000")]
    public interface ISampleService: IXmlRpcProxy
    {
        [XmlRpcMethod("example.sumAndDifference")]
        XmlRpcStruct SumAndDifference(int num1, int num2);
    }
    public static void Main(string[] args)
    {
        ISampleService proxy = XmlRpcProxyGen.Create<ISampleService>();
        var res = proxy.SumAndDifference(41,26);
        Console.WriteLine($"response, sum: {res["sum"]}, " +
            $"difference: {res["difference"]}");
    }
}

In progress:

Free Pascal and Lazarus

With difficulty, but we still managed to revive and make the library work DXmlRpcwith XML-RPC implementation for both Delphi/Kylix and Lazarus.

Source code:

unit Hello;

interface

uses
  LCLIntf, LCLType, LMessages, Messages, SysUtils, 
  Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, XmlRpcTypes, XmlRpcClient;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  RpcCaller: TRpcCaller;
  RpcFunction: IRpcFunction;
  RpcResult: IRpcResult;
begin

  RpcCaller := TRpcCaller.Create;
  try
    RpcCaller.EndPoint := '/';
    RpcCaller.HostName := 'localhost';
    RpcCaller.HostPort := 8000;

    RpcFunction := TRpcFunction.Create;
    RpcFunction.ObjectMethod := 'example.sumAndDifference';
    RpcFunction.AddItem(21);
    RpcFunction.AddItem(5);


    RpcResult := RpcCaller.Execute(RpcFunction);
    if RpcResult.IsError then
      ShowMessageFmt('Error: (%d) %s', [RpcResult.ErrorCode,
          RpcResult.ErrorMsg])
    else
      ShowMessage('Success: ' + RpcResult.AsString);
  finally
    RpcCaller.Free;
  end;
end;
end.

This is what it looks like in action:

Total

With this article I wanted to demonstrate once again:

At the heart of all complex things are very simple ideas

It doesn't always make sense to bother with SOAP or modern REST+JSON implementations, despite general trends and “fashionability”.

A very simple protocol with fixed data types and minimal implementation can solve many practical problems without delving into the specifics of serializing complex data types, working with dates or fractional numbers.

Legacy environments, exotic OS, embedded systems – there are plenty of application options.

Enjoy 😉

This is a slightly edited and edited version of the article, original which is available on our blog.

0x08 Software

We are a small team of IT industry veterans, we create and refine a wide variety of software, our software automates business processes on three continents, in a wide variety of industries and conditions.

Bringing it to life long dead, we fix what never worked and we create impossible — then we talk about it in our articles.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *